ci: mint beta images from a beta branch, and derive their version from git #15

Merged
jlxq0 merged 1 commit from feat-beta-loop into main 2026-08-26 07:48:22 +00:00
Owner

Closes #13.

Where the prerelease version comes from, and why not Cargo.toml

bin/ci-version derives it from git: newest stable tag, bumped by the
conventional-commit range since that tag, plus a UTC timestamp.

Cargo.toml is not read on the beta path, on purpose. Making it carry the
next version between releases would create a second source for a number git
already knows, and two sources for one fact disagree silently, because nothing
compares them. lenno_web's bin/ci-version — the house reference, and the
loop the platform's Renovate rule is written against — never reads mix.exs
either.

There are three version surfaces here and it is worth being explicit about
which is which, because they will not always agree and that is correct:

surface value source
image tag and org.opencontainers.image.version the artefact version bin/ci-version, from git
/health version what the source says it is CARGO_PKG_VERSION
/health revision which artefact this is, exactly GITHUB_SHA

revision is the field to read when the other two disagree, which is why
/health is left alone rather than taught to report the derived version.

On a release tag Cargo.toml is read, and the guard that the tag agrees with
it moves out of the workflow and into the script. /health reports
CARGO_PKG_VERSION, so a release tag naming a different number is a lie the
image cannot correct.

The base is bumped before the suffix is added

0.1.2-beta.<ts> sorts below the released 0.1.2, so beta would read as
older than production while running newer code. That is the cost I took
knowingly when cutting v0.1.2-beta.20260826052605 by hand, and this removes it:
the next beta off this loop is 0.1.3-beta.<ts>.

The suffix is digits, and that is not cosmetic

The platform's clusters/fondue/*-beta/** rule takes -(beta|alpha)\.\d+$. A
sha suffix parses as valid semver, is accepted by everything in this repo, and
is then unmatchable by the rule that maintains the deployment pin — no bump PR
is ever opened and beta silently stops moving. That is exactly how
caldav-mcp-beta ran a pre-security-fix image for four days while reading
Synced and Healthy.

Buildcache

Beta imports and does not export. #8 made main the only writer after two
concurrent exporters failed a run whose image had already been pushed, and
beta and main get pushed within seconds of each other often enough to
reproduce it. A partial cache hit when beta has diverged is the cheaper side.

Verified by mutation

bin/ci-version is shell, so nothing else in this repository checks it.
tests/ci_version.rs builds throwaway git repositories and runs it. Five
mutations, each against the full nine-test file:

mutation died
bump floor patchnone beta_bumps_the_patch_for_an_ordinary_commit, beta_ignores_prerelease_tags_when_choosing_its_base
stable-tag filter removed from the base lookup beta_ignores_prerelease_tags_when_choosing_its_base
suffix timestamp → short sha five tests
Cargo.toml agreement guard removed a_release_tag_must_agree_with_cargo_toml
default suffix format → %Y-%m-%d beta_suffix_is_digits_only, alone

The third row was a finding rather than a pass. beta_suffix_is_digits_only
died alongside four tests that pin exact version strings, which meant it was
carrying no weight of its own: every one of those tests injects
CI_BETA_TIMESTAMP, so none of them can see a change to how the suffix is
actually generated. It now runs the script's own clock, and the fifth mutation
— changing only the default format — kills it and nothing else.

Gates

cargo fmt --check, cargo clippy -D warnings, cargo test --all-features
(103 + 9), cargo audit, cargo deny check bans licenses sources — green
locally on rustc 1.98.0.

After this merges

Create the beta branch from main and push it; the first build off the loop
mints v0.1.3-beta.<ts>, Renovate's existing rule opens the bump, and the check
is the pod's digest rather than the PR.

