fix(ci): enforce architecture guards before dependency setup (#8027)

* fix(ci): enforce architecture guards before dependency setup

* test(ci): bind cache policy to preceding Node setup
This commit is contained in:
gensokyo-dev authored and GitHub committed 2026-10-29 21:29:15 +08:00
1 parent a108a1b817
commit cac6e0613e
4 files changed
+47 -10

No files matched your search

+3
View File
@@ -144,6 +144,9 @@ jobs:
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Validate source architecture before dependency installation
if: needs.changes.outputs.code == 'true'
run: node scripts/verify/architecture-guards.mjs
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
@@ -5,6 +5,7 @@ import {
} from "../../domain/javascript/javascriptSourceCoordinates.js";
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
import { compositeKey } from "../../domain/unicodeCodePointOrder.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import {
addSemanticGraphRelation,
@@ -71,10 +72,11 @@ const operatorLiteralIndex = (
return node;
seen.add(node.node_id);
const entry = { node, range, order: order++ };
const id = `${literalValueKey(value)}\0${javascriptSourceRangeKey(range)}`;
const group = byValue.get(literalValueKey(value)) ?? [];
const valueKey = literalValueKey(value);
const id = compositeKey([valueKey, javascriptSourceRangeKey(range)]);
const group = byValue.get(valueKey) ?? [];
group.push(entry);
byValue.set(literalValueKey(value), group);
byValue.set(valueKey, group);
if (!exact.has(id)) exact.set(id, entry);
return node;
};
@@ -92,7 +94,7 @@ const operatorLiteralIndex = (
)
best = entry;
};
consider(exact.get(`${valueKey}\0${operandKey}`));
consider(exact.get(compositeKey([valueKey, operandKey])));
const group = byValue.get(valueKey) ?? [];
let low = 0;
let high = group.length;
@@ -361,7 +361,7 @@ printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache'
);
});
it("replaces every inline setup-node npm cache immediately and preserves conditions", async () => {
it("pairs npm caches with preceding Node setup and preserves conditions", async () => {
const workflows = await readWorkflows();
let npmCacheSteps = 0;
@@ -377,12 +377,17 @@ printf '%s\n' 'C:\Users\runner with spaces\AppData\Local\npm-cache'
const step = steps[index];
if (step?.uses !== "./.github/actions/npm-cache") continue;
npmCacheSteps++;
const setup = steps[index - 1];
const setup = steps
.slice(0, index)
.findLast(
(candidate) =>
typeof candidate.uses === "string" &&
candidate.uses.startsWith("actions/setup-node@"),
);
expect(
typeof setup?.uses === "string" &&
setup.uses.startsWith("actions/setup-node@"),
`${file}:${jobName} npm cache action must immediately follow setup-node`,
).toBe(true);
setup,
`${file}:${jobName} npm cache action requires preceding setup-node`,
).toBeDefined();
expect(step.if).toBe(setup?.if);
expect(setup?.with).not.toHaveProperty("cache", "npm");
const npmCiIndex = steps.findIndex(
@@ -180,6 +180,33 @@ it("makes the required gate wait for every CI lane, including future additions",
);
});
it("runs architecture guards for fast code feedback before installing dependencies", async () => {
const steps = z
.array(
z
.object({
run: z.string().optional(),
uses: z.string().optional(),
if: z.string().optional(),
})
.passthrough(),
)
.parse((await workflow()).jobs.static?.steps);
const guard = steps.findIndex(
(step) => step.run === "node scripts/verify/architecture-guards.mjs",
);
const setup = steps.findIndex((step) =>
step.uses?.startsWith("actions/setup-node@"),
);
const install = steps.findIndex((step) => step.run === "npm ci");
expect(guard).toBeGreaterThanOrEqual(0);
expect(setup).toBeGreaterThanOrEqual(0);
expect(install).toBeGreaterThan(guard);
expect(guard).toBeGreaterThan(setup);
expect(steps[guard]?.if).toBe("needs.changes.outputs.code == 'true'");
expect(steps[guard]?.["continue-on-error"]).toBeUndefined();
});
it("holds native and provider fan-out behind static validation while portable feedback stays independent", async () => {
const { jobs } = await workflow();
const earlyJobs = new Set([