Skip to content

Release pipeline: signed macOS DMG, Windows installer, Linux tarball - #6

Open
pob31 wants to merge 2 commits into
mainfrom
feat/release-ci
Open

Release pipeline: signed macOS DMG, Windows installer, Linux tarball#6
pob31 wants to merge 2 commits into
mainfrom
feat/release-ci

Conversation

@pob31

@pob31 pob31 commented Aug 18, 2026

Copy link
Copy Markdown
Owner

Sets up WP14's packaging half (decision record D55). Nothing here cuts a release — it makes one possible, and adds a dry-run path so the pipeline can be proven while the app is still unfinished.

What a tagged release produces

OS Asset Signed?
macOS XOA-v<version>-macos-arm64.dmg Yes — Developer ID, hardened runtime, notarized + stapled
Windows XOA-<tag>-windows-x64-Setup.exe No (no Windows cert)
Linux XOA-<tag>-linux-x86_64.tar.gz n/a — carries install.sh / uninstall.sh

Packaging logic lives in scripts, not in the YAML, so each lane runs identically on a dev machine: tools/ci/build-macos-release.sh, tools/linux/build-app-tarball.sh, Installer/XOA-Installer.iss.

The bug this shook out

The first local signing run failed:

XOA.app: code object is not signed at all
In subcomponent: .../Contents/MacOS/Resources/SOFA/D1_48K_24bit_256tap_FIR_SOFA.sofa

Contents/MacOS is the directory codesign treats as code, so every staged data file there fails --verify --strict and the notary rejects the build.

Chasing it surfaced a second, pre-existing bug: LocalizationManager::getResourceDirectory() has always resolved the macOS resource dir as currentApplicationFile/Contents/Resources — which nothing populated. The macOS app has been running with no translations. Only XoaMonitoringEngine's dev-tree fallback kept the bundled HRTF set reachable.

Fixed in the first commit; three places now agree (CMake POST_BUILD, LocalizationManager, builtInSofaFile()), and the invariant is recorded in CLAUDE.md because it is easy to drift.

Choices worth reviewing

  • DMG, not the .pkg the devplan originally specified — needs only the Developer ID Application identity, so one signing credential instead of two.
  • arm64 only. ARCHS="arm64;x86_64" still yields a universal DMG on demand.
  • Secrets live in the protected xoa environment, not at repo level. This repo is public; the environment's reviewer rule is the boundary keeping the Developer ID key away from untrusted refs. Consequence: every release pauses for a manual approval.
  • Head-tracker plugin ships on Windows only. macOS/Linux would need WFS-DIY's static WFS_HEADTRACK_BUNDLED_OPENCV build ported into tools/headtrack. The Windows step is continue-on-error and the .iss marks plugin files skipifsourcedoesntexist, so a break degrades the installer rather than blocking the release.
  • udev rules ship ahead of the feature. assets/linux/70-xoa.rules covers Stream Deck+ and SpaceMouse; they only relax device permissions, so they are inert until that support lands and packaging won't need revisiting.

Verified

Locally on macOS: Release arm64 build → hardened-runtime sign (entitlements confirmed, get-task-allow false) → strict codesign verify → signed 17 MB DMG; the signed bundle passes --gui-smoke.

Not verified: notarization itself, and the Windows/Linux lanes. Use Run workflow with dry_run ticked and tag empty to exercise all three (including the notary round-trip and the approval flow) without touching a Release.

Unrelated finding

ctest on a Release build fails tests/XoaRotationTests.cpp:523 — the in-place-vs-separate-buffer rotation aliasing guard, at 1e-15. CI only ever builds Debug, so Release optimization/FP settings appear to let rot::apply diverge between the two call sites. Not touched here; flagged for a separate look.

🤖 Generated with Claude Code

Pierre-Olivier Boulant and others added 2 commits August 18, 2026 20:43
The POST_BUILD steps staged Resources/lang and Resources/SOFA into
$<TARGET_FILE_DIR:XOA>, which inside an .app is Contents/MacOS. That is
wrong twice over.

codesign treats Contents/MacOS as CODE, so `codesign --verify --strict`
fails with "code object is not signed at all" on every staged data file,
and the notary rejects the build. Found while validating the release
signing path locally.

Worse, LocalizationManager has always resolved its macOS resource dir as
currentApplicationFile/Contents/Resources — which nothing populated. The
macOS app has been running with no translations at all; only the dev-tree
fallback in XoaMonitoringEngine kept the bundled HRTF set reachable.

Stage into Contents/Resources on Apple and beside the binary elsewhere,
and make builtInSofaFile() mirror the same split. The staging location is
now an invariant shared by three places, so record it in CLAUDE.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… a tarball

Closes the packaging half of WP14 (decision record D55). A published
GitHub Release — or a manual dispatch against a tag — builds and attaches:

  macOS    XOA-v<version>-macos-arm64.dmg   signed, notarized, stapled
  Windows  XOA-<tag>-windows-x64-Setup.exe  unsigned Inno Setup installer
  Linux    XOA-<tag>-linux-x86_64.tar.gz    install.sh / uninstall.sh

The packaging logic lives in scripts rather than in the workflow YAML, so
every lane runs identically on a developer machine:

  tools/ci/build-macos-release.sh + release-entitlements.plist
  tools/linux/build-app-tarball.sh
  Installer/XOA-Installer.iss

Notable choices, all recorded in D55 and Documentation/XOA-RELEASE.md:

- DMG, not the .pkg the devplan originally called for: drag-to-Applications
  needs only the Developer ID Application identity, so the release carries
  one signing credential instead of two.
- project(XOA VERSION ...) is the single source of truth; a verify-version
  job refuses a tag that disagrees before spending build minutes.
- arm64 only. ARCHS="arm64;x86_64" still yields a universal DMG on demand.
- The signing secrets live in the protected `xoa` environment, not at repo
  level — this repo is public, and the environment's reviewer rule is the
  boundary that keeps the Developer ID key away from untrusted refs. Every
  release therefore pauses for a manual approval.
- A dry_run dispatch input packages every lane into workflow artifacts
  without touching a Release, so the pipeline can be proven before there is
  a v1 to cut. The app is not finished; this is how it gets exercised.
- The head-tracker plugin ships on Windows only. macOS/Linux would need
  WFS-DIY's static WFS_HEADTRACK_BUNDLED_OPENCV build ported into
  tools/headtrack. The Windows step is continue-on-error and the .iss marks
  the plugin files skipifsourcedoesntexist, so a break there degrades the
  installer instead of blocking the release; the job summary says which.
- assets/linux/70-xoa.rules (Stream Deck+ / SpaceMouse hidraw access) ships
  ahead of the controller support itself. The rules only relax device
  permissions, so they are inert until that work lands and the packaging
  will not need revisiting when it does.

tools/ci/set-macos-cert-secret.sh verifies a .p12 really holds a Developer
ID Application certificate AND its private key, then sets
MACOS_CERT_P12_BASE64 and MACOS_CERT_PASSWORD together — a mismatch between
those two only surfaces inside the signing job, minutes into a build.

Validated locally on macOS: Release arm64 build, hardened-runtime sign
(entitlements verified, get-task-allow false), strict codesign verify, DMG
built and signed, and the signed bundle passes --gui-smoke. Notarization
and the Windows/Linux lanes are still unexercised.

Co-Authored-By: Claude Opus 5 (1M context) <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