Compare commits

...

9 Commits

Author SHA1 Message Date
github-actions[bot] 9e1662f1d4 Release extension version 0.3.50 (#344)
Co-authored-by: GitHub Actions <github-actions@github.com>
2026-03-31 20:46:47 -05:00
eric sciple 5db2e80f32 Add entrypoint and command keys for service containers (#343)
Introduce service-container-mapping schema definition with entrypoint
and command properties, gated behind allowServiceContainerCommand
feature flag. Job containers remain unaffected.
2026-03-31 15:45:18 -05:00
github-actions[bot] 83de320ba9 Release extension version 0.3.49 (#342)
Co-authored-by: GitHub Actions <github-actions@github.com>
2026-03-20 09:47:56 -05:00
Angel Kou 74e6638098 Remove timezone feature flag in languageservice (#341)
* Remove timezone feature flag in languageservice

* Prettier

* Address comment

---------

Co-authored-by: Angel Kou <jiakou@microsoft.com>
2026-03-19 14:10:38 -07:00
github-actions[bot] f8b8b57248 Release extension version 0.3.48 (#340)
Co-authored-by: GitHub Actions <github-actions@github.com>
2026-03-18 11:02:31 -05:00
eric sciple aa1e7d8aec Add deployment key support for job environment (#338)
Add a boolean 'deployment' property to the job environment mapping.
When set to false, the parsed environment reference sets
skipDeployment to signal that no deployment record should be created.
2026-03-18 10:53:25 -05:00
github-actions[bot] bd6ce5923b Release extension version 0.3.47 (#336)
Co-authored-by: GitHub Actions <github-actions@github.com>
2026-03-10 11:38:05 -05:00
Tim Rogers 3de9820cd8 Add copilot-requests permission, gated by feature flag (#335)
* Add copilot-requests permission gated by feature flag

This add a new 'copilot-requests' permission to the workflow schema,
gated behind the 'allowCopilotRequestsPermission' experimental
feature flag.

When the flag is disabled (default), `copilot-requests` is filtered
out of autocomplete suggestions. When enabled, it appears
alongside other permissions like actions, contents, pull-requests,
etc.

* Update workflow-parser/src/workflow-v1.0.json

* Add additional unit test coverage

* Fix formatting
2026-03-10 09:48:54 -05:00
Angel Kou a7f581bde5 Add timezone to workflow and pass FF (#334)
* Add timezone to workflow and pass FF

* Prettier fixes

* Prettier fixes

* Prettier fixes

* Guard timezone autocomplete behind FF

* Prettier fix

* Address PR comments

* Prettier fix

* Remove comma

* Remove template assignment

* Move description

* Fix test

* Prettier again!

* Address comments

* Change error when timezone key is entered but FF is off

* Prettier

---------

Co-authored-by: Angel Kou <jiakou@microsoft.com>
2026-03-05 17:59:56 -08:00
22 changed files with 695 additions and 40 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/expressions",
"version": "0.3.46",
"version": "0.3.50",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
+3 -1
View File
@@ -54,7 +54,9 @@ describe("FeatureFlags", () => {
expect(flags.getEnabledFeatures()).toEqual([
"missingInputsQuickfix",
"blockScalarChompingWarning",
"allowCaseFunction"
"allowCaseFunction",
"allowCopilotRequestsPermission",
"allowServiceContainerCommand"
]);
});
});
+15 -1
View File
@@ -34,6 +34,18 @@ export interface ExperimentalFeatures {
* @default false
*/
allowCaseFunction?: boolean;
/**
* Enable the copilot-requests permission in workflow permissions.
* @default false
*/
allowCopilotRequestsPermission?: boolean;
/**
* Enable `entrypoint` and `command` keys in service containers (`jobs.<job_id>.services.*`).
* @default false
*/
allowServiceContainerCommand?: boolean;
}
/**
@@ -48,7 +60,9 @@ export type ExperimentalFeatureKey = Exclude<keyof ExperimentalFeatures, "all">;
const allFeatureKeys: ExperimentalFeatureKey[] = [
"missingInputsQuickfix",
"blockScalarChompingWarning",
"allowCaseFunction"
"allowCaseFunction",
"allowCopilotRequestsPermission",
"allowServiceContainerCommand"
];
export class FeatureFlags {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/languageserver",
"version": "0.3.46",
"version": "0.3.50",
"description": "Language server for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -48,8 +48,8 @@
"actions-languageserver": "./bin/actions-languageserver"
},
"dependencies": {
"@actions/languageservice": "^0.3.46",
"@actions/workflow-parser": "^0.3.46",
"@actions/languageservice": "^0.3.50",
"@actions/workflow-parser": "^0.3.50",
"@octokit/rest": "^21.1.1",
"@octokit/types": "^9.0.0",
"vscode-languageserver": "^8.0.2",
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/languageservice",
"version": "0.3.46",
"version": "0.3.50",
"description": "Language service for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -47,8 +47,8 @@
"watch": "tsc --build tsconfig.build.json --watch"
},
"dependencies": {
"@actions/expressions": "^0.3.46",
"@actions/workflow-parser": "^0.3.46",
"@actions/expressions": "^0.3.50",
"@actions/workflow-parser": "^0.3.50",
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.8",
+125
View File
@@ -925,3 +925,128 @@ jobs:
});
});
});
describe("schedule timezone completion", () => {
it("includes timezone for schedule", async () => {
const input = `on:
schedule:
- |`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("cron");
expect(labels).toContain("timezone");
});
});
describe("permissions copilot-requests completion", () => {
it("includes copilot-requests when allowCopilotRequestsPermission is enabled", async () => {
const input = `on: push
permissions:
|`;
const result = await complete(...getPositionFromCursor(input), {
featureFlags: new FeatureFlags({allowCopilotRequestsPermission: true})
});
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("actions");
expect(labels).toContain("copilot-requests");
});
it("excludes copilot-requests when allowCopilotRequestsPermission is disabled", async () => {
const input = `on: push
permissions:
|`;
const result = await complete(...getPositionFromCursor(input), {
featureFlags: new FeatureFlags({allowCopilotRequestsPermission: false})
});
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("actions");
expect(labels).not.toContain("copilot-requests");
});
it("excludes copilot-requests when no feature flags are provided", async () => {
const input = `on: push
permissions:
|`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("actions");
expect(labels).not.toContain("copilot-requests");
});
it("includes copilot-requests in job-level permissions when allowCopilotRequestsPermission is enabled", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
permissions:
|`;
const result = await complete(...getPositionFromCursor(input), {
featureFlags: new FeatureFlags({allowCopilotRequestsPermission: true})
});
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("actions");
expect(labels).toContain("copilot-requests");
});
it("excludes copilot-requests from job-level permissions when allowCopilotRequestsPermission is disabled", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
permissions:
|`;
const result = await complete(...getPositionFromCursor(input), {
featureFlags: new FeatureFlags({allowCopilotRequestsPermission: false})
});
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("actions");
expect(labels).not.toContain("copilot-requests");
});
});
describe("service container command/entrypoint completion", () => {
it("suggests entrypoint and command in service container", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis
|`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("entrypoint");
expect(labels).toContain("command");
});
it("does not suggest entrypoint and command in job container", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20
|`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).not.toContain("entrypoint");
expect(labels).not.toContain("command");
});
});
+10 -1
View File
@@ -116,7 +116,8 @@ export async function complete(
config,
{
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
errorPolicy: ErrorPolicy.TryConversion
errorPolicy: ErrorPolicy.TryConversion,
featureFlags: config?.featureFlags
},
true
);
@@ -163,6 +164,14 @@ export async function complete(
values = filterActionRunsCompletions(values, path, parsedTemplate.value);
}
// Filter `copilot-requests` from permissions completions when the feature flag is disabled
if (
!config?.featureFlags?.isEnabled("allowCopilotRequestsPermission") &&
parent?.definition?.key === "permissions-mapping"
) {
values = values.filter(v => v.label !== "copilot-requests");
}
// Offer "(switch to list)" / "(switch to mapping)" when the schema allows alternative forms
const escapeHatches = getEscapeHatchCompletions(token, keyToken, indentString, newPos, schema);
values.push(...escapeHatches);
+6 -2
View File
@@ -120,7 +120,9 @@ jobs:
`;
const result = await hover(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
expect(result?.contents).toEqual("");
expect(result?.contents).toEqual(
"A cron expression that represents a schedule. A scheduled workflow will run at most once every 5 minutes."
);
});
it("on an invalid cron schedule", async () => {
@@ -130,7 +132,9 @@ jobs:
`;
const result = await hover(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
expect(result?.contents).toEqual("");
expect(result?.contents).toEqual(
"A cron expression that represents a schedule. A scheduled workflow will run at most once every 5 minutes."
);
});
it("shows context inherited from parent nodes", async () => {
@@ -0,0 +1,147 @@
import {FeatureFlags} from "@actions/expressions";
import {registerLogger} from "./log.js";
import {createDocument} from "./test-utils/document.js";
import {TestLogger} from "./test-utils/logger.js";
import {clearCache} from "./utils/workflow-cache.js";
import {validate, ValidationConfig} from "./validate.js";
registerLogger(new TestLogger());
const configWithFlag: ValidationConfig = {
featureFlags: new FeatureFlags({allowServiceContainerCommand: true})
};
beforeEach(() => {
clearCache();
});
describe("service container command/entrypoint", () => {
describe("with feature flag enabled", () => {
it("allows command in service container", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis
command: --port 6380
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input), configWithFlag);
const commandErrors = result.filter(d => d.message.includes("command"));
expect(commandErrors).toEqual([]);
});
it("allows entrypoint in service container", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis
entrypoint: /usr/local/bin/redis-server
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input), configWithFlag);
const entrypointErrors = result.filter(d => d.message.includes("entrypoint"));
expect(entrypointErrors).toEqual([]);
});
it("allows both command and entrypoint in service container", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis
entrypoint: /usr/local/bin/redis-server
command: --port 6380
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input), configWithFlag);
const relevantErrors = result.filter(d => d.message.includes("command") || d.message.includes("entrypoint"));
expect(relevantErrors).toEqual([]);
});
it("rejects command in job container even with flag enabled", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20
command: node
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input), configWithFlag);
const commandErrors = result.filter(d => d.message.includes("command"));
expect(commandErrors.length).toBeGreaterThan(0);
});
it("rejects entrypoint in job container even with flag enabled", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
container:
image: node:20
entrypoint: /bin/bash
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input), configWithFlag);
const entrypointErrors = result.filter(d => d.message.includes("entrypoint"));
expect(entrypointErrors.length).toBeGreaterThan(0);
});
});
describe("with feature flag disabled", () => {
it("rejects command in service container", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis
command: --port 6380
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
const commandErrors = result.filter(d => d.message.includes("command"));
expect(commandErrors.length).toBeGreaterThan(0);
});
it("rejects entrypoint in service container", async () => {
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
services:
redis:
image: redis
entrypoint: /usr/local/bin/redis-server
steps:
- run: echo hi
`;
const result = await validate(createDocument("wf.yaml", input));
const entrypointErrors = result.filter(d => d.message.includes("entrypoint"));
expect(entrypointErrors.length).toBeGreaterThan(0);
});
});
});
+18
View File
@@ -368,6 +368,24 @@ jobs:
});
});
describe("environment deployment", () => {
it("allows deployment boolean under environment mapping", async () => {
const workflow = `
on: push
jobs:
build:
runs-on: ubuntu-latest
environment:
name: prod
deployment: false
steps:
- run: echo
`;
const result = await validate(createDocument("wf.yaml", workflow));
expect(result).toEqual([]);
});
});
describe("workflow_dispatch", () => {
it("allows empty string in choice options", async () => {
const result = await validate(
+2 -1
View File
@@ -84,7 +84,8 @@ async function validateWorkflow(textDocument: TextDocument, config?: ValidationC
// Errors will be updated in the context. Attempt to do the conversion anyway in order to give the user more information
const template = await getOrConvertWorkflowTemplate(result.context, result.value, textDocument.uri, config, {
fetchReusableWorkflowDepth: config?.fileProvider ? 1 : 0,
errorPolicy: ErrorPolicy.TryConversion
errorPolicy: ErrorPolicy.TryConversion,
featureFlags: config?.featureFlags
});
// Validate expressions and value providers
+1 -1
View File
@@ -6,5 +6,5 @@
"languageservice",
"languageserver"
],
"version": "0.3.46"
"version": "0.3.50"
}
+9 -9
View File
@@ -136,7 +136,7 @@
},
"expressions": {
"name": "@actions/expressions",
"version": "0.3.46",
"version": "0.3.50",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.0.3",
@@ -396,11 +396,11 @@
},
"languageserver": {
"name": "@actions/languageserver",
"version": "0.3.46",
"version": "0.3.50",
"license": "MIT",
"dependencies": {
"@actions/languageservice": "^0.3.46",
"@actions/workflow-parser": "^0.3.46",
"@actions/languageservice": "^0.3.50",
"@actions/workflow-parser": "^0.3.50",
"@octokit/rest": "^21.1.1",
"@octokit/types": "^9.0.0",
"vscode-languageserver": "^8.0.2",
@@ -927,11 +927,11 @@
},
"languageservice": {
"name": "@actions/languageservice",
"version": "0.3.46",
"version": "0.3.50",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.46",
"@actions/workflow-parser": "^0.3.46",
"@actions/expressions": "^0.3.50",
"@actions/workflow-parser": "^0.3.50",
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.8",
@@ -14020,10 +14020,10 @@
},
"workflow-parser": {
"name": "@actions/workflow-parser",
"version": "0.3.46",
"version": "0.3.50",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.46",
"@actions/expressions": "^0.3.50",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/workflow-parser",
"version": "0.3.46",
"version": "0.3.50",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
@@ -48,7 +48,7 @@
"watch": "tsc --build tsconfig.build.json --watch"
},
"dependencies": {
"@actions/expressions": "^0.3.46",
"@actions/expressions": "^0.3.50",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
+82
View File
@@ -578,4 +578,86 @@ jobs:
}
});
});
describe("schedule timezone", () => {
it("allows timezone in schedule", async () => {
const result = parseWorkflow(
{
name: "wf.yaml",
content: `on:
schedule:
- cron: '0 0 * * *'
timezone: America/New_York
jobs:
build:
runs-on: ubuntu-latest`
},
nullTrace
);
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
errorPolicy: ErrorPolicy.TryConversion
});
expect(result.context.errors.getErrors()).toHaveLength(0);
expect(template.events?.schedule).toHaveLength(1);
expect(template.events?.schedule?.[0]).toEqual({
cron: "0 0 * * *",
timezone: "America/New_York"
});
});
it("reports error when cron is missing from schedule entry", async () => {
const result = parseWorkflow(
{
name: "wf.yaml",
content: `on:
schedule:
- timezone: America/New_York
jobs:
build:
runs-on: ubuntu-latest`
},
nullTrace
);
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
errorPolicy: ErrorPolicy.TryConversion
});
// Both schema validation and converter report the missing cron
expect(result.context.errors.getErrors().length).toBeGreaterThanOrEqual(1);
const errorMessages = result.context.errors
.getErrors()
.map(e => e.message)
.join(", ");
expect(errorMessages).toMatch(/Required property is missing: cron|Missing required key 'cron'/);
expect(template.events?.schedule).toHaveLength(0);
});
it("converts schedule without timezone", async () => {
const result = parseWorkflow(
{
name: "wf.yaml",
content: `on:
schedule:
- cron: '0 0 * * *'
jobs:
build:
runs-on: ubuntu-latest`
},
nullTrace
);
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
errorPolicy: ErrorPolicy.TryConversion
});
expect(result.context.errors.getErrors()).toHaveLength(0);
expect(template.events?.schedule).toHaveLength(1);
expect(template.events?.schedule?.[0]).toEqual({
cron: "0 0 * * *"
});
});
});
});
+12 -2
View File
@@ -1,3 +1,4 @@
import {FeatureFlags} from "@actions/expressions/features";
import {TemplateContext} from "../templates/template-context.js";
import {TemplateToken, TemplateTokenError} from "../templates/tokens/template-token.js";
import {FileProvider} from "../workflows/file-provider.js";
@@ -37,12 +38,18 @@ export type WorkflowTemplateConverterOptions = {
* By default, conversion will be skipped if there are errors in the {@link TemplateContext}.
*/
errorPolicy?: ErrorPolicy;
/**
* Feature flags for experimental features.
*/
featureFlags?: FeatureFlags;
};
const defaultOptions: Required<WorkflowTemplateConverterOptions> = {
maxReusableWorkflowDepth: 4,
fetchReusableWorkflowDepth: 0,
errorPolicy: ErrorPolicy.ReturnErrorsOnly
errorPolicy: ErrorPolicy.ReturnErrorsOnly,
featureFlags: new FeatureFlags()
};
export async function convertWorkflowTemplate(
@@ -54,6 +61,8 @@ export async function convertWorkflowTemplate(
const result = {} as WorkflowTemplate;
const opts = getOptionsWithDefaults(options);
context.state.featureFlags = opts.featureFlags;
if (context.errors.getErrors().length > 0 && opts.errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
result.errors = context.errors.getErrors().map(x => ({
Message: x.message
@@ -142,6 +151,7 @@ function getOptionsWithDefaults(options: WorkflowTemplateConverterOptions): Requ
options.fetchReusableWorkflowDepth !== undefined
? options.fetchReusableWorkflowDepth
: defaultOptions.fetchReusableWorkflowDepth,
errorPolicy: options.errorPolicy !== undefined ? options.errorPolicy : defaultOptions.errorPolicy
errorPolicy: options.errorPolicy !== undefined ? options.errorPolicy : defaultOptions.errorPolicy,
featureFlags: options.featureFlags ?? defaultOptions.featureFlags
};
}
@@ -70,13 +70,91 @@ export function convertToJobContainer(context: TemplateContext, container: Templ
}
}
export function convertToServiceContainer(context: TemplateContext, container: TemplateToken): Container | undefined {
let image: StringToken | undefined;
let env: MappingToken | undefined;
let ports: SequenceToken | undefined;
let volumes: SequenceToken | undefined;
let options: StringToken | undefined;
let entrypoint: StringToken | undefined;
let command: StringToken | undefined;
// Skip validation for expressions for now to match
// behavior of the other parsers
for (const [, token] of TemplateToken.traverse(container)) {
if (token.isExpression) {
return;
}
}
if (isString(container)) {
image = container.assertString("container item");
return {image: image};
}
const mapping = container.assertMapping("container item");
if (mapping)
for (const item of mapping) {
const key = item.key.assertString("container item key");
const value = item.value;
switch (key.value) {
case "image":
image = value.assertString("container image");
break;
case "credentials":
convertToJobCredentials(context, value);
break;
case "env":
env = value.assertMapping("container env");
for (const envItem of env) {
envItem.key.assertString("container env value");
}
break;
case "ports":
ports = value.assertSequence("container ports");
for (const port of ports) {
port.assertString("container port");
}
break;
case "volumes":
volumes = value.assertSequence("container volumes");
for (const volume of volumes) {
volume.assertString("container volume");
}
break;
case "options":
options = value.assertString("container options");
break;
case "entrypoint":
entrypoint = value.assertString("container entrypoint");
break;
case "command":
command = value.assertString("container command");
break;
default:
context.error(key, `Unexpected container item key: ${key.value}`);
}
}
if (!image) {
context.error(container, "Container image cannot be empty");
} else {
return {image, env, ports, volumes, options, entrypoint, command};
}
}
export function convertToJobServices(context: TemplateContext, services: TemplateToken): Container[] | undefined {
const serviceList: Container[] = [];
const flags = context.state.featureFlags as import("@actions/expressions/features").FeatureFlags | undefined;
const useServiceContainer = flags?.isEnabled("allowServiceContainerCommand") ?? false;
const mapping = services.assertMapping("services");
for (const service of mapping) {
service.key.assertString("service key");
const container = convertToJobContainer(context, service.value);
const container = useServiceContainer
? convertToServiceContainer(context, service.value)
: convertToJobContainer(context, service.value);
if (container) {
serviceList.push(container);
}
+21 -10
View File
@@ -149,23 +149,34 @@ function convertFilter<T extends TypesFilterConfig & WorkflowFilterConfig & Vers
function convertSchedule(context: TemplateContext, token: SequenceToken): ScheduleConfig[] | undefined {
const result = [] as ScheduleConfig[];
for (const item of token) {
const mappingToken = item.assertMapping(`event schedule`);
if (mappingToken.count == 1) {
const schedule = mappingToken.get(0);
const scheduleKey = schedule.key.assertString(`schedule key`);
if (scheduleKey.value == "cron") {
const cron = schedule.value.assertString(`schedule cron`);
// Validate the cron string
const config: ScheduleConfig = {cron: ""};
let valid = true;
for (const entry of mappingToken) {
const key = entry.key.assertString(`schedule key`);
if (key.value === "cron") {
const cron = entry.value.assertString(`schedule cron`);
if (!isValidCron(cron.value)) {
context.error(cron, "Invalid cron expression. Expected format: '* * * * *' (minute hour day month weekday)");
}
result.push({cron: cron.value});
config.cron = cron.value;
} else if (key.value === "timezone") {
const timezone = entry.value.assertString(`schedule timezone`);
config.timezone = timezone.value;
} else {
context.error(scheduleKey, `Invalid schedule key`);
context.error(key, `Invalid schedule key`);
valid = false;
}
} else {
context.error(mappingToken, "Invalid format for 'schedule'");
}
if (valid && config.cron) {
result.push(config);
} else if (valid && !config.cron) {
context.error(mappingToken, "Missing required key 'cron' in schedule entry");
}
}
@@ -34,6 +34,14 @@ export function convertToActionsEnvironmentRef(
case "url":
result.url = property.value;
break;
case "deployment": {
const deploymentValue = property.value.assertBoolean("job environment deployment");
if (deploymentValue.value === false) {
result.skipDeployment = true;
}
break;
}
}
}
@@ -26,6 +26,7 @@ export type ConcurrencySetting = {
export type ActionsEnvironmentReference = {
name?: TemplateToken;
url?: TemplateToken;
skipDeployment?: boolean;
};
export type WorkflowJob = Job | ReusableWorkflowJob;
@@ -74,6 +75,8 @@ export type Container = {
ports?: SequenceToken;
volumes?: SequenceToken;
options?: StringToken;
entrypoint?: StringToken;
command?: StringToken;
};
export type Credential = {
@@ -196,6 +199,7 @@ export type SecretConfig = {
export type ScheduleConfig = {
cron: string;
timezone?: string;
};
export type WorkflowFilterConfig = {
+53 -2
View File
@@ -1602,6 +1602,10 @@
"type": "permission-level-any",
"description": "Repository contents, commits, branches, downloads, releases, and merges."
},
"copilot-requests": {
"type": "permission-level-write-or-no-access",
"description": "GitHub Copilot requests."
},
"deployments": {
"type": "permission-level-any",
"description": "Deployments and deployment statuses."
@@ -2075,6 +2079,10 @@
"url": {
"type": "string-runner-context-no-secrets",
"description": "The environment URL, which maps to `environment_url` in the deployments API."
},
"deployment": {
"type": "boolean",
"description": "Whether to create a deployment record for this environment. Defaults to true."
}
}
}
@@ -2391,7 +2399,7 @@
],
"one-of": [
"non-empty-string",
"container-mapping"
"service-container-mapping"
]
},
"container-registry-credentials": {
@@ -2620,14 +2628,57 @@
"cron-mapping": {
"mapping": {
"properties": {
"cron": "cron-pattern"
"cron": {
"type": "cron-pattern",
"required": true
},
"timezone": "timezone-string"
}
}
},
"cron-pattern": {
"description": "A cron expression that represents a schedule. A scheduled workflow will run at most once every 5 minutes.",
"string": {
"require-non-empty": true
}
},
"timezone-string": {
"description": "A string that represents the time zone a scheduled workflow will run relative to in IANA format (e.g. 'America/New_York' or 'Europe/London'). If omitted, the workflow will run relative to midnight UTC.",
"string": {
"require-non-empty": true
}
},
"service-container-mapping": {
"mapping": {
"properties": {
"image": {
"type": "non-empty-string",
"description": "The Docker image to use as the container. The value can be the Docker Hub image or a registry name."
},
"options": {
"type": "string",
"description": "Additional Docker container resource options."
},
"env": "container-env",
"ports": {
"type": "sequence-of-non-empty-string",
"description": "An array of ports to expose on the container."
},
"volumes": {
"type": "sequence-of-non-empty-string",
"description": "An array of volumes for the container to use. You can use volumes to share data between services or other steps in a job. You can specify named Docker volumes, anonymous Docker volumes, or bind mounts on the host."
},
"credentials": "container-registry-credentials",
"entrypoint": {
"type": "string",
"description": "Override the default ENTRYPOINT in the service container image."
},
"command": {
"type": "string",
"description": "Override the default CMD in the service container image."
}
}
}
}
}
}
@@ -0,0 +1,91 @@
include-source: false # Drop file/line/col from output
skip:
- C#
---
on: push
jobs:
build:
environment:
name: production
deployment: false
runs-on: ubuntu-latest
steps:
- run: echo hi
build2:
environment:
name: staging
deployment: true
runs-on: ubuntu-latest
steps:
- run: echo hi
---
{
"jobs": [
{
"type": "job",
"id": "build",
"name": "build",
"if": {
"type": 3,
"expr": "success()"
},
"environment": {
"type": 2,
"map": [
{
"Key": "name",
"Value": "production"
},
{
"Key": "deployment",
"Value": false
}
]
},
"runs-on": "ubuntu-latest",
"steps": [
{
"id": "__run",
"if": {
"type": 3,
"expr": "success()"
},
"run": "echo hi"
}
]
},
{
"type": "job",
"id": "build2",
"name": "build2",
"if": {
"type": 3,
"expr": "success()"
},
"environment": {
"type": 2,
"map": [
{
"Key": "name",
"Value": "staging"
},
{
"Key": "deployment",
"Value": true
}
]
},
"runs-on": "ubuntu-latest",
"steps": [
{
"id": "__run",
"if": {
"type": 3,
"expr": "success()"
},
"run": "echo hi"
}
]
}
]
}