Compare commits

..

1 Commits

Author SHA1 Message Date
GitHub Actions fdad3474fc Release extension version 0.4.0 2026-04-14 14:24:54 +00:00
15 changed files with 25 additions and 335 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/expressions",
"version": "0.3.54",
"version": "0.4.0",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
+1 -3
View File
@@ -25,7 +25,6 @@ describe("FeatureFlags", () => {
it("returns true when all is enabled", () => {
const flags = new FeatureFlags({all: true});
expect(flags.isEnabled("missingInputsQuickfix")).toBe(true);
expect(flags.isEnabled("allowConcurrencyQueue")).toBe(true);
});
it("explicit feature flag takes precedence over all:true", () => {
@@ -56,8 +55,7 @@ describe("FeatureFlags", () => {
"missingInputsQuickfix",
"blockScalarChompingWarning",
"allowCaseFunction",
"allowCopilotRequestsPermission",
"allowConcurrencyQueue"
"allowCopilotRequestsPermission"
]);
});
});
+1 -8
View File
@@ -40,12 +40,6 @@ export interface ExperimentalFeatures {
* @default false
*/
allowCopilotRequestsPermission?: boolean;
/**
* Enable the queue property in workflow concurrency settings.
* @default false
*/
allowConcurrencyQueue?: boolean;
}
/**
@@ -61,8 +55,7 @@ const allFeatureKeys: ExperimentalFeatureKey[] = [
"missingInputsQuickfix",
"blockScalarChompingWarning",
"allowCaseFunction",
"allowCopilotRequestsPermission",
"allowConcurrencyQueue"
"allowCopilotRequestsPermission"
];
export class FeatureFlags {
-1
View File
@@ -127,7 +127,6 @@ initializationOptions: {
|---------|-------------|
| `missingInputsQuickfix` | Code action to add missing required inputs for actions |
| `blockScalarChompingWarning` | Warn when block scalars (`\|` or `>`) use implicit clip chomping, which adds a trailing newline that may be unintentional |
| `allowConcurrencyQueue` | Enable the `concurrency.queue` workflow property |
Individual feature flags take precedence over `all`. For example, `{ all: true, missingInputsQuickfix: false }` enables all experimental features except `missingInputsQuickfix`.
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/languageserver",
"version": "0.3.54",
"version": "0.4.0",
"description": "Language server for GitHub Actions",
"license": "MIT",
"type": "module",
@@ -48,8 +48,8 @@
"actions-languageserver": "./bin/actions-languageserver"
},
"dependencies": {
"@actions/languageservice": "^0.3.54",
"@actions/workflow-parser": "^0.3.54",
"@actions/languageservice": "^0.4.0",
"@actions/workflow-parser": "^0.4.0",
"@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.54",
"version": "0.4.0",
"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.54",
"@actions/workflow-parser": "^0.3.54",
"@actions/expressions": "^0.4.0",
"@actions/workflow-parser": "^0.4.0",
"vscode-languageserver-textdocument": "^1.0.7",
"vscode-languageserver-types": "^3.17.2",
"vscode-uri": "^3.0.8",
@@ -1,4 +1,3 @@
import {FeatureFlags} from "@actions/expressions/features";
import {DiagnosticSeverity} from "vscode-languageserver-types";
import {validate} from "./validate.js";
import {createDocument} from "./test-utils/document.js";
@@ -8,10 +7,6 @@ beforeEach(() => {
clearCache();
});
const queueValidationConfig = {
featureFlags: new FeatureFlags({allowConcurrencyQueue: true})
};
describe("validate concurrency deadlock", () => {
describe("should error on matching concurrency groups", () => {
it("simple string match", async () => {
@@ -248,186 +243,3 @@ jobs:
});
});
});
describe("validate concurrency queue + cancel-in-progress conflict", () => {
describe("should error", () => {
it("workflow-level queue: max with cancel-in-progress: true", async () => {
const input = `
on: push
concurrency:
group: deploy
cancel-in-progress: true
queue: max
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input), queueValidationConfig);
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(1);
expect(queueErrors[0]).toMatchObject({
message: "'queue: max' cannot be combined with 'cancel-in-progress: true'.",
severity: DiagnosticSeverity.Error
});
});
it("job-level queue: max with cancel-in-progress: true", async () => {
const input = `
on: push
jobs:
job1:
runs-on: ubuntu-latest
concurrency:
group: deploy
cancel-in-progress: true
queue: max
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input), queueValidationConfig);
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(1);
expect(queueErrors[0]).toMatchObject({
severity: DiagnosticSeverity.Error
});
});
it("both workflow and job level have the conflict", async () => {
const input = `
on: push
concurrency:
group: deploy
cancel-in-progress: true
queue: max
jobs:
job1:
runs-on: ubuntu-latest
concurrency:
group: build
cancel-in-progress: true
queue: max
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input), queueValidationConfig);
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(2);
});
});
describe("should not error", () => {
it("queue: max without cancel-in-progress", async () => {
const input = `
on: push
concurrency:
group: deploy
queue: max
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input));
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(0);
});
it("queue: single with cancel-in-progress: true", async () => {
const input = `
on: push
concurrency:
group: deploy
cancel-in-progress: true
queue: single
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input));
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(0);
});
it("cancel-in-progress: false with queue: max", async () => {
const input = `
on: push
concurrency:
group: deploy
cancel-in-progress: false
queue: max
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input));
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(0);
});
it("no queue property", async () => {
const input = `
on: push
concurrency:
group: deploy
cancel-in-progress: true
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input));
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(0);
});
it("string form concurrency (no mapping)", async () => {
const input = `
on: push
concurrency: deploy
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input));
const queueErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueErrors).toHaveLength(0);
});
it("does not report queue conflict when the feature is disabled", async () => {
const input = `
on: push
concurrency:
group: deploy
cancel-in-progress: true
queue: max
jobs:
job1:
runs-on: ubuntu-latest
steps:
- run: echo hi`;
const result = await validate(createDocument("wf.yaml", input));
const queueConflictErrors = result.filter(d => d.message.includes("queue: max"));
expect(queueConflictErrors).toHaveLength(0);
});
});
});
+1 -62
View File
@@ -1,13 +1,6 @@
import {FeatureFlags, Lexer, Parser} from "@actions/expressions";
import {Expr} from "@actions/expressions/ast";
import {
TemplateParseResult,
WorkflowTemplate,
isBasicExpression,
isBoolean,
isMapping,
isString
} from "@actions/workflow-parser";
import {TemplateParseResult, WorkflowTemplate, isBasicExpression, isMapping, isString} from "@actions/workflow-parser";
import {ErrorPolicy} from "@actions/workflow-parser/model/convert";
import {getCronDescription, hasCronIntervalLessThan5Minutes} from "@actions/workflow-parser/model/converter/cron";
import {ensureStatusFunction} from "@actions/workflow-parser/model/converter/if-condition";
@@ -246,11 +239,6 @@ async function additionalValidations(
// Validate concurrency deadlock between workflow and job levels
validateConcurrencyDeadlock(diagnostics, template);
// Validate incompatible concurrency options
if (featureFlags?.isEnabled("allowConcurrencyQueue")) {
validateConcurrencyQueueCancelInProgress(diagnostics, template);
}
}
function invalidValue(diagnostics: Diagnostic[], token: StringToken, kind: ValueProviderKind) {
@@ -676,55 +664,6 @@ function validateConcurrencyDeadlock(diagnostics: Diagnostic[], template: Workfl
}
}
/**
* Validates that `queue: max` and `cancel-in-progress: true` are not both set
* in a concurrency mapping, as this combination is invalid.
*/
function validateConcurrencyQueueCancelInProgress(diagnostics: Diagnostic[], template: WorkflowTemplate): void {
// Check workflow-level concurrency
if (template.concurrency) {
checkConcurrencyQueueConflict(diagnostics, template.concurrency);
}
// Check job-level concurrency
for (const job of template.jobs || []) {
if (job.concurrency) {
checkConcurrencyQueueConflict(diagnostics, job.concurrency);
}
}
}
function checkConcurrencyQueueConflict(diagnostics: Diagnostic[], token: TemplateToken): void {
if (!isMapping(token)) {
return;
}
let hasQueueMax = false;
let hasCancelInProgressTrue = false;
let queueRange: TokenRange | undefined;
for (const pair of token) {
if (!isString(pair.key) || pair.key.isExpression || pair.value.isExpression) {
continue;
}
if (pair.key.value === "queue" && isString(pair.value) && pair.value.value === "max") {
hasQueueMax = true;
queueRange = pair.key.range;
}
if (pair.key.value === "cancel-in-progress" && isBoolean(pair.value) && pair.value.value) {
hasCancelInProgressTrue = true;
}
}
if (hasQueueMax && hasCancelInProgressTrue && queueRange) {
diagnostics.push({
message: "'queue: max' cannot be combined with 'cancel-in-progress: true'.",
range: mapRange(queueRange),
severity: DiagnosticSeverity.Error
});
}
}
/**
* Extracts the static concurrency group name from a concurrency token.
* Returns undefined if the token is an expression or doesn't have a static group.
+1 -1
View File
@@ -6,5 +6,5 @@
"languageservice",
"languageserver"
],
"version": "0.3.54"
"version": "0.4.0"
}
+9 -9
View File
@@ -136,7 +136,7 @@
},
"expressions": {
"name": "@actions/expressions",
"version": "0.3.54",
"version": "0.4.0",
"license": "MIT",
"devDependencies": {
"@types/jest": "^29.0.3",
@@ -396,11 +396,11 @@
},
"languageserver": {
"name": "@actions/languageserver",
"version": "0.3.54",
"version": "0.4.0",
"license": "MIT",
"dependencies": {
"@actions/languageservice": "^0.3.54",
"@actions/workflow-parser": "^0.3.54",
"@actions/languageservice": "^0.4.0",
"@actions/workflow-parser": "^0.4.0",
"@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.54",
"version": "0.4.0",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.54",
"@actions/workflow-parser": "^0.3.54",
"@actions/expressions": "^0.4.0",
"@actions/workflow-parser": "^0.4.0",
"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.54",
"version": "0.4.0",
"license": "MIT",
"dependencies": {
"@actions/expressions": "^0.3.54",
"@actions/expressions": "^0.4.0",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@actions/workflow-parser",
"version": "0.3.54",
"version": "0.4.0",
"license": "MIT",
"type": "module",
"source": "./src/index.ts",
@@ -48,7 +48,7 @@
"watch": "tsc --build tsconfig.build.json --watch"
},
"dependencies": {
"@actions/expressions": "^0.3.54",
"@actions/expressions": "^0.4.0",
"cronstrue": "^2.21.0",
"yaml": "^2.0.0-8"
},
@@ -1,12 +1,10 @@
import type {FeatureFlags} from "@actions/expressions/features";
import {TemplateContext} from "../../templates/template-context.js";
import {TemplateToken} from "../../templates/tokens/template-token.js";
import {isString} from "../../templates/tokens/type-guards.js";
import {ConcurrencyQueue, ConcurrencySetting} from "../workflow-template.js";
import {ConcurrencySetting} from "../workflow-template.js";
export function convertConcurrency(context: TemplateContext, token: TemplateToken): ConcurrencySetting {
const result: ConcurrencySetting = {};
const featureFlags = context.state.featureFlags as FeatureFlags | undefined;
if (token.isExpression) {
return result;
@@ -28,11 +26,6 @@ export function convertConcurrency(context: TemplateContext, token: TemplateToke
case "cancel-in-progress":
result.cancelInProgress = property.value.assertBoolean("cancel-in-progress").value;
break;
case "queue":
if (featureFlags?.isEnabled("allowConcurrencyQueue")) {
result.queue = property.value.assertString("queue").value as ConcurrencyQueue;
}
break;
default:
context.error(propertyName, `Invalid property name: ${propertyName.value}`);
}
@@ -18,12 +18,9 @@ export type WorkflowTemplate = {
}[];
};
export type ConcurrencyQueue = "single" | "max";
export type ConcurrencySetting = {
group?: StringToken;
cancelInProgress?: boolean;
queue?: ConcurrencyQueue;
};
export type ActionsEnvironmentReference = {
+1 -15
View File
@@ -1644,15 +1644,11 @@
},
"security-events": {
"type": "permission-level-any",
"description": "Code scanning alerts."
"description": "Code scanning and Dependabot alerts."
},
"statuses": {
"type": "permission-level-any",
"description": "Commit statuses."
},
"vulnerability-alerts": {
"type": "permission-level-read-or-no-access",
"description": "Dependabot alerts."
}
}
}
@@ -2054,20 +2050,10 @@
"cancel-in-progress": {
"type": "boolean",
"description": "To cancel any currently running job or workflow in the same concurrency group, specify cancel-in-progress: true."
},
"queue": {
"type": "concurrency-queue",
"description": "The queuing mode for the concurrency group. When set to `max`, workflows or jobs will wait in a queue for the concurrency group up to the maximum queue length. Default: `single` meaning at most one item can be pending."
}
}
}
},
"concurrency-queue": {
"allowed-values": [
"single",
"max"
]
},
"job-environment": {
"description": "The environment that the job references. All environment protection rules must pass before a job referencing the environment is sent to a runner.",
"context": [
+1 -28
View File
@@ -25,11 +25,7 @@ jobs:
concurrency:
group: ref
cancel-in-progress: ${{ github.ref }}
build5:
runs-on: ubuntu-latest
concurrency:
group: deploy
queue: max
---
{
@@ -145,29 +141,6 @@ jobs:
]
},
"runs-on": "macos-latest"
},
{
"type": "job",
"id": "build5",
"name": "build5",
"if": {
"type": 3,
"expr": "success()"
},
"concurrency": {
"type": 2,
"map": [
{
"Key": "group",
"Value": "deploy"
},
{
"Key": "queue",
"Value": "max"
}
]
},
"runs-on": "ubuntu-latest"
}
]
}