Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e1662f1d4 | |||
| 5db2e80f32 | |||
| 83de320ba9 | |||
| 74e6638098 |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/expressions",
|
||||
"version": "0.3.48",
|
||||
"version": "0.3.50",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"source": "./src/index.ts",
|
||||
|
||||
@@ -55,8 +55,8 @@ describe("FeatureFlags", () => {
|
||||
"missingInputsQuickfix",
|
||||
"blockScalarChompingWarning",
|
||||
"allowCaseFunction",
|
||||
"allowCronTimezone",
|
||||
"allowCopilotRequestsPermission"
|
||||
"allowCopilotRequestsPermission",
|
||||
"allowServiceContainerCommand"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,17 +35,17 @@ export interface ExperimentalFeatures {
|
||||
*/
|
||||
allowCaseFunction?: boolean;
|
||||
|
||||
/**
|
||||
* Enable the timezone input in cron schedule mappings.
|
||||
* @default false
|
||||
*/
|
||||
allowCronTimezone?: 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,8 +61,8 @@ const allFeatureKeys: ExperimentalFeatureKey[] = [
|
||||
"missingInputsQuickfix",
|
||||
"blockScalarChompingWarning",
|
||||
"allowCaseFunction",
|
||||
"allowCronTimezone",
|
||||
"allowCopilotRequestsPermission"
|
||||
"allowCopilotRequestsPermission",
|
||||
"allowServiceContainerCommand"
|
||||
];
|
||||
|
||||
export class FeatureFlags {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/languageserver",
|
||||
"version": "0.3.48",
|
||||
"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.48",
|
||||
"@actions/workflow-parser": "^0.3.48",
|
||||
"@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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/languageservice",
|
||||
"version": "0.3.48",
|
||||
"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.48",
|
||||
"@actions/workflow-parser": "^0.3.48",
|
||||
"@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",
|
||||
|
||||
@@ -927,35 +927,7 @@ jobs:
|
||||
});
|
||||
|
||||
describe("schedule timezone completion", () => {
|
||||
it("includes timezone when allowCronTimezone is enabled", async () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- |`;
|
||||
const result = await complete(...getPositionFromCursor(input), {
|
||||
featureFlags: new FeatureFlags({allowCronTimezone: true})
|
||||
});
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
const labels = result.map(x => x.label);
|
||||
expect(labels).toContain("cron");
|
||||
expect(labels).toContain("timezone");
|
||||
});
|
||||
|
||||
it("excludes timezone when allowCronTimezone is disabled", async () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- |`;
|
||||
const result = await complete(...getPositionFromCursor(input), {
|
||||
featureFlags: new FeatureFlags({allowCronTimezone: false})
|
||||
});
|
||||
|
||||
expect(result).not.toBeUndefined();
|
||||
const labels = result.map(x => x.label);
|
||||
expect(labels).toContain("cron");
|
||||
expect(labels).not.toContain("timezone");
|
||||
});
|
||||
|
||||
it("excludes timezone when no feature flags are provided", async () => {
|
||||
it("includes timezone for schedule", async () => {
|
||||
const input = `on:
|
||||
schedule:
|
||||
- |`;
|
||||
@@ -964,7 +936,7 @@ describe("schedule timezone completion", () => {
|
||||
expect(result).not.toBeUndefined();
|
||||
const labels = result.map(x => x.label);
|
||||
expect(labels).toContain("cron");
|
||||
expect(labels).not.toContain("timezone");
|
||||
expect(labels).toContain("timezone");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1043,3 +1015,38 @@ jobs:
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,11 +164,6 @@ export async function complete(
|
||||
values = filterActionRunsCompletions(values, path, parsedTemplate.value);
|
||||
}
|
||||
|
||||
// Filter `timezone` from schedule completions when the feature flag is disabled
|
||||
if (!config?.featureFlags?.isEnabled("allowCronTimezone") && parent?.definition?.key === "schedule") {
|
||||
values = values.filter(v => v.label !== "timezone");
|
||||
}
|
||||
|
||||
// Filter `copilot-requests` from permissions completions when the feature flag is disabled
|
||||
if (
|
||||
!config?.featureFlags?.isEnabled("allowCopilotRequestsPermission") &&
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
"languageservice",
|
||||
"languageserver"
|
||||
],
|
||||
"version": "0.3.48"
|
||||
"version": "0.3.50"
|
||||
}
|
||||
Generated
+9
-9
@@ -136,7 +136,7 @@
|
||||
},
|
||||
"expressions": {
|
||||
"name": "@actions/expressions",
|
||||
"version": "0.3.48",
|
||||
"version": "0.3.50",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^29.0.3",
|
||||
@@ -396,11 +396,11 @@
|
||||
},
|
||||
"languageserver": {
|
||||
"name": "@actions/languageserver",
|
||||
"version": "0.3.48",
|
||||
"version": "0.3.50",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/languageservice": "^0.3.48",
|
||||
"@actions/workflow-parser": "^0.3.48",
|
||||
"@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.48",
|
||||
"version": "0.3.50",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/expressions": "^0.3.48",
|
||||
"@actions/workflow-parser": "^0.3.48",
|
||||
"@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.48",
|
||||
"version": "0.3.50",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/expressions": "^0.3.48",
|
||||
"@actions/expressions": "^0.3.50",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@actions/workflow-parser",
|
||||
"version": "0.3.48",
|
||||
"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.48",
|
||||
"@actions/expressions": "^0.3.50",
|
||||
"cronstrue": "^2.21.0",
|
||||
"yaml": "^2.0.0-8"
|
||||
},
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
||||
import {FeatureFlags} from "@actions/expressions/features";
|
||||
import {nullTrace} from "../test-utils/null-trace.js";
|
||||
import {parseWorkflow} from "../workflows/workflow-parser.js";
|
||||
import {convertWorkflowTemplate, ErrorPolicy} from "./convert.js";
|
||||
@@ -580,8 +579,8 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
describe("schedule timezone with feature flags", () => {
|
||||
it("allows timezone when allowCronTimezone is enabled", async () => {
|
||||
describe("schedule timezone", () => {
|
||||
it("allows timezone in schedule", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -597,8 +596,7 @@ jobs:
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion,
|
||||
featureFlags: new FeatureFlags({allowCronTimezone: true})
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
@@ -609,57 +607,6 @@ jobs:
|
||||
});
|
||||
});
|
||||
|
||||
it("reports error when timezone is present but allowCronTimezone is disabled", 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,
|
||||
featureFlags: new FeatureFlags({allowCronTimezone: false})
|
||||
});
|
||||
|
||||
// When timezone feature is disabled, error points at the timezone key
|
||||
expect(result.context.errors.getErrors()).toHaveLength(1);
|
||||
expect(result.context.errors.getErrors()[0].message).toContain("Key 'timezone' is not supported");
|
||||
// Schedule entry is dropped due to unsupported key
|
||||
expect(template.events?.schedule).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("reports error when timezone is present with no feature flags provided", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
content: `on:
|
||||
schedule:
|
||||
- cron: '0 0 * * *'
|
||||
timezone: America/New_York
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest`
|
||||
},
|
||||
nullTrace
|
||||
);
|
||||
|
||||
await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
// Default is timezone disabled, so error points at the timezone key
|
||||
expect(result.context.errors.getErrors()).toHaveLength(1);
|
||||
expect(result.context.errors.getErrors()[0].message).toContain("Key 'timezone' is not supported");
|
||||
});
|
||||
|
||||
it("reports error when cron is missing from schedule entry", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
@@ -675,8 +622,7 @@ jobs:
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion,
|
||||
featureFlags: new FeatureFlags({allowCronTimezone: true})
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
// Both schema validation and converter report the missing cron
|
||||
@@ -689,7 +635,7 @@ jobs:
|
||||
expect(template.events?.schedule).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("converts schedule without timezone when allowCronTimezone is enabled", async () => {
|
||||
it("converts schedule without timezone", async () => {
|
||||
const result = parseWorkflow(
|
||||
{
|
||||
name: "wf.yaml",
|
||||
@@ -704,8 +650,7 @@ jobs:
|
||||
);
|
||||
|
||||
const template = await convertWorkflowTemplate(result.context, result.value!, undefined, {
|
||||
errorPolicy: ErrorPolicy.TryConversion,
|
||||
featureFlags: new FeatureFlags({allowCronTimezone: true})
|
||||
errorPolicy: ErrorPolicy.TryConversion
|
||||
});
|
||||
|
||||
expect(result.context.errors.getErrors()).toHaveLength(0);
|
||||
|
||||
@@ -40,7 +40,7 @@ export type WorkflowTemplateConverterOptions = {
|
||||
errorPolicy?: ErrorPolicy;
|
||||
|
||||
/**
|
||||
* Optional feature flags to control which experimental features are enabled.
|
||||
* Feature flags for experimental features.
|
||||
*/
|
||||
featureFlags?: FeatureFlags;
|
||||
};
|
||||
@@ -61,10 +61,7 @@ export async function convertWorkflowTemplate(
|
||||
const result = {} as WorkflowTemplate;
|
||||
const opts = getOptionsWithDefaults(options);
|
||||
|
||||
// Store feature flags in context state so converters can access them
|
||||
if (opts.featureFlags) {
|
||||
context.state["featureFlags"] = opts.featureFlags;
|
||||
}
|
||||
context.state.featureFlags = opts.featureFlags;
|
||||
|
||||
if (context.errors.getErrors().length > 0 && opts.errorPolicy === ErrorPolicy.ReturnErrorsOnly) {
|
||||
result.errors = context.errors.getErrors().map(x => ({
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {FeatureFlags} from "@actions/expressions/features";
|
||||
import {TemplateContext} from "../../templates/template-context.js";
|
||||
import {MappingToken} from "../../templates/tokens/mapping-token.js";
|
||||
import {SequenceToken} from "../../templates/tokens/sequence-token.js";
|
||||
@@ -56,8 +55,7 @@ export function convertOn(context: TemplateContext, token: TemplateToken): Event
|
||||
// Schedule is the only event that can be a sequence, handle that separately
|
||||
if (eventName === "schedule") {
|
||||
const scheduleToken = item.value.assertSequence(`event ${eventName}`);
|
||||
const featureFlags = context.state["featureFlags"] as FeatureFlags | undefined;
|
||||
result.schedule = convertSchedule(context, scheduleToken, featureFlags);
|
||||
result.schedule = convertSchedule(context, scheduleToken);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -149,13 +147,7 @@ function convertFilter<T extends TypesFilterConfig & WorkflowFilterConfig & Vers
|
||||
return result;
|
||||
}
|
||||
|
||||
function convertSchedule(
|
||||
context: TemplateContext,
|
||||
token: SequenceToken,
|
||||
featureFlags?: FeatureFlags
|
||||
): ScheduleConfig[] | undefined {
|
||||
const flags = featureFlags ?? new FeatureFlags();
|
||||
const allowTimezone = flags.isEnabled("allowCronTimezone");
|
||||
function convertSchedule(context: TemplateContext, token: SequenceToken): ScheduleConfig[] | undefined {
|
||||
const result = [] as ScheduleConfig[];
|
||||
|
||||
for (const item of token) {
|
||||
@@ -173,13 +165,8 @@ function convertSchedule(
|
||||
}
|
||||
config.cron = cron.value;
|
||||
} else if (key.value === "timezone") {
|
||||
if (allowTimezone) {
|
||||
const timezone = entry.value.assertString(`schedule timezone`);
|
||||
config.timezone = timezone.value;
|
||||
} else {
|
||||
context.error(key, `Key 'timezone' is not supported`);
|
||||
valid = false;
|
||||
}
|
||||
const timezone = entry.value.assertString(`schedule timezone`);
|
||||
config.timezone = timezone.value;
|
||||
} else {
|
||||
context.error(key, `Invalid schedule key`);
|
||||
valid = false;
|
||||
|
||||
@@ -75,6 +75,8 @@ export type Container = {
|
||||
ports?: SequenceToken;
|
||||
volumes?: SequenceToken;
|
||||
options?: StringToken;
|
||||
entrypoint?: StringToken;
|
||||
command?: StringToken;
|
||||
};
|
||||
|
||||
export type Credential = {
|
||||
|
||||
@@ -2399,7 +2399,7 @@
|
||||
],
|
||||
"one-of": [
|
||||
"non-empty-string",
|
||||
"container-mapping"
|
||||
"service-container-mapping"
|
||||
]
|
||||
},
|
||||
"container-registry-credentials": {
|
||||
@@ -2647,6 +2647,38 @@
|
||||
"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."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user