The release workflow uses only the existing repository contents: write token.
It never force-updates tags, replaces a release, or overwrites release assets.
-
Update
Sources/MoleApp/AppVersion.swiftto the desired SemVer version in the normal development PR. Include the release tooling changes in that PR, but do not add the request file yet. -
Merge the PR. Wait for the Build workflow on the exact final main SHA to pass, including Swift tests, native design captures, contrast checks, and app packaging. Inspect the native screenshot artifact before authorizing release.
-
In a separate main commit, create or update
.github/release-request.json:{ "version": "0.4.2", "source_sha": "REPLACE_WITH_THE_FULL_40_CHARACTER_VETTED_SHA" }The placeholder deliberately fails validation. Substitute the exact lowercase commit SHA from step 2. These are the only permitted keys; duplicate keys, abbreviated SHAs, branch names, and non-SemVer versions are rejected.
-
The request commit triggers Release. It reads the request from the triggering commit, verifies that the source is on main and its version constant matches, then checks out the requested source SHA, not the request commit or a floating branch. It reruns tests and contrast checks and packages that source.
-
The workflow verifies the DMG's drag-install layout (
/Applicationssymlink), the app's version, bundle identity, icon, SwiftPM logo resources, executable mode, embedded Sparkle framework, update settings (SUFeedURL,SUPublicEDKey), Mach-O architecture, and strict ad-hoc signature. It writes a SHA-256 sidecar, then generates an EdDSA-signed Sparkle appcast from the same DMG and verifies that appcast against the packaged bytes. It atomically creates the version tag, uploads the DMG, checksum, and appcast to a new draft release, checks asset names/sizes and tag identity, and finally publishes the release. Notes identify the source revision, architecture, checksum, and ad-hoc / not-notarized status. -
After publication the workflow downloads all three assets from their public GitHub
browser_download_urlwithout an authorization header. It compares their bytes and SHA-256 against the verified originals, checks the downloaded sidecar, and repeats DMG, bundle/resource, Sparkle framework, Mach-O, codesign and appcast-binding verification on the public download. Only transient availability/network failures are retried, up to six attempts. A mismatch fails immediately and never republishes or rolls back. -
The
Qili-release-verificationActions artifact contains the actual public DMG, checksum, appcast, andprovenance.jsonwith the source revision, URLs, observed hashes, architecture, and verification result. Check that artifact and the published tag before reporting publication complete. A post-publication check failure is explicitly recorded asfailed_after_publication; the release remains intact.
The request does not grant permission to skip the main CI and visual review gate. Only a subsequent change to the request path triggers this route again; unrelated main pushes do not publish a release. Do not commit this example as a live request.
- A pushed
v<SemVer>tag must resolve to the triggering commit, be reachable from main, and match that commit's version constant. This path validates the existing tag and never recreates or moves it. - A manual Release run must select
mainand supply both a version and a full vetted source SHA. Floating source refs are refused. It follows the same no-existing-tag rule as a request. - Runs are serialized, and a newer run does not cancel an in-progress release.
Published DMGs double as the Sparkle update payload. The feed lives at
https://github.com/csic21/qili/releases/latest/download/appcast.xml
and every release attaches its own appcast.xml.
- The EdDSA public key is embedded in
scripts/package_app.sh(SPARKLE_PUBLIC_ED_KEY). Replacing it requires shipping a release signed with the old key, per Sparkle's key rotation rules. - The EdDSA private key exists only in the maintainer's login keychain and
as the repository secret
SPARKLE_PRIVATE_KEY. CI passes it togenerate_appcast/sign_updatethrough stdin (--ed-key-file -); it is never written to disk. generate_appcastsilently skips signing when the app's embedded public key does not match the signing key.scripts/generate_appcast.pytherefore re-signs the DMG withsign_updateand fails the release unless the appcast signature and enclosure match the packaged bytes exactly.- Users on releases before 0.5.0 have no Sparkle and must install 0.5.0 (or newer) once by hand; every later version updates itself in-app.
Validation, test, package, and signature failures happen before tag creation. An existing release or tag is an error, even if its version or source looks right (except for the explicitly triggered tag-push path). A racing tag creation fails atomically without changing the winner. Nothing deletes or force-updates refs.
A failure after creating the tag may leave that tag and/or a draft release. Do not blindly rerun the request or delete/reuse the version: inspect the tag, workflow logs, and draft assets first. Recovery requires an explicit maintainer choice; the automated path intentionally refuses to overwrite partial state.
Portable validation tests (no network calls or publishing):
python3 -m unittest discover -s scripts/tests -p 'test_release_*.py'Mach-O and signature checks run on macOS; portable fixture ZIPs do not establish that a production binary is signed or runnable. Public download verification is intended for this public repository; private releases are not treated as publicly verified through an authenticated fallback.
If publication succeeded but post-publication verification failed, leave the tag,
release, and assets intact. The separate Verify Published Release workflow
has only contents: read; its Python client uses anonymous GET requests only.
It never publishes, replaces, deletes, or rolls back a release.
A reviewed .github/release-verification.json pins the original package's
version, vetted source SHA, architecture, and the names, sizes, and SHA-256 hashes
of both uploaded assets. Obtain these values from the successful packaging logs
and original publication metadata, not from an unverified replacement download.
The recovery check verifies source reachability/version, the public tag and
release identity, metadata hashes, then anonymously downloads both public assets
and repeats bundle, resource, architecture, and ad-hoc signature checks. Its
artifact contains the actual downloaded bytes and read-only recovery provenance.
PR and main changes to that request, verifier, or workflow run verification;
they do not activate the publishing workflow.
For v0.4.2, the original Release run published correctly but its final check rejected stale draft-stage asset URLs in the local publication receipt. The publisher now refreshes asset metadata after publishing. The separate recovery request verifies the original v0.4.2 bytes without changing their vetted source or overwriting the public release. A later tooling-only main commit is therefore not a replacement source revision for v0.4.2.