ci(release): gate the tag release on make ci before publishing - #413
Draft
bketelsen wants to merge 1 commit into
Draft
ci(release): gate the tag release on make ci before publishing#413bketelsen wants to merge 1 commit into
bketelsen wants to merge 1 commit into
Conversation
A tag push is the only trigger that publishes, and a tag is not required to point at a commit the Tests workflow ever saw green: it can name an old commit, a branch head, or a `main` whose checks failed. Nothing stood between such a tag and a published, attested, R2-mirrored release. Add a `gate` job that checks the tree out, sets Go up from `go.mod`, installs the same `mise.lock`-verified `golangci-lint` the Tests workflow's Lint job installs, and runs `make ci` (verify-static, unit tests with the coverage floor, e2e, race detector, linux amd64/arm64 cross-builds) under `permissions: contents: read`. The `goreleaser` job now declares `needs: gate`, so GoReleaser, the provenance attestation, the R2 publish, and the snosi dispatch are all skipped when the gate fails. No new tooling is introduced, and the Tests workflow's triggers and jobs are untouched. `TestReleaseWorkflowGatesPublishOnCI` pins the contract on pull requests, where the tag workflow itself never runs: the publish job must need a job that exists, holds only `contents: read`, checks out, sets Go up from `go.mod`, and runs `make ci`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
.github/workflows/release.ymltriggers only on a tag push, and a tag is notrequired to point at a commit the Tests workflow ever saw green — it can name
an old commit, a branch head, or a
mainwhose checks failed. Nothing stoodbetween such a tag and a published, attested, R2-mirrored release plus a
frostyard/snosirebuild dispatch.This adds a
gatejob (CI gate) that runs beforegoreleaser: checkout(
persist-credentials: false),actions/setup-gofromgo.mod, the samemise.lock-verifiedgolangci-lintthe Tests workflow's Lint job installs viajdx/mise-action, and thenmake ci— which already encapsulatesverify-static (tidy, vet, gofmt, pinned lint), unit tests with the coverage
floor, the e2e suite, the race detector, and the linux amd64/arm64
cross-builds. The
goreleaserjob now declaresneeds: gate, so GoReleaser,actions/attest-build-provenance, the R2 publish, and the snosi dispatch areall skipped when the gate fails.
permissions: contents: readon the new job.No new tooling is introduced, and the Tests workflow's triggers and jobs are
untouched — nothing that gates PRs or
maintoday is weakened.updex/release_workflow_contract_test.gogainsTestReleaseWorkflowGatesPublishOnCI, which pins the contract on pullrequests (the tag workflow itself never runs on one, so a contract test is the
only pre-merge gate available, as with the existing trigger and provenance
tests in that file): the publish job must
needs:a job that exists, holdsonly
contents: read, checks the tree out, sets Go up fromgo.mod, and runsmake ci.Addresses Snowcat work item
7bee7eb3-1871-4e5a-8c96-7bc11ec8b91e.Checks
make fmt— code is formattedmake ci— tidy, vet, gofmt, lint (.golangci.yml), unit tests, the80.0% coverage floor (
make test-coverage-checkthenmake coverage-check), race tests, linuxamd64/arm64 builds
go test -v ./cmd/updex ./tests/e2e/...green (viamake ci)Risk classification
Rationale:
.github/workflows/release.yml, inside theworkflow-and-permissionsprotected boundary inpolicies/agent-governance.json(
.github/workflows/**,decision: review-required,minimum_risk_tier: high), and it changes release automation and theartifact-provenance path — Tier 3's "GitHub Actions permissions, release
automation, or artifact provenance". Per
docs/risk-tiers.md, touching aprotected boundary makes it Tier 4: Critical.
Threat/abuse analysis (trust boundary crossed). The boundary is
tag-push → publish: the tag workflow holds
contents: write,id-token: write, andattestations: write, and its output is an artifact setsigned with GitHub's Sigstore instance and mirrored to
repository.frostyard.org. Anyone able to push a tag could previouslymanufacture a provenance-attested release from a commit that fails lint,
vet, the tests, the coverage floor, or the race detector, because the
attestation proves origin, not quality — and downstream
snosiimage buildsconsume it. This PR narrows that: the publish path is now reachable only
through a job that ran the same checks
mainand every PR run. The new jobholds
contents: readonly and usespersist-credentials: false, so itneither widens nor inherits the publishing job's write scopes; the
goreleaserjob's own permissions, steps, action pins, and secret handlingare unchanged. Failure mode added: a flaky gate run blocks a release until
re-run — availability, not integrity, and strictly the safer direction.
Rollback plan. Revert this commit (drop the
gatejob and theneeds:line, drop
TestReleaseWorkflowGatesPublishOnCI) and the release path returnsbyte-for-byte to its current behavior; no state, tag, or published artifact is
migrated, so nothing needs undoing beyond the revert. A maintainer who needs a
release out during an incident can also re-run only the
goreleaserjob, orrevert on a branch and tag from there.
Docs housekeeping
AGENTS.md(Release Automation) anddocs/design/overview.mddescribethe gate job, why a tag needs it, and the new contract test
TEMPLATE.mdand indexed indocs/README.md— n/a, no new docsimplements the existing gate/never-relax posture
(
policies/agent-governance.jsonnever_relax), it does not decideanything new
AGENTS.mdedited as thecanonical file
Verification
make ci— green (==> CI gate passed). One local-environment note:the first run failed at
go testwitherror obtaining VCS status: exit status 128, a-buildvcsstampingartifact of running inside a linked git worktree in this sandbox, not a
repository defect. Re-run as
GOFLAGS=-buildvcs=false make ciand everystage passed: tidy, vet, gofmt,
golangci-lint2.13.1 (0 issues),unit tests,
make test-coverage-check+make coverage-check(total86.2%,
.coverage-baseline86.2 — unchanged, so no baseline bump), e2e,race detector, and both cross-builds. CI's own
actions/checkoutis anormal clone and stamps VCS fine, as today's Tests jobs already show.
actionlint .github/workflows/release.yml— clean (v1.7.12).node scripts/check-docs.mjsgreen —docs_index_coverage,link_integrity,symlink_resolutionall 1.000.TestReleaseWorkflowGatesPublishOnCInegative-tested: with theneeds: gateline removed it fails with"goreleaser job declares no
needs; a tag that fails CI would publishunchecked"; it passes with the line restored. So the test is a real
gate, not a tautology.
PR review rubric
Acceptance criteria: what is proven, and what is not
The originating work item lists three acceptance criteria. Being explicit
about which are evidenced here:
goreleaserdeclaresneeds: gate, andgaterunsmake cibefore any GoReleaser or publish step. Verified by reading the job graph,
by
actionlint, and byTestReleaseWorkflowGatesPublishOnCI(includingthe negative test above).
deliberately broken commit causes
gateto fail andgoreleaserto beskipped." Confirming this end-to-end means pushing a throwaway tag, which
on this repository means exercising the real publish path (
contents: write, Sigstore attestation, thefrostyard/updexrelease list, the R2bucket, and a
frostyard/snosidispatch). That is a maintainer's call, notsomething to do unilaterally, so no tag was pushed. What is shown
statically:
needs:in GitHub Actions skips the dependent job when thedependency fails, and every publishing step lives inside
goreleaser.completed release exactly as before." The
goreleaserjob's steps, actionpins, permissions, secrets, and ordering are byte-for-byte unchanged (only
the
needs:line was added), so no regression is expected — but a real tagis the only way to prove it, and none was pushed.
Suggested maintainer test when convenient: on a throwaway branch, break a test
deliberately, push a
v0.0.0-gate-testtag, confirmgatefails andgoreleasershows as skipped with nothing released or attested; then revertthe breakage, re-tag, and confirm the release completes with the usual
artifacts, attestation, and R2 publish.