fix(ci): refuse a release tag that is not the version Cargo.toml declares - #304
Merged
Merged
Conversation
…ares
The image tag comes from the git ref (docker/metadata-action's
type=ref,event=tag) while the binary inside the image reports the version
Cargo.toml declares (routes/mod.rs /healthz -> env!("CARGO_PKG_VERSION")).
Nothing tied the two together: pushing a tag that does not match the
manifest (or back-filling a tag for an old release) published an image
under a version its own binary does not report - and README teaches
ghcr.io/<repo>:latest as the public pull path.
Add a step that runs before any build work and only on tag refs
(workflow_dispatch's GITHUB_REF_NAME is a branch name, so the check is
conditioned on refs/tags/). The expected value is read from Cargo.toml;
no copy of the version is added to the workflow and no third-party action
is introduced. deploy_gate.rs guards the copies in docker-compose.yml /
Dockerfile; this guards the release chain's input end.
This was referenced Sep 27, 2026
Merged
Merged
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
The published image's tag and its contents state the same fact — the release version — and nothing tied them together.
docker-publish.ymlfeedstype=ref,event=tagtodocker/metadata-action./healthzwithenv!("CARGO_PKG_VERSION")(src/routes/mod.rs:187).grep -rn "ref_name\|GITHUB_REF\|github.ref" .github/returned nothing, so pushing a tag that does not matchCargo.toml(or back-filling a tag for an older release) published an image under a version its own binary does not report. That is a public surface: both READMEs teachdocker pull ghcr.io/argszero/aitokenpool:latest(lines 39/42), and the tag is exactly what a release is supposed to pin.This adds one step to the publish job that refuses such a tag before any build work starts — it fails closed, so it prefers a red run over a mislabelled image. The check is conditioned on
startsWith(github.ref, 'refs/tags/'), becauseworkflow_dispatch'sGITHUB_REF_NAMEis a branch name and comparing it there would kill manual re-publishes.The expected value is read from
Cargo.tomlitself: no copy of the version is added to the workflow and no third-party action is introduced.src/deploy_gate.rs(R78) already guards the copies indocker-compose.yml/Dockerfile; this guards the release chain's input end, which that gate cannot see (itsFILESset does not include workflows).No drift has happened yet — the last 10 tags each match the manifest at their commit (
v0.7.19…v0.7.28) — so this is a latent gap, not an incident. It is the same shape as the declared MSRV that had no enforcer (#302): a declaration with a consumer and nothing checking it.Related Issue
(no linked issue — not filed as one because it is a one-line-shaped fix with no ambiguity about the intended behaviour; say the word and I will open one and rebase the reference in)
Changes
docker-publish.yml: new stepRefuse a tag that is not the version Cargo.toml declares, placed afteractions/checkoutand beforeSet up Docker Buildx, i.e. before anything is built, logged in to, or pushed.Tests
cargo test— 399 passed / 0 failed (unchanged from the baseline; no Rust source is touched)cargo fmt --check— rc=0cargo clippy --all-targets -- -D warnings— rc=0ci.yml's MSRV assertion has no test either). The replacement evidence is the honest boundary below.What was measured, and what could not be
This step is reachable only on a tag ref, which is exactly why it cannot be exercised by the very PR that adds it — the
Docker Publishworkflow does not run onmainpushes or pull requests (the comment at the top of that file says so deliberately, from rant 2026-08-22T07:14:15). So the evidence is split, and I would rather state the split than imply a green check proves it:Measured locally, on the exact bytes that ship. The
run:block was extracted from the workflow file itself (YAML-parsed, not re-typed) and executed under the runner's shell flags (bash -e -o pipefail), against the realCargo.toml:GITHUB_REF_NAMEv0.7.28(matches)Cargo.toml version = 0.7.28; release tag = v0.7.28v0.7.29::error::— release tag does not matchlatest::error::::error::— fails closed rather than passing on a missing variablev0.7.28-rc1::error::A negative control (a copy of
Cargo.tomlwhose[package] versionwas changed to9.9.9) shows the extractor follows the manifest rather than a cached value: the same block then acceptsv9.9.9and rejectsv0.7.28. The extractor's output agrees with two independent readers (grep -m1 '^version'and a section-scoped parse) on the real file.The version is read with a single-process
awkdeliberately: an earlier draft usedsed … | head -n1, and under the runner's-o pipefailaSIGPIPErace on that pipe would have failed the release, not the check.Not measured / residual risk, stated plainly:
if:evaluation andGITHUB_REF_NAMEfor a real tag push are not exercised here (I did not push a tag — that would publish a real image). The condition is the standardstartsWith(github.ref, 'refs/tags/'), and a dispatch on a branch ref evaluates tofalse⇒ step skipped, so manual re-publish keeps working.docker buildhere; this machine has no running Docker daemon). What the step does is fail before that point, so the paths it can affect are only "stop" or "continue"."v"against the tag and fails — fail-closed by construction, not silently passing.I am happy to prove it live instead: if you push (or let me push) a tag that intentionally mismatches in a scratch fork, the run should fail at this step. I did not do that against this repository because a tag push here also publishes to GHCR and moves
latest.Checklist
fix/…)fix(ci): …), authorargszero <argszero@gmail.com>