ci(workflows): isolated manual workflow_dispatch runs in release concurrency groups

- Updated the CI workflow concurrency rules to treat `workflow_dispatch` like a release path, grouping those runs by SHA and disabling cancel-in-progress.
- Extended the `GhaEval` expression evaluator in `scripts/ci-concurrency.test.ts` to support `==`/`!=` and align falsy checks.
- Added a regression test covering tagged-main `workflow_dispatch` runs using the release-style concurrency behavior.
This commit is contained in:
can1357
2026-06-14 19:04:05 +02:00
parent 31138bf7ec
commit 73f2dbc3b5
2 changed files with 41 additions and 14 deletions
+35 -8
View File
@@ -26,8 +26,8 @@ interface GhaCtx {
}
// Single-purpose, hand-rolled evaluator for the operators / functions the
// workflow's `concurrency` block uses: `startsWith`, `format`, `!`, `&&`, `||`,
// parens, single-quoted strings, dotted property access. Matches GitHub's
// workflow's `concurrency` block uses: `startsWith`, `format`, `!`, `==`,
// `&&`, `||`, parens, single-quoted strings, dotted property access. Matches
// short-circuit semantics: `&&`/`||` return the underlying value (not a coerced
// bool), missing identifiers resolve to `null`, and `startsWith(null, …)` is
// false because the searchString coerces to `""`.
@@ -73,30 +73,47 @@ class GhaEval {
let left = this.#and();
while (this.#consume("||")) {
const right = this.#and();
// Truthy left wins; only null/false/""/0 fall through.
if (left !== null && left !== false && left !== "" && left !== 0) continue;
// Truthy left wins; only null/false/"" fall through.
if (left !== null && left !== false && left !== "") continue;
left = right;
}
return left;
}
#and(): Value {
let left = this.#unary();
let left = this.#eq();
while (this.#consume("&&")) {
const right = this.#unary();
const right = this.#eq();
// Falsy left short-circuits and is returned verbatim.
if (left === null || left === false || left === "" || left === 0) continue;
if (left === null || left === false || left === "") continue;
left = right;
}
return left;
}
#eq(): Value {
let left = this.#unary();
while (true) {
if (this.#consume("==")) {
const right = this.#unary();
left = left === right;
continue;
}
if (this.#consume("!=")) {
const right = this.#unary();
left = left !== right;
continue;
}
return left;
}
}
#unary(): Value {
this.#skipWs();
if (this.src[this.#pos] === "!") {
this.#pos++;
const v = this.#unary();
return v === null || v === false || v === "" || v === 0;
return v === null || v === false || v === "";
}
return this.#primary();
}
@@ -270,6 +287,16 @@ describe("ci.yml concurrency", () => {
expect(GhaEval.template(cancelTemplate, ctx)).toBe("false");
});
it("workflow_dispatch from tagged main HEAD is isolated before release_metadata can inspect tags", () => {
const ctx = baseCtx({
event_name: "workflow_dispatch",
sha: "taggedmain123",
event: {},
});
expect(GhaEval.template(groupTemplate, ctx)).toBe("CI-release-taggedmain123");
expect(GhaEval.template(cancelTemplate, ctx)).toBe("false");
});
it("regular main push: branch-wide group, cancel-in-progress enabled", () => {
const ctx = baseCtx({ event: { head_commit: { message: "fix(ux): theme tweak" } } });
expect(GhaEval.template(groupTemplate, ctx)).toBe("CI-refs/heads/main");