|
cpp-toolchain documentation
Up-to-date C++ toolchain images for the complete development cycle - GNU and LLVM side by side
|
The operator's runbook: consumers should read Tags & versioning instead.
This page is about cutting releases, and it is the only place the cadence and the procedures are documented/stated.
An rc is built, a release is that same rc re-tagged. Merging the candidate pull request is what promotes it:
graph LR
main["main"] -->|"cron, or gh workflow run"| build["fresh build<br>~15-25 min"]
build --> rc["pre-release v1.2-rc.1<br>every stage pushed"]
rc --> pr["candidate PR<br>adds releases/v1.2.yaml"]
pr -->|"you merge"| promote["re-tag the recorded digests<br>~90 s, no rebuild"]
promote --> release["release v1.2<br>+ latest"]
The whole cycle, on demand:
Substitute the rc the dispatch actually minted: you do not choose the number. It is the newest release tag with its minor bumped, so with v1.1 released the dispatch cuts v1.2-rc.1, and a second dispatch cuts v1.2-rc.2 and closes the first candidate as superseded.
Same rc, same digests - only the record is retitled, between steps 2 and 3 above:
Merging then re-tags the rc's digests to v2.0 + latest. The schema sanctions exactly this one mismatch between version and candidate - a target ending in .0 - and bumps survives the rename untouched, because it is recomputed against the newest release either way. The other route to a major, when no rc was built at the commit you want, is in Cutting a major.
| Not available | Why |
|---|---|
| Choose the rc number, or cut an rc for a given version | Computed from the newest release tag - see above |
| Cut a minor by hand as a GitHub release | Refused: it would rebuild instead of promoting, shipping an artifact nobody tested |
| Release a commit that is not on main | Both the build and the promotion assert containment in main - no cherry-pick channel |
| Move latest faster than one build | The byte-identical guarantee costs one rc build, always |
| Channel | Cut by | Moves latest | How |
|---|---|---|---|
| rc (v1.2-rc.1) | schedule - 8th and 22nd, 4am UTC (or manual dispatch) | ❌ | fresh build from main, published as a GitHub pre-release; skipped when nothing image-relevant changed |
| minor (v1.2) | you, by merging the candidate PR | ✅ | the digests recorded in releases/v1.2.yaml are re-tagged - no rebuild |
| major (v2.0) | you, by cutting a vX.0 GitHub release | ✅ | fresh build (the image contract changed - only a human decides that) |
The promotion guarantee, stated precisely:
Every moving part lives in two workflows, one schema and one configuration file:
Deliberately not a fourth channel - it is the normal path with the cron replaced by a dispatch:
Two constraints, both enforced and neither obvious:
git revert of the promotion commit does not move image tags back on its own - the record is gone from main, but latest still points at the reverted release.
What actually rolls back: re-run the previous version's promotion workflow run from the Actions tab. Promotion is idempotent and reads its own releases/v*.yaml, so the old run re-tags latest back to the old digests in seconds. The digests being in git is what makes this archaeology-free.
graph LR
q{"what are you<br>cutting?"} -->|"a minor"| a["merge the candidate PR as-is"]
q -->|"a major, and an rc<br>you validated exists"| b["retitle its record to v2.0, then merge<br>re-tag, no rebuild"]
q -->|"a major, at a commit<br>no rc was built at"| c["cut a v2.0 GitHub release by hand<br>fresh build, then a records-only PR"]
Retitle the open candidate PR instead of merging it as a minor
Promotion reads the YAML, so the same digests are re-tagged to v2.0.
Cut a v2.0 GitHub release by hand.
It builds fresh (~15-25 min), publishes, then opens a records-only PR adding releases/v2.0.yaml - merge it so releases/ stays the complete digest record (rollback depends on it).
Merging that PR re-fires the promote job as an idempotent no-op.
So the guarantees are not read as stronger than they are:
| Failure | State left behind | Recovery |
|---|---|---|
| Expired DOCKERHUB_TOKEN during an rc build | Nothing published anywhere - login precedes every push. No tag, no PR. | Rotate the secret, re-run. The next run recomputes the same rc number. |
| Expired DOCKERHUB_TOKEN during a promotion | The merge already landed: main records a release the registries do not have. | Rotate, then re-run the failed run - every step is idempotent (same-digest re-tags are no-ops, tag and release are upserts). |
| Digest mismatch at promotion | Nothing mutated - the assert runs before any re-tag. | The rc moved since it was built (a re-run, or tampering). Do not bypass the check: cut a fresh rc, validate that, promote it. |
| "rc is superseded" at promotion | Nothing mutated. | Only the newest rc of a minor is promotable. Promote the newer candidate; if it regressed, revert on main and cut a fresh rc. Reachable only via a hand-crafted record - the candidate PR machinery closes superseded PRs. |
| Expired RELEASE_PR_TOKEN | The rc's images and pre-release are published, but the candidate PR was never opened. | Rotate the secret, then gh workflow run docker-publish.yml for a fresh rc + PR. Do not just re-run the failed job: the skip check now sees the fresh rc tag and exits green without opening anything. |
| Candidate PR appears with no checks | Wrong token created it (GITHUB_TOKEN PRs fire no pull_request events). | Close and reopen the PR by hand - the reopen is human-caused, so the checks fire. |
| rc cron fails outright | No pre-release and no PR, which on its own is indistinguishable from the skip case - so the run opens release: the image build is failing, rewritten on each run and closed by the first green one. | Read the issue: it names the commit, the intended tag and the run. Fix the cause, then gh run rerun <id> --failed. The build already retries with backoff, so a single upstream blip does not reach here. Nothing warns before a secret expires, so this issue is the signal. |