Skip to content

fix(ci): assert the release toolchain pins by shape, not by literal value - #1324

Merged
krisarmstrong merged 1 commit into
mainfrom
fix/release-contract-pin-shape
Sep 17, 2026
Merged

krisarmstrong merged 1 commit into
mainfrom
fix/release-contract-pin-shape

Conversation

@krisarmstrong

Copy link
Copy Markdown
Collaborator

Summary

scripts/check-release-workflow-contract.sh required five Renovate-managed values as exact literals. The value therefore lived in two places — release.yml, which Renovate edits, and this gate, which it does not — so every release-toolchain bump was born red and unfixable by a rebase.

PR #1298 is blocked on exactly this: Renovate moved the builder image to v1.27.1, and the contract's Workflow Lint job failed with FAIL: contract rejected the real workflow. Note the shape of that failure — the gate was not catching a regression, it was rejecting the workflow it is supposed to describe.

This replaces the five literals with require_pin, which asserts the shape of each pin. The gate keeps its purpose: it exists so nobody silently un-pins the release toolchain, and "pinned to an exact version and a full digest" is a shape a legitimate bump satisfies and an un-pinning does not.

Seed hit this first and solved it the same way — this ports seed's require_pin helper and its rationale verbatim, so the two repos' contracts stay readable side by side. Stem was the only repo still carrying literals: seed and trellis are already on goreleaser-cross v1.27.1 on main and merged their bumps cleanly, which is why only stem's PR went red.

Unblocks #1298. Does not itself bump any toolchain version — release.yml is untouched.

Linked Issue

Related to #1298 (and seed#2622, the sibling finding)

Testing Evidence

RED — the old gate rejects the legitimate v1.27.1 bump that #1298 carries:

$ ./scripts/check-release-workflow-contract.sh
release workflow contract missing: image: goreleaser/goreleaser-cross:v1.27.0@sha256:3ce3506ee9179c4122ba0b5dc13ab564ff259fb65f45bfad005ddd5e4a3d326d
OLD GATE EXIT=1

GREEN — with require_pin, against the same bumped workflow:

$ ./scripts/check-release-workflow-contract.sh
release workflow contract OK
NEW GATE EXIT=0

The existing self-test still passes in full, including its unpinned-builder-image mutant (which substitutes :latest, so the shape regex still rejects it) and its positive case:

$ ./scripts/test-check-release-workflow-contract.sh
ok: rejected publish-without-event-check
ok: rejected snapshot-predicate-drift
ok: rejected dispatch-refusal-removed
ok: rejected provenance-condition-loosened
ok: rejected publish-skips-validation
ok: rejected workspace-assertion-removed
ok: rejected unpinned-builder-image
ok: rejected unpinned-action
ok: rejected syft-checksum-removed
ok: rejected mutable-latest-lookup
ok: rejected write-permissions-at-workflow-level
ok: accepted the real workflow
release workflow contract self-test passed

Because a shape assertion is weaker than a literal, three further mutations confirm the new checks still catch an actual un-pinning — these are the cases the existing mutants do not cover:

-- mutant drop-image-digest     (v1.27.1@sha256:... -> v1.27.1)   rejected
-- mutant truncate-syft-sha     (64 hex -> 8 hex)                 rejected
-- mutant float-cosign          ("v3.1.3" -> "latest")            rejected

Gate green on the unbumped workflow too (EXIT=0), so this does not depend on #1298 landing first. shellcheck scripts/check-release-workflow-contract.sh clean (exit 0).

Security and Release Checklist

  • The gate still fails closed on an un-pinned image, a truncated checksum, or a floated version — proven by three mutations above, not asserted.
  • No release-toolchain version changed; release.yml is untouched in this PR.
  • Digest pinning, checksum verification, and action pinning all remain required.
  • No product code, no route, no auth surface touched.
  • shellcheck clean.

…alue

The release-workflow contract required five Renovate-managed values as exact
literals. The value therefore lived in two places — the workflow, which
Renovate edits, and this gate, which it does not — so every toolchain bump
was born red and no rebase could fix it. PR #1298 was blocked on exactly
this: Renovate moved the builder image to v1.27.1 and the contract rejected
its own real workflow.

The gate keeps its purpose. It exists so nobody silently UN-pins the release
toolchain, and "pinned to an exact version and a full digest" is a shape a
bump satisfies and an un-pinning does not. Seed hit this first and solved it
the same way (seed#2622); this ports its require_pin helper.
@krisarmstrong
krisarmstrong added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 9d6f307 Sep 17, 2026
38 checks passed
@krisarmstrong
krisarmstrong deleted the fix/release-contract-pin-shape branch September 17, 2026 15:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants