Release Policy
August 20, 2026 · View on GitHub
This repository publishes from two lanes while the native channel remains Beta. This document is the source of truth for what each lane produces, which release to download, and how the lanes converge.
Active release hold: the current Flow pin is not releasable
main pins openadapt-flow[browser,console]==1.31.0. That pin is an interim
source pin so the tree builds and tests against the newest published Flow. It
is not a releasable pin: the immutable 1.31.0 wheel on PyPI predates the
openadapt.push-result/v1 contract that Desktop's governed push requires, so
a release built on it can only fail closed on every hosted handoff.
Do not tag desktop-v*, dispatch the engine release workflow, or publish any
installer while this pin is in force. The hold clears only when all of the
following are true, in order:
- The next
openadapt-flowsemantic release ships the reviewed push-result contract and is published to PyPI. - The exact managed Cloud runtime candidate is reviewed, deployed, and acknowledged.
- Desktop updates
pyproject.toml,uv.lock, the frozen-sidecar inventory, and its qualification evidence from that accepted release. - This section is deleted in the same pull request that lands the new pin.
This is a distribution hold, not a claim of production acceptance. Neither lane carries a signed qualification-admission record today, so no release note, manifest, or installer may state or imply production acceptance.
The two lanes
| Lane | Tag | Trigger | Marked as | Assets |
|---|---|---|---|---|
| Engine (Python package) | vX.Y.Z | Explicit Release and PyPI Publish dispatch from reviewed, green main | Regular release ("Latest") | Wheel, sdist, an attested engine-release provenance receipt, PyPI publish attestations, and a mirrored copy of the matching desktop-vX.Y.Z installer set |
| Native installers | desktop-vX.Y.Z | Explicit Native Installer Release dispatch from reviewed, green main | Published prerelease | Beta installers, platform metadata, SBOM, website manifest, signed build provenance, and SHA256SUMS |
| Stable native channel | desktop-channel | Final promotion step in the same native dispatch | Published prerelease authority | The attested, strictly monotonic openadapt-desktop-channel.json descriptor |
The engine lane stays non-prerelease so GitHub's "Latest" pointer always names the canonical engine release. The native lane stays prerelease because its installer surface is Beta. The native release workflow now requires Developer ID plus notarization on macOS, Authenticode on Windows, and GitHub OIDC attestation over the exact Linux DEB and AppImage bytes; see docs/BETA_NATIVE_INSTALLERS.md for the verification scope and signing states.
Which release should I download?
- Python package / CLI: install from PyPI (
pip install openadapt-desktop) or take the wheel from the newestvX.Y.Zrelease. - Native installers (Beta): use the
vX.Y.Zengine release selected by the attestedopenadapt-desktop-verified-release.jsonchannel index. The index binds the matchingdesktop-vX.Y.Zsource release and the identical mirrored bytes. AuthenticateSHA256SUMS, then verify its exact inventory. Do not use mutable release notes as a release-selection authority.
The "Latest" installer path
GitHub's /releases/latest excludes prereleases by definition, so it always
resolves to an engine release. That link is the one cited in launch material, so
it must not dead-end.
Two mechanisms keep it working. Both run after publication in the same
reviewed-main native-release.yml transaction:
mirror-installers-to-engine-releasecopies the exact attested asset set fromdesktop-vX.Y.ZontovX.Y.Z. Before any write, it verifies the exact asset inventory against the signed GitHub attestation, workflow, source commit, and run attempt./releases/latesttherefore carries a verified installer, not just a link.point-engine-releaseprepends a marker-delimited pointer block at the top of the engine release notes:
<!-- openadapt-installer-pointer:start -->
...
<!-- openadapt-installer-pointer:end -->
The publish job creates the immutable tag and public prerelease once. It does not create a draft, overwrite an existing release, or replace published native bytes. The pointer block is rewritten in place, so pointers never accumulate. If the matching engine release or its attested receipt is missing, the transaction fails before the platform builds start.
Why mirroring does not promote the Beta channel
The earlier policy here was "linked, not mirrored", on the reasoning that
putting ~757 MB of Beta binaries on the release GitHub labels "Latest" would
overstate their maturity. A notes-only link was not
enough: /releases/latest still showed a visitor nothing but a wheel and an
sdist, and that link is what launch material points at. The maturity concern is
addressed directly instead of by withholding the artifact:
desktop-vX.Y.Zstays a prerelease. Flipping it to non-prerelease would make the native lane GitHub's "Latest" outright, and that remains forbidden.- Every filename encodes its trust state —
…-developer-id-notarized.dmg,…-authenticode.msi, or…-github-attested.AppImage. - The pointer block leads with the required platform trust contracts and gives
the
sha256sum -candgh attestation verifycommands. - The mirror job verifies every downloaded byte against
SHA256SUMS. It then requires the signed subject set to equal that complete inventory. It also checks the exact GitHub-hosted reviewed-main workflow, source commit, and run attempt before upload. - The engine release gets assets only. It never receives the
<!-- installer-release -->marker, so the machine-readable selection rule below is unchanged and download-page consumers keep resolvingdesktop-v*.
desktop-vX.Y.Z therefore remains the canonical installer release — build
provenance, attestations, and supersession notices are bound to it — and
vX.Y.Z carries a byte-identical convenience copy.
Freshness automation
The native lane previously lagged the engine lane because desktop-v* tags
were pushed by hand. Three workflows now keep it fresh:
- Release and PyPI Publish (
.github/workflows/release.yml): a maintainer explicitly dispatches one semantic release after the intended release train has landed. It refuses to publish until the exact main-contained commit has successfulTestandBuild artifactspush workflows. It writes and attestsopenadapt-desktop-engine-release-provenance.jsonafter the release commit, tag, public release, wheel, and sdist exist. The recovery operation can rebuild and publish a pre-existing, main-contained ref after checking that ref's exact CI; ordinary merges never publish packages. - Native Installer Freshness (
.github/workflows/native-freshness.yml): when that engine release is published, it verifies the stable engine release and opens a pull request with the exact deterministic transform ofpackage.json,package-lock.json,src-tauri/Cargo.toml,src-tauri/Cargo.lock, andsrc-tauri/tauri.conf.json. It never writes tomain, create a native tag, or publish a release. A manual backfill uses the same pull-request path. - Native Installer Release (
.github/workflows/native-release.yml): a maintainer dispatches the workflow from reviewedmainwith the exact version. The workflow verifies current main, the five-file version transform, the stable engine release, and its attested receipt before it starts the fail-closed signing preflight. The same transaction builds, smoke-tests, attests, publishes, mirrors, writes the verified index, promotes the monotonic channel, updates the pointer, and marks older prereleases superseded. Only this workflow createsdesktop-vX.Y.Z.
When the external controls below are active, each engine release vX.Y.Z can
get a matching native prerelease desktop-vX.Y.Z from the same reviewed source.
External activation requirements
The workflow code is not the complete trust boundary. At the time of this
document update, the repository has no main or release-tag ruleset, and the
native signing identities are not configured. The historical desktop-v0.15.0
prerelease keeps its original ad-hoc and unsigned labels. Do not describe it as
a trusted release.
Before the next native tag or release:
- Add a no-bypass pull-request ruleset for
main, with the required exact-head checks. Require branches to be up to date before merge. Require theReject a stale native version pull requestcheck fornative-version/v*pull requests. - Add an immutable release-tag ruleset for
v*. Only the engine release identity can create a tag. Do not permit a tag update or deletion. - Add an immutable release-tag ruleset for
desktop-v*. Only the explicitly dispatched native release identity can create a tag. Do not permit a tag update or deletion. - Configure Apple Developer ID plus notarization and one Windows Authenticode
identity in the reviewed
native-releaseenvironment. - Permit the reviewed
native-releaseenvironment onmain, with no admin bypass. Publish only after its approval. Then verify the public assets, attestation, pointer, mirror, channel, and supersession result.
The native-release environment reviewer is an additional publish boundary.
It does not replace the main and tag rulesets.
Supersession
After the verified index and channel are published, the same transaction edits
every lower marked desktop-v* prerelease to carry a prominent "Superseded by
desktop-vX.Y.Z — do not use"
notice at the top of its notes (machine marker:
<!-- openadapt-superseded-by: desktop-vX.Y.Z -->). CI never
deletes releases or assets; superseded assets are retained for provenance and
any deletion is a human decision.
Machine-readable selection rule (download pages)
Consumers must not select a release from mutable release notes. Fetch the
attested openadapt-desktop-channel.json asset from the desktop-channel
release. It binds the selected openadapt-desktop-verified-release.json on the
matching engine release. The channel must strictly advance from its prior
descriptor.
- Fetch and attest the channel against
.github/workflows/native-release.yml@refs/heads/main. - Follow its hash-bound verified-index URL.
- Verify the index attestation against
.github/workflows/native-release.yml@refs/heads/main. - Require the closed index schema and the expected repository.
- Require a version that does not decrease from the last accepted index.
- Use only the engine release, native tag, checksum digest, and asset names in the verified index.
- Download all named files into an empty directory.
- Authenticate
SHA256SUMSagainst.github/workflows/native-release.yml@refs/heads/main. - Run
sha256sum -c SHA256SUMSandverify-openadapt-native-release.py. The helper refuses an incomplete or expanded directory.
The release-note markers remain useful for human notices and historical supersession. They are not a machine trust boundary.
openadapt-desktop-release-manifest.jsonis the website-readable index. It lists each artifact name, platform, architecture, signing state, and SHA-256, plus the checked CycloneDX SBOM. The release workflow recomputes the named hashes and validates the complete file set againstSHA256SUMSbefore it creates the GitHub attestation. The signed subject set must equal the complete checksum inventory. The verification path also binds the workflow, source commit, run ID, run attempt, and protected publish job. A consumer must still verify the checksums and attestation;github-attestedis an exact-byte Linux trust label and is not a native-signing claim.
Convergence plan
The next native release cannot publish until Apple Developer ID plus notarization and Windows Authenticode are configured and the Linux exact-byte attestations verify. After the first complete trusted release proves the full channel, the repository can simplify the two upload targets:
- The native build workflow attaches its attested installer assets to the
canonical
vX.Y.Zengine release instead of also creating a separatedesktop-v*prerelease. (Step 1 is already half-done:vX.Y.Zcarries the assets today viamirror-installers-to-engine-release. What remains is retiring the second upload target, not adding the first.) - The
desktop-v*prerelease lane retires; existingdesktop-v*prereleases remain as historical, superseded records. - The
<!-- installer-release -->marker moves tovX.Y.Zwith the assets, and the pointer/mirror jobs retire with the lane.
Until then, the freshness automation above keeps the two lanes at the same version.