fix(release): point "Latest" at the native installers - #69
Merged
Conversation
`v0.14.0` is GitHub's "Latest" release and carries only `openadapt_desktop-0.14.0-py3-none-any.whl`, its sdist, and two publish attestations. Every installer -- 2x DMG, MSI, NSIS `.exe`, `.deb`, `.AppImage` -- plus `SHA256SUMS`, the per-platform metadata JSON, and the CycloneDX SBOM live on the separate prerelease tag `desktop-v0.14.0`. `/releases/latest` excludes prereleases by definition, so a human landing on the link cited in launch material saw no installer at all. The split is intentional and documented in RELEASES.md: the engine lane stays non-prerelease so "Latest" names the canonical engine release, and the native lane stays prerelease because its installers are ad-hoc-signed or unsigned pending signing credentials. That policy is preserved -- `desktop-v0.14.0` is NOT flipped to non-prerelease. Adds `installer_pointer_notes()` and an `installer-pointer-notes` subcommand to `scripts/native_release.py`, modelled on the existing `superseded_notes()`: it prepends a marker-delimited block naming the matching `desktop-vX.Y.Z` release, rewrites that block in place so republishing is idempotent, and preserves the original notes verbatim. The new `point-engine-release` job in `native-release.yml` applies it. It runs on `release: published` for a non-draft `desktop-v*` prerelease rather than beside the upload step, for the same reason the supersession notice does: `publish-draft` creates a DRAFT, whose tag page 404s publicly, and a pointer must never advertise a URL nobody can open. Publishing can only follow a successful upload, so it is still strictly after the installers exist. `validate` is gated to `github.event_name == 'push'`, which skips the entire build lane on release events through `needs`. A missing engine release fails the job loudly. Chose linking over mirroring the assets onto `vX.Y.Z`. The installer set is 757 MB and the engine lane has cut six releases in three days, so mirroring would duplicate multiple GB per week -- but cost is the weakest argument. The real ones: putting unsigned and ad-hoc-signed binaries on the release GitHub labels "Latest" makes them its default download, which is the maturity overstatement the two-lane split exists to prevent (the prerelease flag is a label; the assets are what a human clicks); it contradicts the documented machine-readable selection rule that identifies installers by the `desktop-v` prefix and labels plain `v*` releases "CLI/engine only", which the openadapt.ai download page follows; supersession automation only edits `desktop-v*` bodies, so a mirrored copy on an old `v*` release would never be marked superseded and would present a stale installer as current forever; and attaching installers to `vX.Y.Z` is explicitly step 1 of the post-signing convergence plan in RELEASES.md, which mirroring now would pre-empt. The live `v0.14.0` notes were edited with this exact generator, so the workflow will replace that block cleanly on the next native publish. `/releases/latest` returns 200 with the pointer, the linked `desktop-v0.14.0` tag returns 200, and its `SHA256SUMS` asset serves. 572 tests pass; ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM
abrichr
added a commit
that referenced
this pull request
Jul 27, 2026
…se (#73) GitHub's /releases/latest excludes prereleases by definition, so it always resolves to the engine release vX.Y.Z -- which carried only a wheel and an sdist. A visitor following the canonical download link, the one cited in launch material, found no installer. PR #69 added a notes pointer; that still made a human read a paragraph and click through to a second tag. Add `mirror-installers-to-engine-release`, which downloads the exact attested asset set from desktop-vX.Y.Z, re-verifies every byte against the attested SHA256SUMS, and uploads it to vX.Y.Z. "Latest" now carries a working installer. The prior policy was "linked, not mirrored", on the reasoning that ~757 MB of ad-hoc-signed and unsigned binaries on "Latest" would overstate their maturity. That concern is addressed directly rather than by withholding the artifact: - desktop-vX.Y.Z STAYS a prerelease. Flipping it is still forbidden. - Every filename encodes its signing state (-adhoc.dmg, -unsigned.msi). - The pointer block now leads with "Beta ... ad-hoc-signed (macOS) or unsigned (Windows, Linux)", says the OS will warn, and gives the verification commands before anything else. - The mirror re-verifies against the attested manifest, so an engine release can never carry a byte that was not attested on the native tag. - Assets only: the engine release never receives the `<!-- installer-release -->` marker, so the documented machine-readable selection rule is unchanged and download-page consumers keep resolving desktop-v*. Cost: ~757 MB duplicated per release. GitHub release storage and bandwidth are unbilled for public repositories, so the recurring cost is this one job's download+upload minutes, not storage spend. Tests updated in the same change: the pointer-block test pinned "this release has no installer", which is now false, and the workflow test asserted that no installer bytes are uploaded to vX.Y.Z, which is now the opposite of the intent. Both are rewritten to pin the three invariants that keep mirroring safe. Claude-Session: https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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.
Problem
v0.14.0is GitHub's Latest release. It carries only the wheel, the sdist, and two publish attestations. Every installer lives on the separate prerelease tagdesktop-v0.14.0:prereleasev0.14.0(Latest)falsedesktop-v0.14.0true.exe,.deb,.AppImage, 4× metadata JSON, CycloneDX SBOM,SHA256SUMS— 757 MB/releases/latestexcludes prereleases by definition, socurl -sSL .../releases/latestresolves to.../releases/tag/v0.14.0— no installer. That is the link cited in launch material.(Minor correction to the brief: there are 6 installer binaries, not 8 — 12 assets total including metadata, SBOM, and
SHA256SUMS.)What this does not do
desktop-v0.14.0is not flipped to non-prerelease. That would make unsigned and ad-hoc-signed binaries the repository's "Latest" release and reverse the deliberate, documented policy inRELEASES.md.Immediate fix (already applied)
The existing
v0.14.0body was captured first (gh release view v0.14.0 --json body), then re-published with a pointer block prepended — the original notes are preserved verbatim. Generated with the same code this PR adds, so the workflow will replace it cleanly on the next native publish.Verified live:
Durable fix
scripts/native_release.py:installer_pointer_notes()+ aninstaller-pointer-notessubcommand, modelled on the existingsuperseded_notes(). Marker-delimited (<!-- openadapt-installer-pointer:start/end -->), rewritten in place, so republishing is idempotent and pointers never accumulate. ReturnsNonewhen already correct..github/workflows/native-release.yml: newpoint-engine-releasejob.Why it runs on
release: publishedrather than beside the upload step.publish-draftcreates a draft, whose tag page 404s for the public. A pointer written there would advertise a URL nobody can open until a maintainer un-drafts it — the same hazard the supersession notice already handles ("the notice always points at a publicly available replacement"). Publishing can only follow a successful upload, so it is still strictly after the installers exist.validateis gated togithub.event_name == 'push'; because the whole build lane reaches it throughneeds, that one guard skipsbuild-*,attest, andpublish-drafton release events.The engine release is guaranteed to exist —
native-freshness.ymlrefuses to pushdesktop-vX.Y.Zuntilrefs/tags/vX.Y.Zexists and is an ancestor ofmain— so a missing one fails the job loudly rather than silently leaving "Latest" without a link.Pointer vs. mirror — and why pointer
Duplication cost is the weakest argument, though it is real: 757 MB × six engine releases in three days is multiple GB/week uploaded from a human-gated job. The decisive reasons are:
RELEASES.mdreserves "attach installer assets to the canonicalvX.Y.Z" for step 1 after signing lands.desktop-vprefix, and plainv*releases are to be labelled "CLI/engine only". The openadapt.ai download page follows that rule.desktop-v*bodies. A mirrored copy on an oldv*release would never be marked superseded and would present a stale installer as current forever.A pointer fixes the actual broken path — a human on
/releases/latestseeing no installer — at zero duplication, zero provenance dilution, and zero policy reversal.Also updated
RELEASES.mdgains a "The Latest installer pointer" section documenting the block, the publish-time trigger, and the link-not-mirror rationale.Verification
uv run pytest tests/— 572 passed, 1 skipped. 4 new tests: block/preservation, idempotency + retarget, malformed-input refusal, and a workflow-contract test asserting the job exists, triggers onpublished, checksdraft, and never uploads assets to${engine_tag}.uv run ruff check engine/ tests/ scripts/— clean.if:conditions verified.Separately reported (not fixed here)
RELEASES.mdclaims every engine release gets a matching native prerelease. Tagsdesktop-v0.12.0,desktop-v0.8.0,desktop-v0.7.0, anddesktop-v0.1.0exist with no release object at all. Root causes differ (build failure, approval-gate cancellation, run cancellation, and atomllibfailure on Python < 3.11) but share one shape: nothing alerts when the human-gatedpublish-draftjob is skipped, cancelled, or fails, so the invariant broke silently four times.🤖 Generated with Claude Code
https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM