Skip to content

fix(release): point "Latest" at the native installers - #69

Merged
abrichr merged 1 commit into
mainfrom
fix/latest-release-installer-pointer
Jul 27, 2026
Merged

fix(release): point "Latest" at the native installers#69
abrichr merged 1 commit into
mainfrom
fix/latest-release-installer-pointer

Conversation

@abrichr

@abrichr abrichr commented Jul 27, 2026

Copy link
Copy Markdown
Member

Problem

v0.14.0 is GitHub's Latest release. It carries only the wheel, the sdist, and two publish attestations. Every installer lives on the separate prerelease tag desktop-v0.14.0:

Release prerelease Assets
v0.14.0 (Latest) false wheel, sdist, 2 publish attestations
desktop-v0.14.0 true 2× DMG, MSI, NSIS .exe, .deb, .AppImage, 4× metadata JSON, CycloneDX SBOM, SHA256SUMS757 MB

/releases/latest excludes prereleases by definition, so curl -sSL .../releases/latest resolves 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.0 is 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 in RELEASES.md.

Immediate fix (already applied)

The existing v0.14.0 body 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:

/releases/latest                       -> HTTP 200, resolves to tag/v0.14.0, pointer text present
/releases/tag/desktop-v0.14.0          -> HTTP 200
/releases/download/desktop-v0.14.0/SHA256SUMS -> HTTP 206

Durable fix

  • scripts/native_release.py: installer_pointer_notes() + an installer-pointer-notes subcommand, modelled on the existing superseded_notes(). Marker-delimited (<!-- openadapt-installer-pointer:start/end -->), rewritten in place, so republishing is idempotent and pointers never accumulate. Returns None when already correct.
  • .github/workflows/native-release.yml: new point-engine-release job.

Why it runs on release: published rather than beside the upload step. publish-draft creates 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. validate is gated to github.event_name == 'push'; because the whole build lane reaches it through needs, that one guard skips build-*, attest, and publish-draft on release events.

The engine release is guaranteed to exist — native-freshness.yml refuses to push desktop-vX.Y.Z until refs/tags/vX.Y.Z exists and is an ancestor of main — 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:

  1. It defeats the control it nominally respects. The prerelease flag is a label; the assets are what a human clicks. Putting unsigned/ad-hoc-signed installers on the release labelled "Latest" makes them its default download — precisely the maturity overstatement the two-lane split exists to prevent.
  2. It pre-empts the documented convergence plan. RELEASES.md reserves "attach installer assets to the canonical vX.Y.Z" for step 1 after signing lands.
  3. It breaks the machine-readable selection rule. Installer releases are identified by the desktop-v prefix, and plain v* releases are to be labelled "CLI/engine only". The openadapt.ai download page follows that rule.
  4. Stale-installer hazard. Supersession automation only edits desktop-v* bodies. A mirrored copy on an old v* 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/latest seeing no installer — at zero duplication, zero provenance dilution, and zero policy reversal.

Also updated

RELEASES.md gains 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 on published, checks draft, and never uploads assets to ${engine_tag}.
  • uv run ruff check engine/ tests/ scripts/ — clean.
  • Workflow YAML parses; job graph and if: conditions verified.

Separately reported (not fixed here)

RELEASES.md claims every engine release gets a matching native prerelease. Tags desktop-v0.12.0, desktop-v0.8.0, desktop-v0.7.0, and desktop-v0.1.0 exist with no release object at all. Root causes differ (build failure, approval-gate cancellation, run cancellation, and a tomllib failure on Python < 3.11) but share one shape: nothing alerts when the human-gated publish-draft job is skipped, cancelled, or fails, so the invariant broke silently four times.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NyCHrzA1psrKMFfroYbzaM

`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
abrichr merged commit 3cb7adb into main Jul 27, 2026
15 checks passed
@abrichr
abrichr deleted the fix/latest-release-installer-pointer branch July 27, 2026 15:12
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant