`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.
Stable Release Publication ran every 15 minutes (96 runs a day, each
checking out full history, setting up node and buildx, logging into
Docker Hub, and taking the release-signing environment) only because the
sequencer held a failed run for a 15-minute backoff that the failure
event could never satisfy, so the cron was what actually retried.
Drop the backoff: the reconcile pass started by a failed Stable Release
reruns its failed jobs right away. MAX_ATTEMPTS burning, oldest-first
retry ordering, the attempt-entry check, and the needs_retarget repair
stay. The schedule trigger goes; workflow_run and workflow_dispatch
remain the recovery paths.
The shared stable-release concurrency group cannot deadlock: the rerun
waits as pending behind this job, and the sequencer only confirms the
new attempt is queued before it exits and frees the group.
The Partner Center submission API has no call that releases a held
(Manual) submission, and update refuses a committed one, so the release
step's PUT and re-commit could not work. The publication pass now only
reads the submission: a certified one prints a GitHub warning to click
Publish now in Partner Center, one still in certification says what comes
next, a live one is a no-op, and a failed one leaves the run red.
It finds the held submission through pendingApplicationSubmission on the
app, instead of posting a probe submission and scraping an id out of the
409 error. The HTTP runner also sends the request headers it is given:
before, every API call after the token went out without Authorization.
The green run no longer lets the Store go live on its own: stable-store
deletes any in-flight submission, uploads the verified msixbundle as a
draft (msstore publish --noCommit), sets targetPublishMode to Manual via
msstore submission get/update, and commits with msstore submission
publish. The publication pass releases it from sequencer production_advance
(after the feeds and Docker aliases move) through the Partner Center
submission REST API with the same MS_STORE_* credentials: it finds the
in-flight submission, and rewrites it with targetPublishMode Immediate and
commits, so a certified (status Release) submission goes live now and one
still in certification goes live when certification passes. Both calls act
on the submission's current state and are safe to rerun; a failed Store
call leaves the run red. The Store product id is optional in the
publication environment, so runs without Store credentials skip the step.
The final tag is the custody receipt of one publication pass: publish
hashes the candidate manifest from the attempt archive (nothing records
the digest earlier), resolves the image digest from the attempt-ref tag,
and binds both with the claim and archive path into v{version}. The
draft is retargeted onto the receipt tag and stripped while it is still
a draft, and only then does a final, separate call make it public —
immutable releases take no edits afterwards. The needs_retarget recovery
reruns exactly that draft edit; a public release can never be repaired.
The stable/latest Docker aliases move onto the attempt's image in the
same pass as the feed pointers, and no bytes are copied to a v-tag path.
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.
The final tag is a publication custody receipt, so complete() only
validates the accepted candidate archive and the draft stays on the
attempt ref. A succeeded run with its draft is green; a missing draft
after success is an error, not an inferred abandonment.