Skip to content

Commit 550eee2

Browse files
committed
fix CI validation dependency ordering
1 parent 86c4aad commit 550eee2

3 files changed

Lines changed: 90 additions & 7 deletions

File tree

.github/workflows/ci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ jobs:
3030
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
3131
with:
3232
node-version: 24.x
33+
cache: npm
34+
- name: Install locked dependencies
35+
run: npm ci
3336
- name: Validate Public Preview content and identity
3437
run: npm run check:public-preview
3538
- name: Download the pinned actionlint release

.github/workflows/publish.yml

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ jobs:
8080
EOF
8181
8282
echo "release-commit=${release_commit}" >> "$GITHUB_OUTPUT"
83+
- name: Set up Node.js 24
84+
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
85+
with:
86+
node-version: 24.x
87+
cache: npm
88+
- name: Install validation dependencies without lifecycle scripts
89+
run: npm ci --ignore-scripts
8390
- name: Verify release metadata and derive the npm dist-tag
8491
id: version
8592
env:
@@ -93,11 +100,6 @@ jobs:
93100
--release-prerelease "$RELEASE_IS_PRERELEASE" \
94101
--require-final \
95102
--require-releasable-docs >> "$GITHUB_OUTPUT"
96-
- name: Set up Node.js 24
97-
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
98-
with:
99-
node-version: 24.x
100-
cache: npm
101103
- name: Use a Trusted Publishing-capable npm CLI
102104
run: npm install --global npm@11.12.1
103105
- name: Install locked dependencies

tests/ci-workflow.test.mjs

Lines changed: 80 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { URL } from "node:url";
44
import { describe, expect, it } from "vitest";
55
import { parseDocument } from "yaml";
66

7-
function readWorkflow() {
7+
function readWorkflow(name = "ci.yml") {
88
const source = readFileSync(
9-
new URL("../.github/workflows/ci.yml", import.meta.url),
9+
new URL(`../.github/workflows/${name}`, import.meta.url),
1010
"utf8",
1111
);
1212
const document = parseDocument(source, { uniqueKeys: true });
@@ -15,6 +15,84 @@ function readWorkflow() {
1515
}
1616

1717
describe("blocking CI workflow", () => {
18+
it("installs dependencies before dependency-backed workflow gates", () => {
19+
const workflowLint = readWorkflow().jobs?.["workflow-lint"];
20+
expect(workflowLint).toBeDefined();
21+
const previewInstall = workflowLint.steps.findIndex(
22+
(step) => step.run === "npm ci",
23+
);
24+
const previewGate = workflowLint.steps.findIndex(
25+
(step) => step.run === "npm run check:public-preview",
26+
);
27+
expect(workflowLint.steps[previewInstall]).toEqual({
28+
name: "Install locked dependencies",
29+
run: "npm ci",
30+
});
31+
expect(workflowLint.steps[previewGate]).toEqual({
32+
name: "Validate Public Preview content and identity",
33+
run: "npm run check:public-preview",
34+
});
35+
expect(previewGate).toBeGreaterThan(previewInstall);
36+
37+
const releaseVerify = readWorkflow("publish.yml").jobs?.verify;
38+
expect(releaseVerify).toBeDefined();
39+
const trustGate = releaseVerify.steps.findIndex(
40+
(step) => step.id === "trust",
41+
);
42+
const validationInstall = releaseVerify.steps.findIndex(
43+
(step) => step.run === "npm ci --ignore-scripts",
44+
);
45+
const releaseGate = releaseVerify.steps.findIndex(
46+
(step) =>
47+
typeof step.run === "string" &&
48+
step.run.includes("node scripts/validate-release.mjs"),
49+
);
50+
const fullInstall = releaseVerify.steps.findIndex(
51+
(step, index) => index > releaseGate && step.run === "npm ci",
52+
);
53+
expect(Object.keys(releaseVerify.steps[trustGate]).sort()).toEqual([
54+
"env",
55+
"id",
56+
"name",
57+
"run",
58+
"shell",
59+
]);
60+
expect(releaseVerify.steps[trustGate]).toMatchObject({
61+
id: "trust",
62+
name: "Reject an untrusted release target",
63+
shell: "bash",
64+
});
65+
expect(releaseVerify.steps[validationInstall]).toEqual({
66+
name: "Install validation dependencies without lifecycle scripts",
67+
run: "npm ci --ignore-scripts",
68+
});
69+
expect(validationInstall).toBeGreaterThan(trustGate);
70+
expect(releaseVerify.steps[releaseGate]).toEqual({
71+
env: {
72+
RELEASE_IS_PRERELEASE: "${{ github.event.release.prerelease }}",
73+
RELEASE_TAG: "${{ github.event.release.tag_name }}",
74+
},
75+
id: "version",
76+
name: "Verify release metadata and derive the npm dist-tag",
77+
run: [
78+
"set -euo pipefail",
79+
"node scripts/validate-release.mjs \\",
80+
' --tag "$RELEASE_TAG" \\',
81+
' --release-prerelease "$RELEASE_IS_PRERELEASE" \\',
82+
" --require-final \\",
83+
' --require-releasable-docs >> "$GITHUB_OUTPUT"',
84+
"",
85+
].join("\n"),
86+
shell: "bash",
87+
});
88+
expect(releaseGate).toBeGreaterThan(validationInstall);
89+
expect(releaseVerify.steps[fullInstall]).toEqual({
90+
name: "Install locked dependencies",
91+
run: "npm ci",
92+
});
93+
expect(fullInstall).toBeGreaterThan(releaseGate);
94+
});
95+
1896
it("runs the live-smoke contract in the locked Node.js 22 and 24 job", () => {
1997
const workflow = readWorkflow();
2098
const locked = workflow.jobs?.locked;

0 commit comments

Comments
 (0)