From 113a88db41e95757045eaa4c0e3b6d74dcd860f4 Mon Sep 17 00:00:00 2001 From: flint Date: Sun, 25 Oct 2026 15:19:10 +0000 Subject: [PATCH 1/3] fix(ci): match Renovate action runtime deps by name; read workflow versions in tests The github-actions uses-with rule matched package names, but setup-node's node dependency is named node and packaged as actions/node-versions, so the rule never fired. Match matchDepNames instead; the npm/bun rule keeps matchPackageNames because there the names are equal. Reviewer tests now derive skipped-action tags and runtime pins from the workflow, and the trusted runtime matrix gains Node 24.22.0 for the pin cli#613 carries. The allowlist guard remains: a workflow SHA that is not reviewed still fails. --- .../unreleased/fixed-renovate-action-bumps.md | 3 + .github/renovate.json | 3 +- docker/reviewer/runtime-matrix.json | 11 +++ packages/cli/test/reviewer/ci-job.test.ts | 85 ++++++++++++++----- .../cli/test/reviewer/resolve-runtime.test.ts | 29 ++++--- .../cli/test/reviewer/reviewer-launch.test.ts | 6 +- 6 files changed, 105 insertions(+), 32 deletions(-) create mode 100644 .changelog/unreleased/fixed-renovate-action-bumps.md diff --git a/.changelog/unreleased/fixed-renovate-action-bumps.md b/.changelog/unreleased/fixed-renovate-action-bumps.md new file mode 100644 index 00000000..563d8764 --- /dev/null +++ b/.changelog/unreleased/fixed-renovate-action-bumps.md @@ -0,0 +1,3 @@ +- **Renovate's setup-action runtime rule matches by dependency name, and reviewer tests read workflow versions.** The github-actions `uses-with` rule now matches `depName`, so setup-node's `node` dependency (package `actions/node-versions`) is skipped; the trusted runtime matrix gains Node 24.22.0. + + Reviewer tests derive skipped-action tags and runtime pins from the workflow, and fail on an unallowlisted SHA. diff --git a/.github/renovate.json b/.github/renovate.json index 9e675408..70940525 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -35,13 +35,14 @@ "enabled": false }, { + "description": "A setup action's uses-with dependency is named bun or node while its package is the action (e.g. actions/node-versions), so match it by dep name.", "matchManagers": [ "github-actions" ], "matchDepTypes": [ "uses-with" ], - "matchPackageNames": [ + "matchDepNames": [ "bun", "node" ], diff --git a/docker/reviewer/runtime-matrix.json b/docker/reviewer/runtime-matrix.json index e25d8d76..815569f0 100644 --- a/docker/reviewer/runtime-matrix.json +++ b/docker/reviewer/runtime-matrix.json @@ -17,6 +17,10 @@ "24.21.0": { "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.xz", "sha256": "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6" + }, + "24.22.0": { + "url": "https://nodejs.org/dist/v24.22.0/node-v24.22.0-linux-x64.tar.xz", + "sha256": "cc2f601cee82d9a5a233302fe307abc600727035fd7d2c3a3571755989e95008" } }, "bun": { @@ -54,6 +58,13 @@ "node": "24.21.0", "bun": "1.3.10", "gh": "2.101.0" + }, + { + "id": "reviewer-node2422-bun1310", + "platform": "linux/amd64", + "node": "24.22.0", + "bun": "1.3.10", + "gh": "2.101.0" } ] } diff --git a/packages/cli/test/reviewer/ci-job.test.ts b/packages/cli/test/reviewer/ci-job.test.ts index 32d96915..f349111e 100644 --- a/packages/cli/test/reviewer/ci-job.test.ts +++ b/packages/cli/test/reviewer/ci-job.test.ts @@ -22,14 +22,64 @@ import yaml from "js-yaml"; const SUITE_STEP = "Unit + integration tests, HOME-isolated (cli#430)"; +type WorkflowStep = { name?: string; uses?: string; with?: Record; env?: unknown }; + +/** The test job's steps, read with the planner's YAML schema. */ +function testJobSteps(workflowText: string): WorkflowStep[] { + const wf = yaml.load(workflowText, { schema: yaml.CORE_SCHEMA }) as { jobs: { test: { steps: WorkflowStep[] } } }; + const steps = wf.jobs.test?.steps; + if (!Array.isArray(steps)) throw new Error("no test job steps"); + return steps; +} + /** Whether this repository's HOME-isolated suite step declares an env key, read with the planner's YAML schema. */ function suiteStepDeclaresEnv(workflowText: string): boolean { - const wf = yaml.load(workflowText, { schema: yaml.CORE_SCHEMA }) as { jobs: { test: { steps: Array> } } }; - const step = wf.jobs.test.steps.find((s) => s.name === SUITE_STEP); + const step = testJobSteps(workflowText).find((s) => s.name === SUITE_STEP); if (!step) throw new Error(`no step named ${SUITE_STEP}`); return Object.hasOwn(step, "env"); } +/** Every uses step of the test job, named with the tag the reviewed allowlist gives its SHA. */ +function workflowSkipped(workflowText: string): string[] { + return testJobSteps(workflowText) + .map((s) => s.uses) + .filter((uses): uses is string => typeof uses === "string") + .map((uses) => { + const [name, sha] = uses.split("@"); + const action = (SKIPPED_ACTIONS as Record }>)[name.toLowerCase()]; + if (!action) throw new Error(`${name} is not an action the launcher skips`); + const tag = action.refs[sha]; + if (!tag) throw new Error(`${name}@${sha} is not in the reviewed allowlist`); + return `${name}@${tag}`; + }); +} + +/** The runtime pins the given planned jobs' setup actions declare, in job and step order. */ +function workflowPins(workflowText: string, jobIds: string[]): string[] { + const wf = yaml.load(workflowText, { schema: yaml.CORE_SCHEMA }) as { jobs: Record }; + const inputOf: Record = { "actions/setup-node": "node-version", "oven-sh/setup-bun": "bun-version" }; + return jobIds.flatMap((id) => { + const steps = wf.jobs[id]?.steps; + if (!Array.isArray(steps)) throw new Error(`no steps for job ${id}`); + return steps.flatMap((s) => { + if (typeof s.uses !== "string") return []; + const input = inputOf[s.uses.split("@")[0].toLowerCase()]; + if (!input) return []; + const value = s.with?.[input]; + if (value === undefined) throw new Error(`${s.uses} declares no ${input}`); + return [String(value)]; + }); + }); +} + +/** The Node version the test job's setup-node step pins. */ +function workflowNodePin(workflowText: string): string { + const step = testJobSteps(workflowText).find((s) => typeof s.uses === "string" && s.uses.split("@")[0].toLowerCase() === "actions/setup-node"); + const value = step?.with?.["node-version"]; + if (value === undefined) throw new Error("the test job declares no setup-node node-version"); + return String(value); +} + const here = dirname(fileURLToPath(import.meta.url)); const repo = resolve(here, "..", "..", "..", ".."); @@ -70,8 +120,9 @@ const extraJob = (id: string, body: string, runsOn = "ubuntu-latest") => ` ${id}:\n runs-on: ${runsOn}\n${body}`; describe("this repository's test job", () => { + const workflowText = readFileSync(resolve(repo, ".github", "workflows", "test.yml"), "utf8"); const p = planJob({ - workflowText: readFileSync(resolve(repo, ".github", "workflows", "test.yml"), "utf8"), + workflowText, workflowFile: ".github/workflows/test.yml", jobId: "test", baseBranch: "main", @@ -137,28 +188,24 @@ describe("this repository's test job", () => { test("the setup actions are skipped by name at their reviewed tags, their pins kept, and sfw is shimmed", () => { const t = jobOf(p, "test"); - expect(t.skipped.map((s: { uses: string; tag: string }) => `${s.uses.split("@")[0]}@${s.tag}`)).toEqual([ - "actions/checkout@v4.2.2", - "actions/setup-node@v4.4.0", - "oven-sh/setup-bun@v2.0.2", - "socketdev/action@v1.3.2", - ]); + expect(t.skipped.map((s: { uses: string; tag: string }) => `${s.uses.split("@")[0]}@${s.tag}`)).toEqual(workflowSkipped(workflowText)); if (!p.ok) return; - expect(p.pins.map((x: { range: string }) => x.range)).toEqual(["24.21.0", "1.3.10", "24.21.0", "1.3.10"]); + expect(p.pins.map((x: { range: string }) => x.range)).toEqual(workflowPins(workflowText, p.jobs.map((j: { id: string }) => j.id))); expect(p.shims).toEqual(["sfw"]); }); - test("every run: step of the test job is planned on Node 24.21.0", () => { + test("every run: step of the test job is planned on the Node the workflow pins", () => { const t = jobOf(p, "test"); + const node = workflowNodePin(workflowText); expect(t.steps.map((s: { index: number; node: string }) => [s.index, s.node])).toEqual([ - [4, "24.21.0"], - [6, "24.21.0"], - [7, "24.21.0"], - [8, "24.21.0"], - [9, "24.21.0"], - [10, "24.21.0"], - [11, "24.21.0"], - [12, "24.21.0"], + [4, node], + [6, node], + [7, node], + [8, node], + [9, node], + [10, node], + [11, node], + [12, node], ]); }); diff --git a/packages/cli/test/reviewer/resolve-runtime.test.ts b/packages/cli/test/reviewer/resolve-runtime.test.ts index 3d6b5e07..bea3929d 100644 --- a/packages/cli/test/reviewer/resolve-runtime.test.ts +++ b/packages/cli/test/reviewer/resolve-runtime.test.ts @@ -15,6 +15,8 @@ import { isSemverRange, resolveRuntime, satisfiesRange } from "../../../../scrip const here = dirname(fileURLToPath(import.meta.url)); const repo = resolve(here, "..", "..", "..", ".."); const table = JSON.parse(readFileSync(resolve(repo, "docker", "reviewer", "runtime-matrix.json"), "utf8")); +const node24 = table.images.find((i: { id: string }) => i.id === "reviewer-node24-bun1310"); +if (!node24) throw new Error("the runtime matrix has no reviewer-node24-bun1310 image"); const dockerfile = readFileSync(resolve(repo, "docker", "reviewer", "Dockerfile"), "utf8"); function refusal(r: ReturnType) { @@ -84,10 +86,16 @@ describe("A2 — requirement resolution", () => { expect(plan.ok).toBe(true); if (!plan.ok) return; const r = resolveRuntime({ table, manifest, runtimeFiles: {}, ciConstraints: plan.pins }); + // engines.node "22.x || 24.x" admits more than one image; the test job's + // exact setup-node pin selects one. The expected image is read from the + // workflow and the table, so a legitimate Node bump selects its own image + // instead of restating today's version here. + const nodePin = plan.pins.find((c: { tool: string }) => c.tool === "node")?.range; + const bunPin = plan.pins.find((c: { tool: string }) => c.tool === "bun")?.range; + const expected = table.images.filter((img: { node: string; bun: string }) => img.node === nodePin && img.bun === bunPin); + expect(expected.length).toBe(1); expect(r.ok).toBe(true); - // engines.node "22.x || 24.x" admits both images; the test job's exact - // setup-node 24.21.0 (for the github-review suite) selects one. - if (r.ok) expect(r.image.id).toBe("reviewer-node24-bun1310"); + if (r.ok) expect(r.image.id).toBe(expected[0].id); }); test("a runtime outside the matrix refuses and NAMES the missing image by its requirements", () => { @@ -126,10 +134,11 @@ describe("A2 — requirement resolution", () => { expect(refusal(resolveRuntime({ table, manifest: { engines: { node: ">=22" } } })).kind).toBe("ambiguous"); }); - test("a bun-only repository matches both images: image-level ambiguity refuses", () => { + test("a bun-only repository matches more than one image: image-level ambiguity refuses", () => { const r = refusal(resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10" } })); expect(r.kind).toBe("ambiguous"); - expect(r.message).toContain("matches more than one image (reviewer-node22-bun1310, reviewer-node24-bun1310)"); + const ids = table.images.filter((img: { bun: string }) => img.bun === "1.3.10").map((img: { id: string }) => img.id); + expect(r.message).toContain(`matches more than one image (${ids.join(", ")})`); }); test("mutually unsatisfiable requirements conflict", () => { @@ -170,16 +179,16 @@ describe("A2 — requirement resolution", () => { expect(beta.kind).toBe("out-of-matrix"); }); - test("partial comparators follow npm semver: >22 excludes 22.x, <=24 includes 24.x", () => { + test("partial comparators follow npm semver: >22 excludes 22.x, an upper bound selects one image", () => { const gt = refusal( resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: ">22" } }, runtimeFiles: { ".nvmrc": "22.22.1" } }), ); expect(gt.kind).toBe("conflicting"); const both = refusal(resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: ">=22 <=24" } } })); expect(both.kind).toBe("ambiguous"); - const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: ">22 <=24" } } }); + const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: `>22 <=${node24.node}` } } }); expect(r.ok).toBe(true); - if (r.ok) expect(r.image.id).toBe("reviewer-node24-bun1310"); + if (r.ok) expect(r.image.id).toBe(node24.id); }); test(".tool-versions is read, with comments stripped", () => { @@ -221,9 +230,9 @@ describe("A2 — requirement resolution", () => { test("a version file listing two versions refuses; a commented one is read", () => { expect(refusal(resolveRuntime({ table, manifest: {}, runtimeFiles: { ".nvmrc": "22\n24\n" } })).kind).toBe("ambiguous"); - const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10" }, runtimeFiles: { ".nvmrc": "# team pin\n24\n" } }); + const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10" }, runtimeFiles: { ".nvmrc": `# team pin\n${node24.node}\n` } }); expect(r.ok).toBe(true); - if (r.ok) expect(r.image.id).toBe("reviewer-node24-bun1310"); + if (r.ok) expect(r.image.id).toBe(node24.id); }); test("non-string engines / packageManager values refuse by name, never skipped", () => { diff --git a/packages/cli/test/reviewer/reviewer-launch.test.ts b/packages/cli/test/reviewer/reviewer-launch.test.ts index ae49b4a6..37e241d8 100644 --- a/packages/cli/test/reviewer/reviewer-launch.test.ts +++ b/packages/cli/test/reviewer/reviewer-launch.test.ts @@ -57,6 +57,8 @@ const here = dirname(fileURLToPath(import.meta.url)); const repo = resolve(here, "..", "..", "..", ".."); const table = loadTable(resolve(repo, "docker", "reviewer", "runtime-matrix.json")); const node22 = table.images.find((i: { id: string }) => i.id === "reviewer-node22-bun1310"); +const node24 = table.images.find((i: { id: string }) => i.id === "reviewer-node24-bun1310"); +if (!node24) throw new Error("the runtime matrix has no reviewer-node24-bun1310 image"); const launcher = resolve(repo, "scripts", "reviewer", "reviewer-launch.mjs"); const CHECKOUT = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683"; @@ -511,12 +513,12 @@ describe("A2/A3 — the review build", () => { }); test("a pin that resolves to ANOTHER matrix image refuses (wrong-image) before anything runs", async () => { - writeWorkspace("24.x", " - run: echo ran > review-marker\n"); + writeWorkspace(node24.node, " - run: echo ran > review-marker\n"); const r = await build(); expect(r.ok).toBe(false); if (!r.ok) { expect(r.refusal.kind).toBe("wrong-image"); - expect(r.refusal.message).toContain("resolves to reviewer-node24-bun1310"); + expect(r.refusal.message).toContain(`resolves to ${node24.id}`); } expect(existsSync(join(workspace, "review-marker"))).toBe(false); }); From b9348f3b9eaf31297b8b2ab0e004d38f522acc40 Mon Sep 17 00:00:00 2001 From: flint Date: Sun, 25 Oct 2026 15:35:39 +0000 Subject: [PATCH 2/3] test(ci): keep the trusted runtime matrix unchanged With the uses-with rule matching dep names, Renovate no longer proposes the Node bump, so the Node 24.22.0 entry and its image are removed and the resolve-runtime and reviewer-launch cases go back to main's expectations. --- .../unreleased/fixed-renovate-action-bumps.md | 2 +- docker/reviewer/runtime-matrix.json | 11 ---------- .../cli/test/reviewer/resolve-runtime.test.ts | 20 ++++++++----------- .../cli/test/reviewer/reviewer-launch.test.ts | 6 ++---- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/.changelog/unreleased/fixed-renovate-action-bumps.md b/.changelog/unreleased/fixed-renovate-action-bumps.md index 563d8764..bf751b5e 100644 --- a/.changelog/unreleased/fixed-renovate-action-bumps.md +++ b/.changelog/unreleased/fixed-renovate-action-bumps.md @@ -1,3 +1,3 @@ -- **Renovate's setup-action runtime rule matches by dependency name, and reviewer tests read workflow versions.** The github-actions `uses-with` rule now matches `depName`, so setup-node's `node` dependency (package `actions/node-versions`) is skipped; the trusted runtime matrix gains Node 24.22.0. +- **Renovate's setup-action runtime rule matches by dependency name, and reviewer tests read workflow versions.** The github-actions `uses-with` rule now matches `depName`, so setup-node's `node` dependency (package `actions/node-versions`) is skipped. Reviewer tests derive skipped-action tags and runtime pins from the workflow, and fail on an unallowlisted SHA. diff --git a/docker/reviewer/runtime-matrix.json b/docker/reviewer/runtime-matrix.json index 815569f0..e25d8d76 100644 --- a/docker/reviewer/runtime-matrix.json +++ b/docker/reviewer/runtime-matrix.json @@ -17,10 +17,6 @@ "24.21.0": { "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-linux-x64.tar.xz", "sha256": "fd8e59d5a511510f6a298afb548f18c7d2b1be404d8b4a27d94fbe49f56cb2d6" - }, - "24.22.0": { - "url": "https://nodejs.org/dist/v24.22.0/node-v24.22.0-linux-x64.tar.xz", - "sha256": "cc2f601cee82d9a5a233302fe307abc600727035fd7d2c3a3571755989e95008" } }, "bun": { @@ -58,13 +54,6 @@ "node": "24.21.0", "bun": "1.3.10", "gh": "2.101.0" - }, - { - "id": "reviewer-node2422-bun1310", - "platform": "linux/amd64", - "node": "24.22.0", - "bun": "1.3.10", - "gh": "2.101.0" } ] } diff --git a/packages/cli/test/reviewer/resolve-runtime.test.ts b/packages/cli/test/reviewer/resolve-runtime.test.ts index bea3929d..6eaa3754 100644 --- a/packages/cli/test/reviewer/resolve-runtime.test.ts +++ b/packages/cli/test/reviewer/resolve-runtime.test.ts @@ -15,8 +15,6 @@ import { isSemverRange, resolveRuntime, satisfiesRange } from "../../../../scrip const here = dirname(fileURLToPath(import.meta.url)); const repo = resolve(here, "..", "..", "..", ".."); const table = JSON.parse(readFileSync(resolve(repo, "docker", "reviewer", "runtime-matrix.json"), "utf8")); -const node24 = table.images.find((i: { id: string }) => i.id === "reviewer-node24-bun1310"); -if (!node24) throw new Error("the runtime matrix has no reviewer-node24-bun1310 image"); const dockerfile = readFileSync(resolve(repo, "docker", "reviewer", "Dockerfile"), "utf8"); function refusal(r: ReturnType) { @@ -88,8 +86,7 @@ describe("A2 — requirement resolution", () => { const r = resolveRuntime({ table, manifest, runtimeFiles: {}, ciConstraints: plan.pins }); // engines.node "22.x || 24.x" admits more than one image; the test job's // exact setup-node pin selects one. The expected image is read from the - // workflow and the table, so a legitimate Node bump selects its own image - // instead of restating today's version here. + // workflow and the table. const nodePin = plan.pins.find((c: { tool: string }) => c.tool === "node")?.range; const bunPin = plan.pins.find((c: { tool: string }) => c.tool === "bun")?.range; const expected = table.images.filter((img: { node: string; bun: string }) => img.node === nodePin && img.bun === bunPin); @@ -134,11 +131,10 @@ describe("A2 — requirement resolution", () => { expect(refusal(resolveRuntime({ table, manifest: { engines: { node: ">=22" } } })).kind).toBe("ambiguous"); }); - test("a bun-only repository matches more than one image: image-level ambiguity refuses", () => { + test("a bun-only repository matches both images: image-level ambiguity refuses", () => { const r = refusal(resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10" } })); expect(r.kind).toBe("ambiguous"); - const ids = table.images.filter((img: { bun: string }) => img.bun === "1.3.10").map((img: { id: string }) => img.id); - expect(r.message).toContain(`matches more than one image (${ids.join(", ")})`); + expect(r.message).toContain("matches more than one image (reviewer-node22-bun1310, reviewer-node24-bun1310)"); }); test("mutually unsatisfiable requirements conflict", () => { @@ -179,16 +175,16 @@ describe("A2 — requirement resolution", () => { expect(beta.kind).toBe("out-of-matrix"); }); - test("partial comparators follow npm semver: >22 excludes 22.x, an upper bound selects one image", () => { + test("partial comparators follow npm semver: >22 excludes 22.x, <=24 includes 24.x", () => { const gt = refusal( resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: ">22" } }, runtimeFiles: { ".nvmrc": "22.22.1" } }), ); expect(gt.kind).toBe("conflicting"); const both = refusal(resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: ">=22 <=24" } } })); expect(both.kind).toBe("ambiguous"); - const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: `>22 <=${node24.node}` } } }); + const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10", engines: { node: ">22 <=24" } } }); expect(r.ok).toBe(true); - if (r.ok) expect(r.image.id).toBe(node24.id); + if (r.ok) expect(r.image.id).toBe("reviewer-node24-bun1310"); }); test(".tool-versions is read, with comments stripped", () => { @@ -230,9 +226,9 @@ describe("A2 — requirement resolution", () => { test("a version file listing two versions refuses; a commented one is read", () => { expect(refusal(resolveRuntime({ table, manifest: {}, runtimeFiles: { ".nvmrc": "22\n24\n" } })).kind).toBe("ambiguous"); - const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10" }, runtimeFiles: { ".nvmrc": `# team pin\n${node24.node}\n` } }); + const r = resolveRuntime({ table, manifest: { packageManager: "bun@1.3.10" }, runtimeFiles: { ".nvmrc": "# team pin\n24\n" } }); expect(r.ok).toBe(true); - if (r.ok) expect(r.image.id).toBe(node24.id); + if (r.ok) expect(r.image.id).toBe("reviewer-node24-bun1310"); }); test("non-string engines / packageManager values refuse by name, never skipped", () => { diff --git a/packages/cli/test/reviewer/reviewer-launch.test.ts b/packages/cli/test/reviewer/reviewer-launch.test.ts index 37e241d8..ae49b4a6 100644 --- a/packages/cli/test/reviewer/reviewer-launch.test.ts +++ b/packages/cli/test/reviewer/reviewer-launch.test.ts @@ -57,8 +57,6 @@ const here = dirname(fileURLToPath(import.meta.url)); const repo = resolve(here, "..", "..", "..", ".."); const table = loadTable(resolve(repo, "docker", "reviewer", "runtime-matrix.json")); const node22 = table.images.find((i: { id: string }) => i.id === "reviewer-node22-bun1310"); -const node24 = table.images.find((i: { id: string }) => i.id === "reviewer-node24-bun1310"); -if (!node24) throw new Error("the runtime matrix has no reviewer-node24-bun1310 image"); const launcher = resolve(repo, "scripts", "reviewer", "reviewer-launch.mjs"); const CHECKOUT = "actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683"; @@ -513,12 +511,12 @@ describe("A2/A3 — the review build", () => { }); test("a pin that resolves to ANOTHER matrix image refuses (wrong-image) before anything runs", async () => { - writeWorkspace(node24.node, " - run: echo ran > review-marker\n"); + writeWorkspace("24.x", " - run: echo ran > review-marker\n"); const r = await build(); expect(r.ok).toBe(false); if (!r.ok) { expect(r.refusal.kind).toBe("wrong-image"); - expect(r.refusal.message).toContain(`resolves to ${node24.id}`); + expect(r.refusal.message).toContain("resolves to reviewer-node24-bun1310"); } expect(existsSync(join(workspace, "review-marker"))).toBe(false); }); From 25a4ab5e54424376bc6e9f57994610f9580735ef Mon Sep 17 00:00:00 2001 From: flint Date: Sun, 25 Oct 2026 16:25:35 +0000 Subject: [PATCH 3/3] docs(ci): name the datasource repository correctly in the Renovate rule --- .github/renovate.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/renovate.json b/.github/renovate.json index 70940525..db785bac 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -35,7 +35,7 @@ "enabled": false }, { - "description": "A setup action's uses-with dependency is named bun or node while its package is the action (e.g. actions/node-versions), so match it by dep name.", + "description": "Match the Node/Bun uses-with dependencies by depName; their datasource packageName can differ.", "matchManagers": [ "github-actions" ],