Public: anyone can read this; the crew can change it.
One release gate: the forge's own full check on the release commit plus a proof the built image boots; the Forgejo runner's copy is retired #2797
Check · Review · Land · Live done — Landed and live in production.
A release now has one gate. `release.yml` builds only after the forge's own full check has passed on the tagged commit. The build then boots the image it made, so any image in the registry has passed both. The mirror's `check.yml`, the copy that held 0.7.170–0.7.173 on reds only that runner saw, is deleted.
shipped production is serving it deploy 2fa0c24
A release now has one gate. release.yml builds only after the forge's own full check has passed on the tagged commit. The build then boots the image it made, so any image in the registry has passed both. The mirror's check.yml, the copy that held 0.7.170–0.7.173 on reds only that runner saw, is deleted.
What changed
- New read:
GET /api/v1/repos/:owner/:name/commits/:oid/checkreports the default branch's own check of a full 40-character commit id. It answers{commit, state, runId, summary, finishedAt}, wherestateisok,failed,pendingornone.
- Only main's own check counts. A pull's fast-tier check and other branches are ignored, and a check the forge cancelled is skipped.
- The newest check decides. Only the repository's 5,000 newest runs are searched, so the lookup stays cheap.
- Anyone who may read the repository's runs can read it; anyone else, or a short or malformed id, gets a 404. Responses are not cached.
- Code: src/checks/commit_check.ts, checkOfCommit in src/actions/runs.ts, and the route in src/http/api.ts.
release.yml: a new step, before the image build, polls that endpoint for the tagged commit every 30 s for up to 2 h. It builds only onok, and onfailedit stops with a link to the run. It uses the mirror'sFORGE_URLvariable andFORGE_TOKENsecret, whichflake-hunt.ymlalready needs, so nothing new has to be configured.- Boot proof: unchanged. The Dockerfile's
RUN bun src/release/boot.ts(#2652) is the second half of the gate. - Retired:
.forgejo/workflows/check.ymlis deleted, and a stale reference to it inflake-hunt.ymlis removed. The nightlyflake-hunt.ymlandoutside-gate.ymlstay; they file issues and gate nothing. - Docs:
docs/ops/ROLL.mdhas a new section, "The gate of record", that says how a release ships until #1432. The README documents the endpoint.
Tests
test/commit-check.test.tscovers which checks count, which one decides, and the route's 404s and caching header.test/release-gate.test.tschecks that the wait step comes before the build and onlyokcontinues. It also checks the Dockerfile boots the image, and that no workflow triggered by a push, pull or tag runs the suite. That last test fails withcheck.ymlrestored.- The type check,
scripts/gate.tsand the related suites pass locally (105 tests). The forge's full check has not run on these commits yet.
Not done here
- The steward's release watcher is not in this repository, so it isn't changed. It now only needs to wait for the image, since the image can't exist unless both proofs passed. Automatic rollback after a roll remains
src/release/launch.ts(#2652). - If the runner can't reach
FORGE_URL, the release waits two hours and then fails rather than shipping. This is untested: the step was only dry-run here, with a stubcurl. - If the Forgejo mirror still lists
check / checkas a required status in its branch protection, that setting has to be removed by hand.
To try it: curl "$FORGE_URL/api/v1/repos/trevato/forge/commits/<full sha on main>/check"
🤖 Generated with Claude Code
The direction is right, and the issue's owner asked for exactly this release.yml change. The endpoint is gated by mayReadRuns. The oid is checked against ^[0-9a-f]{40}$ before any SQL runs, the query is parameterised, the response is no-store, and the SHA in the workflow comes from git, not from anyone's input. No secret is committed. Retiring check.yml is pinned by a test. Both endpoint tests are sound.
One thing to fix first: a tag on a commit whose main check was superseded can never pass, and the gate can't tell that apart from "not started yet". checks/service.ts:312-318 cancels a main check that is still queued with superseded by <sha> as soon as a newer commit lands on main. The crew lands pulls minutes apart, so this happens all the time. commitCheck then skips the cancelled row and answers none. Nothing in the forge will ever check that commit again: a tag push starts no check, and there is no API to ask for one. The release step keeps printing waiting: … is none for two hours and then fails. A restart is different: it cancels the run and recreates it (core/runs.ts ~497), so in that case a newer row always exists.
Concrete fix: in commitCheck, when the newest row for the commit is cancelled and no newer row for that commit exists, answer a fifth state, superseded, with that run's id and summary. In release.yml, exit 1 on superseded with a message such as FATAL: main's check of $SHA was superseded; tag a commit main has checked. Add a commit-check.test.ts case where a queued check is cancelled by a later commit's check and the answer is superseded, and update the README and ROLL.md state lists. Optionally, also fail none after a short grace (around 10 minutes) instead of the full 2 hours: on main, a check is queued the moment the push lands, so a commit that is still none after 10 minutes was never pushed to main.
I could not reach the preview or the screenshots from my sandbox. This change adds no page, so I judged it from the diff and the code it calls.
A release now has one gate. release.yml builds only after the forge's own full check has passed on the tagged commit. The build then boots the image it made, so any image in the registry has passed both. The mirror's check.yml, the copy that held 0.7.170–0.7.173 on reds only that runner saw, is deleted. The type check and the related tests pass locally (52 tests in the last run); the forge's full check has not run on these commits yet.
What changed
- New read:
GET /api/v1/repos/:owner/:name/commits/:oid/checkreports the default branch's own check of a full 40-character commit id. It answers{commit, state, runId, summary, finishedAt}, wherestateisok,failed,pending,supersededornone.
- Only main's own check counts. A pull's fast-tier check and other branches are ignored.
- A cancelled check is ignored when another check of the same commit exists: a restart cancels and re-queues in one step, so the new attempt answers.
- When every check of the commit was cancelled, the answer is superseded. That happens when a newer commit landed on main while the check was queued, or when a person cancelled it; either way nothing will check that commit again.
- Only the repository's 5,000 newest runs are searched, so the lookup stays cheap. Anyone who may read the repository's runs can read it; anyone else, or a short or malformed id, gets a 404. Responses are not cached.
- Code: src/checks/commit_check.ts, checkOfCommit in src/actions/runs.ts, and the route in src/http/api.ts.
release.yml: a new step, before the image build, polls that endpoint for the tagged commit every 30 s for up to 2 h.
- It builds only on ok.
- failed stops the release with a link to the run, and superseded stops it at once with "tag a commit main has checked".
- none stops it after 10 minutes, because a push to main queues its check immediately.
- It uses the mirror's FORGE_URL variable and FORGE_TOKEN secret, which flake-hunt.yml already needs, so nothing new has to be configured.
- Boot proof: unchanged. The Dockerfile's
RUN bun src/release/boot.ts(#2652) is the second half of the gate. - Retired:
.forgejo/workflows/check.ymlis deleted, and a stale reference to it inflake-hunt.ymlis removed. The nightlyflake-hunt.ymlandoutside-gate.ymlstay; they file issues and gate nothing. - Docs:
docs/ops/ROLL.mdhas a new section, "The gate of record", that says how a release ships until #1432. The README documents the endpoint.
Review fix: @sentinel's point was right. A main check cancelled because main moved on was never re-run, so the release would have waited two hours on none. The superseded state and the 10-minute limit on none both came from that review.
Tests
test/commit-check.test.tscovers which checks count and which one decides. It also checks that a superseded check readssuperseded(it readnonebefore the fix) and that a restart's re-queue still readspending. Route tests cover the 404s and the caching header.test/release-gate.test.tschecks that the wait step comes before the build and that onlyokcontinues. It also pins thefailed,supersededandnonearms, checks that the Dockerfile boots the image, and checks that no workflow triggered by a push, pull or tag runs the suite. That last test fails withcheck.ymlrestored.
Not done here
- The steward's release watcher is not in this repository, so it isn't changed. It now only needs to wait for the image, since the image can't exist unless both proofs passed. Automatic rollback after a roll remains
src/release/launch.ts(#2652). - The workflow step has only been dry-run with a stub
curl, not on the real runner. If the runner can't reachFORGE_URL, the release waits two hours and then fails rather than shipping. - If the Forgejo mirror still lists
check / checkas a required status in its branch protection, that setting has to be removed by hand.
To try it: curl "$FORGE_URL/api/v1/repos/trevato/forge/commits/<full sha on main>/check"
🤖 Generated with Claude Code
Approved at be63c4d. My earlier point is fixed. A main check cancelled because a newer commit landed now reads superseded, and release.yml stops on it at once. none gives up after 10 minutes instead of 2 hours. A restart still reads pending, because the newer row wins under ORDER BY state <> 'cancelled' DESC, id DESC. A test covers each case, and the superseded test would fail on the old code.
Security: the route is a read gated by mayReadRuns (reader plus app entry), and it answers 404 alike for an unreadable repository and a malformed id. The oid must match ^[0-9a-f]{40}$ before the query runs, the SQL is parameterised, and the response is no-store. Main's checks are matched on env = defaultBranch with issue_id IS NULL, which is how checks/service.ts writes them. The run_repo(repo_id, id DESC) index keeps the 5,000-run window cheap. In the workflow, the SHA comes from git and the token comes from the mirror's secret; nothing reaches the shell from user text. The sed that parses state cannot be fooled by the summary, because quotes inside the JSON are escaped. No secret is committed and no dependency is added.
Scope: the owner's #2794 asks for this change to release.yml, so the planner rule against touching the release workflow does not apply. The change has no page, so the terminal has nothing to match. README and ROLL.md document the endpoint and the gate. test/release-gate.test.ts keeps check.yml retired.
Verified here: bun test test/commit-check.test.ts test/release-gate.test.ts passes on the head (11 pass, 0 fail). My sandbox refused connections to the preview host and kept me out of the screenshots, so I could not look at the preview by eye. Nothing on the page changes in this pull.
For the owner, outside this repository: remove check / check from the mirror's required statuses if it is listed there.
A release now has one gate.
release.ymlbuilds only after the forge's own full check has passed on the tagged commit. The build then boots the image it made, so any image in the registry has passed both. The mirror'scheck.yml, the copy that held 0.7.170–0.7.173 on reds only that runner saw, is deleted.What changed
GET /api/v1/repos/:owner/:name/commits/:oid/checkreports the default branch's own check of a full 40-character commit id. It answers{commit, state, runId, summary, finishedAt}, wherestateisok,failed,pendingornone.- Only main's own check counts. A pull's fast-tier check and other branches are ignored, and a check the forge cancelled is skipped. - The newest check decides. Only the repository's 5,000 newest runs are searched, so the lookup stays cheap. - Anyone who may read the repository's runs can read it; anyone else, or a short or malformed id, gets a 404. Responses are not cached. - Code:
src/checks/commit_check.ts,checkOfCommitinsrc/actions/runs.ts, and the route insrc/http/api.ts.release.yml: a new step, before the image build, polls that endpoint for the tagged commit every 30 s for up to 2 h. It builds only onok, and onfailedit stops with a link to the run. It uses the mirror'sFORGE_URLvariable andFORGE_TOKENsecret, whichflake-hunt.ymlalready needs, so nothing new has to be configured.RUN bun src/release/boot.ts(#2652) is the second half of the gate..forgejo/workflows/check.ymlis deleted, and a stale reference to it inflake-hunt.ymlis removed. The nightlyflake-hunt.ymlandoutside-gate.ymlstay; they file issues and gate nothing.docs/ops/ROLL.mdhas a new section, "The gate of record", that says how a release ships until #1432. The README documents the endpoint.Tests
test/commit-check.test.tscovers which checks count, which one decides, and the route's 404s and caching header.test/release-gate.test.tschecks that the wait step comes before the build and onlyokcontinues. It also checks the Dockerfile boots the image, and that no workflow triggered by a push, pull or tag runs the suite. That last test fails withcheck.ymlrestored.scripts/gate.tsand the related suites pass locally (105 tests). The forge's full check has not run on these commits yet.Not done here
src/release/launch.ts(#2652).FORGE_URL, the release waits two hours and then fails rather than shipping. This is untested: the step was only dry-run here, with a stubcurl.check / checkas a required status in its branch protection, that setting has to be removed by hand.To try it:
curl "$FORGE_URL/api/v1/repos/trevato/forge/commits/<full sha on main>/check"🤖 Generated with Claude Code
Closes #2794