Skip to content

CI: production-grade release pipeline — macOS dmg + draft, checksummed, attested releases (T-057, PL-110/111) - #9

Merged
df49b9cd merged 1 commit into
mainfrom
ci/release-pipeline
Oct 10, 2026
Merged

df49b9cd merged 1 commit into
mainfrom
ci/release-pipeline

Conversation

@df49b9cd

@df49b9cd df49b9cd commented Oct 9, 2026

Copy link
Copy Markdown
Owner

What changed

A pushed v* tag now produces a production-grade draft release instead of a bare, note-less, Linux-only one:

  • Release job (new, tag builds only): gates the tag against the versions in package.json, src-tauri/tauri.conf.json and src-tauri/Cargo.toml (all three must match), collects both platforms' bundles, writes SHA256SUMS, attests every asset's build provenance with Sigstore (actions/attest-build-provenance@v4, verifiable via gh attestation verify <asset>), and opens a draft release with install + GPL §6 source notes plus GitHub's generated changelog. Hyphenated tags (e.g. v0.1.0-rc1) are marked prerelease. Publishing stays a human step. All of the job's scripts were rehearsed locally against the real tree.
  • macOS CI (new): check-macos runs scripts/check.sh on macos-26 plus the keychain round-trip (secrets::tests::macos_keychain_roundtrip) against a throwaway keychain, bounded by a 2-minute watchdog (PL-111); bundle-macos builds the .dmg with secret-gated Apple signing (APPLE_CERTIFICATE + APPLE_CERTIFICATE_PASSWORD; notarization with APPLE_ID + APPLE_PASSWORD + APPLE_TEAM_ID, all-or-none) and ad-hoc signed otherwise, then mounts the dmg and asserts the executable links nothing outside Apple's system libraries and the signature verifies.
  • macOS static tail (PL-110): scripts/build-media.sh on Darwin also builds OpenSSL 3.6.5, FreeType 2.14.3, FriBidi 1.0.17, HarfBuzz 14.6.0, libunibreak 8.0 (checksum-pinned release tarball), libass 0.17.5, lcms2 2.19.1 and uchardet 0.0.8 statically into third_party/prefix with MACOSX_DEPLOYMENT_TARGET=13.0; src-tauri/build.rs links them statically and drops the Homebrew dylib path. Before this, a downloaded dmg only ran where Homebrew had those dylibs installed.
  • Side fix: bundle.macOS.files license texts moved from Contents/ to Contents/Resources/ — codesign treats extension-less files in Contents/ as unsigned code objects and refused to sign the bundle.
  • scripts/notices.py now unions the Linux and macOS crate sets (cargo fetch for platform-specific license files) and lists the static-tail libraries; THIRD_PARTY_NOTICES.md regenerated (545 KB). scripts/outdated.sh audits the eight new upstream tags — all current.

Found along the way

  • The documented keychain-test recipe was silently broken: libtest --exact needs the full module path (secrets::tests::macos_keychain_roundtrip) — the bare name matched zero tests. Fixed in the doc comment and CI.
  • The test's locked-keychain phase pops a real SecurityAgent password dialog on any window-server machine (developer Macs and GitHub runners alike), blocking in mach_msg where tokio's timeout cannot preempt it; one stale dialog wedges every later security call session-wide (stack-sampled). Trimmed the test to the unlocked path — locked-marker classification stays covered by the existing unit test — and filed the async-blocking problem as PL-113.

Verification

  • scripts/check.sh green on macOS with the static tail: 89 Rust tests, clippy -D warnings, tsc, 43/43 vitest, notices current.
  • Real ad-hoc-signed dmg built locally: otool -L lists Apple frameworks and /usr/lib only, codesign --verify --deep --strict green, hdiutil verify green, keychain round-trip green (0.17 s).
  • actionlint clean, cargo fmt clean, scripts/outdated.sh all ok (14 media-engine tags, 8 Actions).

Reviewer notes

  • Linux is untouched: the static tail and bundle-macos are Darwin-only; bundle/check run exactly as before apart from the shellcheck-driven mapfile rewrite of the apt install line.
  • The release job uses permissions: contents: write, id-token: write, attestations: write — needed for draft creation and Sigstore attestations (repo is public, so the public-good Sigstore instance applies).
  • Apple signing secrets are unset for now; the pipeline falls back to ad-hoc signing until you configure APPLE_CERTIFICATE etc.
  • WORKLOG has the T-057 card + dated log entry; PL-110/PL-111 are closed in planned, PL-113 filed.

