`release.py release` gains two flags. They can be used together.
--skip-bundles ships only the claim, the GitHub release, the final tag
and the Docker image. No desktop, Termux or PM bundle job runs. The
final tag records candidateManifestSha256: null. Publication moves only
the Docker stable/latest aliases. The R2 stable head, feeds, APT, the
downloads page, the signed-package baseline and the Store stay on the
previous bundle release.
--skip-tests builds, signs and publishes every artifact and runs no
test job: source CI, Nix, PM bundle check, Termux, Windows live,
install/update E2E, bootstrap identity, native smokes, upgrade
acceptance, tests/docker and the in-build vitest step. The candidate
manifest records each smoke as skipped, never as passed.
The flags live in the claim message (skipBundles, skipTests), next to
autopublish. They are not workflow inputs, so a rerun cannot change
them. admit emits them, and every job condition and gate reads them.
stable.validate_claim and stable.validate_final are now the one shape
check for stable.py and the sequencer.
The gates stay strict. SKIPPED_BY in stable.py maps each job to the
flags that remove it. `gate` requires those jobs to report skipped and
every other gated job to report success. A job that ran although a flag
removes it blocks the release.
A release that skipped bundles never moves the R2 stable head. Two
readers depended on that head:
- The next version was derived from it, so the next cut would reuse the
version. It now takes the newer of the R2 head and the newest
published non-prerelease GitHub release with a vX.Y.Z tag. Bare v*
tags do not count, because those refs are not protected yet.
- The sequencer used it to decide which published releases still need
their publication pass, so a bundle-less release would re-advance
every 15 minutes. The head is now the newer of the R2 head and the
published release whose final tag binds the Docker stable alias
digest.
`release` also refuses a cut when its next version already has a final
tag. That closes the window between the final tag and the public
release, where the published identity still names the old version.
Tests: 42 release test files, 546 passed. Three tests fail on this
Windows host, and they fail the same way on a clean HEAD worktree:
- test_stable_release_graph::test_docker_recovery_refuses_to_replace_a_divergent_version_tag
- test_release_artifacts::test_windows_metadata_is_read_from_package_and_stale_stamp_is_rejected
- test_tag_builds_summary::test_admitted_failure_publishes_tag_info_without_promoting_channel[True]
Not verified: no real Stable Release dispatch ran with either flag, and
actionlint is not installed on this host. The workflow changes are
checked by the graph tests and by running the phase-result step script.
The sequencer and the release entrypoint each carried their own copy of
what makes an attempt outstanding, and discover still read the old
vX.Y.Z-rc claim shape. One pure predicate in versioning now answers it
for both, and the sequencer lists receipt tags, attempt refs, and
abandon markers, filtering each through its parser. An attempt is
burned by its marker, published by its final tag, and green only with
its draft and a succeeded workflow; more than one outstanding attempt
across all versions is refused.
A version is now spent only by publication. derive_next_version reads the
published stable head alone, and release claims the next attempt of that
version as rc.<N>-vX.Y.Z. An abandon marker clears an attempt without
freeing its number.
At most one attempt, of any version, is outstanding: an attempt ref with no
marker and no final tag on the remote. release refuses while one exists and
prints its workflow run, the abandon command, and the rerun command. The
pre-check and the push are not atomic across versions, so release re-reads
the attempts after its push and stops before the draft and the dispatch if a
concurrent cut of another version landed.
A new attempt must descend from the published stable head, read from the
stable channel with its version. Abandoned attempts put no constraint on it.
Fetch refspecs are rc.* and abandoned-rc.*: a refspec may hold one '*', and
the parsers filter what the globs over-match.
An attempt ref is rc.<N>-vX.Y.Z and its abandon marker is
abandoned-rc.<N>-vX.Y.Z. The ref grammar only anchors: version_from_tag
still owns the version shape, so CalVer and canary identities stay out.
The family is the published stable head, then outstanding -rc claims, then
the seed 0.21.4 when both are empty, so the first release is 0.21.5. CalVer
tags are excluded rather than sorted: v2026.9.21 is a valid three-component
version and would win every max(). A +canary identity compares equal to its
stable; the legacy -canary prerelease still sorts before its stable so feeds
published before the migration keep their order.