Compare commits

..
Author SHA1 Message Date
Salman Muin Kayser Chishti 7520561423 Add workflows permission to schema
Add 'workflows' as a valid permission scope in the permissions-mapping
schema. The workflows permission supports only 'write' (or none),
matching the GitHub App permissions model where workflows: write
allows updating GitHub Actions workflow files.

This enables autocomplete and validation for 'permissions: workflows:'
in workflow YAML files.
2026-04-10 23:26:45 +01:00
eric sciple cc316ab9de Remove phantom github.job_workflow_sha from language service (#347)
This property is listed in the GitHub context provider but is never
populated at runtime by the runner. Users see it in autocomplete,
use it in workflows, and it silently evaluates to empty string.

Remove from keys array and description metadata.
2026-04-03 18:33:15 -05:00
github-actions[bot]andGitHub Actions d5670c383a Release extension version 0.3.51 (#346)
Co-authored-by: GitHub Actions <[email protected]>
2026-04-03 10:34:58 -05:00
eric sciple f62a0e189d Remove allowServiceContainerCommand feature flag (#345)
Service container entrypoint/command support is now unconditional.
2026-04-03 10:29:34 -05:00
github-actions[bot]andGitHub Actions 9e1662f1d4 Release extension version 0.3.50 (#344)
Co-authored-by: GitHub Actions <[email protected]>
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]andGitHub Actions 83de320ba9 Release extension version 0.3.49 (#342)
Co-authored-by: GitHub Actions <[email protected]>
2026-03-20 09:47:56 -05:00
Angel KouandAngel Kou 74e6638098 Remove timezone feature flag in languageservice (#341)
* Remove timezone feature flag in languageservice

* Prettier

* Address comment

---------

Co-authored-by: Angel Kou <[email protected]>
2026-03-19 14:10:38 -07:00
20 changed files with 359 additions and 157 deletions
+33
View File
@@ -0,0 +1,33 @@
# Agents
## Build
```
npx lerna run build
```
## Test
```
npm -w @actions/expressions test
npm -w @actions/workflow-parser test
npm -w @actions/languageservice test
```
## Format
Always run formatting before committing:
```
npx prettier --write <changed files>
```
Verify with:
```
npm run format-check -ws
```
## Feature flags
Feature flags are defined in `expressions/src/features.ts` (`ExperimentalFeatures` interface + `allFeatureKeys` array). They are plumbed through `ConvertOptions`, `CompletionConfig`, `ValidationConfig`, and `initializationOptions`. When a feature graduates to stable, remove its flag and make the behavior unconditional.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/expressions",
"version": "0.3.48",
"version": "0.3.51",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
-1
View File
@@ -55,7 +55,6 @@ describe("FeatureFlags", () => {
"missingInputsQuickfix",
"blockScalarChompingWarning",
"allowCaseFunction",
"allowCronTimezone",
"allowCopilotRequestsPermission"
]);
});
-7
View File
@@ -35,12 +35,6 @@ 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
@@ -61,7 +55,6 @@ const allFeatureKeys: ExperimentalFeatureKey[] = [
"missingInputsQuickfix",
"blockScalarChompingWarning",
"allowCaseFunction",
"allowCronTimezone",
"allowCopilotRequestsPermission"
];
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/languageserver",
"version": "0.3.48",
"version": "0.3.51",
"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.51",
"@actions/workflow-parser": "^0.3.51",
"@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.48",
"version": "0.3.51",
"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.51",
"@actions/workflow-parser": "^0.3.51",
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.8",
+76 -30
View File
@@ -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,77 @@ jobs:
expect(labels).not.toContain("copilot-requests");
});
});
describe("permissions workflows completion", () => {
it("includes workflows in top-level permissions", 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("workflows");
});
it("offers only write and none for workflows", async () => {
const input = `on: push
permissions:
workflows: |`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("write");
expect(labels).not.toContain("read");
});
it("includes workflows in job-level permissions", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
permissions:
|`;
const result = await complete(...getPositionFromCursor(input));
expect(result).not.toBeUndefined();
const labels = result.map(x => x.label);
expect(labels).toContain("workflows");
});
});
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");
});
});
+2 -6
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,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") &&
@@ -105,13 +105,6 @@
"job": {
"description": "The [`job_id`](https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_id) of the current job.\nNote: This context property is set by the Actions runner, and is only available within the execution `steps` of a job. Otherwise, the value of this property will be `null`."
},
"job_workflow_sha": {
"description": "For jobs using a reusable workflow, the commit SHA for the reusable workflow file.",
"versions": {
"ghes": ">=3.9",
"ghae": ">=3.9"
}
},
"path": {
"description": "Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see \"[Workflow commands for GitHub Actions](https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path).\""
},
@@ -29,7 +29,6 @@ export function getGithubContext(workflowContext: WorkflowContext | undefined, m
"graphql_url",
"head_ref",
"job",
"job_workflow_sha",
"path",
"ref",
"ref_name",
@@ -0,0 +1,102 @@
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} from "./validate.js";
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("service container command/entrypoint", () => {
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));
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));
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));
const relevantErrors = result.filter(d => d.message.includes("command") || d.message.includes("entrypoint"));
expect(relevantErrors).toEqual([]);
});
it("rejects command in job container", 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));
const commandErrors = result.filter(d => d.message.includes("command"));
expect(commandErrors.length).toBeGreaterThan(0);
});
it("rejects entrypoint in job container", 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));
const entrypointErrors = result.filter(d => d.message.includes("entrypoint"));
expect(entrypointErrors.length).toBeGreaterThan(0);
});
});
+1 -1
View File
@@ -6,5 +6,5 @@
"languageservice",
"languageserver"
],
"version": "0.3.48"
"version": "0.3.51"
}
+9 -9
View File
@@ -136,7 +136,7 @@
},
"expressions": {
"name": "@actions/expressions",
"version": "0.3.48",
"version": "0.3.51",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.0.3",
@@ -396,11 +396,11 @@
},
"languageserver": {
"name": "@actions/languageserver",
"version": "0.3.48",
"version": "0.3.51",
"license": "MIT",
"dependencies": {
"@actions/languageservice": "^0.3.48",
"@actions/workflow-parser": "^0.3.48",
"@actions/languageservice": "^0.3.51",
"@actions/workflow-parser": "^0.3.51",
"@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.51",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.48",
"@actions/workflow-parser": "^0.3.48",
"@actions/expressions": "^0.3.51",
"@actions/workflow-parser": "^0.3.51",
"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.51",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.48",
"@actions/expressions": "^0.3.51",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/workflow-parser",
"version": "0.3.48",
"version": "0.3.51",
"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.51",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
+6 -61
View File
@@ -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);
+2 -5
View File
@@ -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,87 @@ 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 mapping = services.assertMapping("services");
for (const service of mapping) {
service.key.assertString("service key");
const container = convertToJobContainer(context, service.value);
const container = convertToServiceContainer(context, service.value);
if (container) {
serviceList.push(container);
}
+4 -17
View File
@@ -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 = {
+38 -2
View File
@@ -1649,6 +1649,10 @@
"statuses": {
"type": "permission-level-any",
"description": "Commit statuses."
},
"workflows": {
"type": "permission-level-write-or-no-access",
"description": "Update GitHub Actions workflow files."
}
}
}
@@ -2399,7 +2403,7 @@
],
"one-of": [
"non-empty-string",
"container-mapping"
"service-container-mapping"
]
},
"container-registry-credentials": {
@@ -2647,6 +2651,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."
}
}
}
}
}
}
}