🤖 Generated with Claude Code

…d, attested releases (T-057, PL-110/111)

A v* tag now publishes a real release. The new release job gates the tag
against the versions in package.json, src-tauri/tauri.conf.json and
src-tauri/Cargo.toml (all three must match), collects both platforms'
bundles, writes SHA256SUMS, attests every asset's build provenance with
Sigstore (actions/attest-build-provenance@v4, verifiable via
`gh attestation verify <asset>`), and opens a DRAFT release with install
+ GPL §6 source notes plus GitHub's generated changelog; hyphenated tags
are marked prerelease. Publishing stays a human step.

macOS builds in CI (new): check-macos runs scripts/check.sh on macos-26
plus the keychain round-trip (PL-111) against a throwaway keychain,
bounded by a 2-min watchdog; bundle-macos builds the .dmg, secret-gated
Apple signing (APPLE_CERTIFICATE + APPLE_CERTIFICATE_PASSWORD; notarization
with APPLE_ID + APPLE_PASSWORD + APPLE_TEAM_ID, all-or-none) and ad-hoc
signed otherwise, then mounts the dmg and asserts the executable links
nothing outside Apple's system libraries and the signature verifies.

PL-110: the macOS static tail — scripts/build-media.sh on Darwin also
builds OpenSSL 3.6.5, FreeType 2.14.3, FriBidi 1.0.17, HarfBuzz 14.6.0,
libunibreak 8.0 (checksum-pinned tarball), libass 0.17.5, lcms2 2.19.1
and uchardet 0.0.8 into third_party/prefix, MACOSX_DEPLOYMENT_TARGET=13.0;
build.rs links them statically and drops the Homebrew dylib path —
otool -L on the bundled executable lists Apple frameworks and /usr/lib
only (verified on a real ad-hoc-signed dmg; codesign/hdiutil green).
Linux is untouched. bundle.macOS.files license texts moved from
Contents/ to Contents/Resources/ — codesign refuses extension-less
files in Contents/ as unsigned code objects.

Along the way: the macos_keychain_roundtrip recipe was wrong (libtest
--exact needs the full module path — the old doc silently matched zero
tests); its locked-keychain phase pops a SecurityAgent dialog on any
window-server machine that tokio's timeout cannot preempt (security(1)
blocks in mach_msg) — trimmed to the unlocked path with the locked-marker
classification covered by the existing unit test; the async-blocking
problem is filed as PL-113. notices.py now unions the Linux and macOS
crate sets (cargo fetch for platform-specific license files) and lists
the static-tail libraries; THIRD_PARTY_NOTICES.md regenerated (545 KB).
outdated.sh audits the eight new upstream tags — all current.

Gate: check.sh green on macOS (89 Rust tests, 43 vitest, clippy, tsc,
notices), keychain round-trip green (0.17 s), actionlint clean,
cargo fmt clean, outdated.sh all ok.

Co-authored-by: Claude <noreply@anthropic.com>
@df49b9cd
df49b9cd merged commit 59fbd47 into main Oct 10, 2026
5 checks passed
df49b9cd added a commit that referenced this pull request Nov 2, 2026
Conflicts:
- .github/workflows/ci.yml: kept main's tag-only `release` job (version
  gate, SHA256SUMS, Sigstore attestation, draft release). The `flatpak`
  job no longer publishes on its own; it uploads its bundle as the
  `testpattern-flatpak-x86_64` artifact, which `release` now needs,
  checksums and attaches. Release notes gain a Flatpak install line.
- WORKLOG.md: kept both sides. Main took T-057 for the release pipeline,
  so the Flatpak work is renumbered T-059 (T-058 is the Chromecast work
  in progress) here, in the manifest, scripts/flatpak.sh and ci.yml.

Checked: scripts/check.sh green on the merged tree (81 Rust tests,
clippy, tsc, 43 vitest, notices), actionlint clean, flatpak.sh pins ok.

Co-authored-by: Claude <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