From 81bc24e226a8fe58f8b9dbb40e33347d2f00748a Mon Sep 17 00:00:00 2001 From: mintaka Date: Wed, 16 Sep 2026 22:27:08 -0400 Subject: [PATCH] test(release): pin the release-pr main-ref guard (RIG-3813) The main-ref guard on `release-pr` is a security invariant that nothing observed. actionlint validates syntax only, and no test inspected release job gating, so an edit deleting the guard would have stayed green while re-opening a path for a `workflow_dispatch` from any ref to cut a release from `main`. Pin it in `tools/toolchain`, which already holds the precedent for asserting about a workflow file from a bun test (the Postgres image digest shared by ci.yml and the local harness). The assertion isolates the `release-pr` job block before matching, because four other jobs carry the identical `github.ref` predicate and a whole-file substring search would pass with the guard deleted from the one job that needs it. Verified against that exact mutation: removing only `release-pr`'s guard while every sibling keeps its own turns the case red. An unlocatable or implausibly small job block fails rather than reporting a pass, so the test cannot turn "I could not check this" into green. `release.yml` joins the `test` task inputs for the same reason ci.yml is already there: a file a test asserts about must invalidate the task, or the assertion stays cached and stops running against what it constrains. Co-authored-by: Matt Wilkinson --- tools/toolchain/moon.yml | 4 +- tools/toolchain/release-guard-core.test.ts | 53 ++++++++++++++++++++++ 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 tools/toolchain/release-guard-core.test.ts diff --git a/tools/toolchain/moon.yml b/tools/toolchain/moon.yml index d67dd25eb..ce653bbea 100644 --- a/tools/toolchain/moon.yml +++ b/tools/toolchain/moon.yml @@ -39,12 +39,14 @@ tasks: # edit to one must invalidate this task — otherwise it stays cached and the # assertion silently stops re-running against the thing it constrains. # ci.yml + pgtest.go: a test asserts their two Postgres image digests are - # equal. flake.nix + devenv.nix: version-guard-core.test.ts lifts the real + # equal. release.yml: a test pins the release-pr main-ref guard. + # flake.nix + devenv.nix: version-guard-core.test.ts lifts the real # version.txt guards out of both and asserts they are still extractable. inputs: - '*.ts' - '/bun.lock' - '/.github/workflows/ci.yml' + - '/.github/workflows/release.yml' - '/go/internal/pgtest/pgtest.go' - '/flake.nix' - '/devenv.nix' diff --git a/tools/toolchain/release-guard-core.test.ts b/tools/toolchain/release-guard-core.test.ts new file mode 100644 index 000000000..e451065d9 --- /dev/null +++ b/tools/toolchain/release-guard-core.test.ts @@ -0,0 +1,53 @@ +// This test is CAPABLE OF FAILING: an absent or unparseable job block must fail +// rather than turn an uncheckable release guard into a pass. + +import { describe, expect, test } from "bun:test"; + +const releasePrBlock = (workflow: string): string => { + const matches = workflow.match( + /^ {2}release-pr:\n([\s\S]*?)(?=^ {2}\S|(?![\s\S]))/m, + ); + if (matches === null) + throw new Error( + "release-pr block could not be located; the privileged contents-write tag-minting job is unchecked", + ); + expect( + matches, + "release-pr must be present so the privileged contents-write tag-minting job stays guarded", + ).toHaveLength(2); + const [, block] = matches; + if (block === undefined) + throw new Error( + "release-pr block could not be extracted; the privileged contents-write tag-minting job is unchecked", + ); + expect( + block.length, + "release-pr must contain a substantial job block so the privileged contents-write tag-minting job is actually checked", + ).toBeGreaterThan(100); + return block; +}; + +const readReleasePrBlock = async (): Promise => { + const root = new URL("../../", import.meta.url).pathname; + return releasePrBlock( + await Bun.file(`${root}.github/workflows/release.yml`).text(), + ); +}; + +describe("the release-pr main-ref guard", () => { + test("keeps the privileged tag-minting job restricted to main", async () => { + const block = await readReleasePrBlock(); + expect( + block, + "release-pr must require main because it holds contents: write and mints release tags", + ).toMatch(/^ {4}if: github\.ref == 'refs\/heads\/main'$/m); + }); + + test("pins release-please to the main target branch", async () => { + const block = await readReleasePrBlock(); + expect( + block, + "release-pr must pass target-branch: main because it holds contents: write and mints release tags", + ).toMatch(/^ {10}target-branch: main$/m); + }); +});