Closes #13. ## Where the prerelease version comes from, and why not `Cargo.toml` `bin/ci-version` derives it from git: newest stable tag, bumped by the conventional-commit range since that tag, plus a UTC timestamp. **`Cargo.toml` is not read on the beta path, on purpose.** Making it carry the next version between releases would create a second source for a number git already knows, and two sources for one fact disagree silently, because nothing compares them. `lenno_web`'s `bin/ci-version` — the house reference, and the loop the platform's Renovate rule is written against — never reads `mix.exs` either. There are three version surfaces here and it is worth being explicit about which is which, because they will not always agree and that is correct: | surface | value | source | |---|---|---| | image tag and `org.opencontainers.image.version` | the artefact version | `bin/ci-version`, from git | | `/health` `version` | what the source says it is | `CARGO_PKG_VERSION` | | `/health` `revision` | which artefact this is, exactly | `GITHUB_SHA` | `revision` is the field to read when the other two disagree, which is why `/health` is left alone rather than taught to report the derived version. On a release tag `Cargo.toml` *is* read, and the guard that the tag agrees with it moves out of the workflow and into the script. `/health` reports `CARGO_PKG_VERSION`, so a release tag naming a different number is a lie the image cannot correct. ## The base is bumped before the suffix is added `0.1.2-beta.<ts>` sorts *below* the released `0.1.2`, so beta would read as older than production while running newer code. That is the cost I took knowingly when cutting `v0.1.2-beta.20260826052605` by hand, and this removes it: the next beta off this loop is `0.1.3-beta.<ts>`. ## The suffix is digits, and that is not cosmetic The platform's `clusters/fondue/*-beta/**` rule takes `-(beta|alpha)\.\d+$`. A sha suffix parses as valid semver, is accepted by everything in this repo, and is then unmatchable by the rule that maintains the deployment pin — no bump PR is ever opened and beta silently stops moving. That is exactly how `caldav-mcp-beta` ran a pre-security-fix image for four days while reading Synced and Healthy. ## Buildcache Beta imports and does not export. #8 made `main` the only writer after two concurrent exporters failed a run whose image had already been pushed, and `beta` and `main` get pushed within seconds of each other often enough to reproduce it. A partial cache hit when beta has diverged is the cheaper side. ## Verified by mutation `bin/ci-version` is shell, so nothing else in this repository checks it. `tests/ci_version.rs` builds throwaway git repositories and runs it. Five mutations, each against the full nine-test file: | mutation | died | |---|---| | bump floor `patch` → `none` | `beta_bumps_the_patch_for_an_ordinary_commit`, `beta_ignores_prerelease_tags_when_choosing_its_base` | | stable-tag filter removed from the base lookup | `beta_ignores_prerelease_tags_when_choosing_its_base` | | suffix timestamp → short sha | five tests | | `Cargo.toml` agreement guard removed | `a_release_tag_must_agree_with_cargo_toml` | | default suffix format → `%Y-%m-%d` | `beta_suffix_is_digits_only`, alone | The third row was a finding rather than a pass. `beta_suffix_is_digits_only` died alongside four tests that pin exact version strings, which meant it was carrying no weight of its own: every one of those tests injects `CI_BETA_TIMESTAMP`, so none of them can see a change to how the suffix is actually generated. It now runs the script's own clock, and the fifth mutation — changing only the default format — kills it and nothing else. ## Gates `cargo fmt --check`, `cargo clippy -D warnings`, `cargo test --all-features` (103 + 9), `cargo audit`, `cargo deny check bans licenses sources` — green locally on `rustc 1.98.0`. ## After this merges Create the `beta` branch from `main` and push it; the first build off the loop mints `v0.1.3-beta.<ts>`, Renovate's existing rule opens the bump, and the check is the pod's digest rather than the PR.
jlxq0 force-pushed feat-beta-loop from 19e232195c
All checks were successful
CI / cargo (pull_request) Successful in 45s
CI / docker (pull_request) Successful in 58s
to dd2460008a
All checks were successful
CI / cargo (pull_request) Successful in 51s
CI / docker (pull_request) Successful in 1m0s
2026-08-26 07:26:42 +00:00
Compare
jlxq0 merged commit 88d89d2418 into main 2026-08-26 07:48:22 +00:00
jlxq0 deleted branch feat-beta-loop 2026-08-26 07:48:22 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/caldav-mcp!15
No description provided.