Skip to content

Releasing

HSMM edited this page Nov 5, 2026 · 2 revisions

Releasing

Releases are cut by tagging main. The tag starts .github/workflows/release.yml, which builds the universal DMG, publishes the GitHub Release, signs and updates the Sparkle feed and refreshes the Homebrew cask. The authoritative text is the "Releasing" section of docs/DEVELOPMENT.md, the checklist in README.md, ADR 0010 and ADR 0013.

Version numbers (ADR 0010)

Versions are YY.MINOR.PATCH: YY is the two-digit year of the release line (26 for 2026), MINOR grows with feature releases within the year, PATCH with fixes. The first release is 26.0.0; a release in 2027 starts 27.0.0 even if it would otherwise be a minor bump. Tags carry the bare number.

A release candidate is tagged N.N.N-rc.N, for example 26.0.0-rc.1. release.yml and scripts/make-appcast.sh refuse any other suffix. Any tag with a - becomes a GitHub pre-release and an appcast item on the beta channel.

Three numbers must agree:

Where Value Source
CFBundleShortVersionString the version shown in About and compared with the tag by the release job make-app.sh: git describe --tags --abbrev=0, before the first tag Product.version
CFBundleVersion the build number Sparkle orders updates by make-app.sh: git rev-list --count HEAD
Product.version (Sources/MacroSIPCore/Product.swift) the SIP User-Agent header and the About panel edited by hand before tagging

Tag every release on a later commit than the previous one, a final release after its candidate too. CFBundleVersion is the commit count, Sparkle identifies items by it, and generate_appcast would silently replace the candidate's item if the final were tagged on the same commit. make-appcast.sh --check refuses a build number the feed already has under another version, and the workflow runs that check before anything is published.

Owner's checklist (from README)

  1. Everything green locally: swift build, scripts/test.sh, scripts/selftest-call.sh, scripts/make-dmg.sh; mount the DMG and start the app.

  2. Bump Product.version to the version being released, update docs/ROADMAP.md, commit.

  3. Tag and push:

    git tag 26.0.0-rc.1 && git push origin 26.0.0-rc.1    # candidate
    git tag 26.0.0 && git push origin 26.0.0              # final, on a later commit
  4. Watch release.yml. It builds the DMG, creates the GitHub Release and adds it to the update feed; the same run updates the Homebrew cask when TAP_TOKEN exists.

  5. Check: brew tap hsmm/tap && brew install --cask hsmm/tap/macrosip, launch (right-click, Open the first time); curl -s https://raw.githubusercontent.com/HSMM/MacroSIP/gh-pages/appcast.xml lists the new item; an installed older version finds it through MacroSIP, Check for Updates (a candidate only with "Include release candidates" on).

What release.yml does

The workflow runs only in HSMM/MacroSIP, on tags matching [0-9]*.[0-9]*.[0-9]*, with contents: write. There is no concurrency group on purpose: GitHub keeps one pending run per group and cancels the rest, which would drop a tag; simultaneous releases queue on gh-pages instead.

Job dmg on macos-15 (arm64):

  1. Check the tag: N.N.N or N.N.N-rc.N, and the SPARKLE_PRIVATE_KEY secret must exist (without it installed apps would never see the release).
  2. Build and test as in CI: cached build-pjsip.sh, swift build, scripts/test.sh, scripts/selftest-call.sh natively and under Rosetta 2.
  3. Build the DMG: scripts/make-dmg.sh release; lipo -info on the binary; the job fails if CFBundleShortVersionString differs from the tag. The DMG is uploaded as an artifact for the cask job.
  4. Check the appcast: scripts/publish-appcast.sh --check <tag> dist/MacroSIP.app fetches gh-pages and refuses a build number the feed has under another version.
  5. Create the GitHub Release with gh release create: title = tag, generated notes prefixed with a fixed line saying that macOS asks for microphone access again after an update, --prerelease for tags with a -. A re-run keeps the release and replaces the DMG with --clobber.
  6. Update the appcast: the release body is rendered to HTML with GitHub's Markdown API and scripts/publish-appcast.sh <tag> <dmg> runs scripts/make-appcast.sh, which calls Sparkle's generate_appcast (from .build/artifacts/sparkle/Sparkle/bin) with the EdDSA key from SPARKLE_PRIVATE_KEY, the download URL of the release asset, --embed-release-notes, --channel beta for candidates, and every earlier item kept (--maximum-versions 0, no deltas). The result is committed to the gh-pages branch (created orphan on the first run; the push is retried up to five times after a fresh fetch when another release pushed first) and attached to the release as appcast.xml.

Job homebrew on ubuntu-latest, after dmg: skipped when the TAP_TOKEN secret is empty; otherwise checks out HSMM/homebrew-tap with that token, downloads the DMG artifact and runs scripts/update-cask.sh <tag> <dmg> with TAP_DIR set, which fills packaging/homebrew/macrosip.rb (version, SHA-256) into Casks/macrosip.rb, commits macrosip <tag> and pushes. One-time setup: create the public repository HSMM/homebrew-tap, create a fine-grained personal access token with Contents read and write on that repository only, store it as the Actions secret TAP_TOKEN of HSMM/MacroSIP. The cask declares auto_updates true and depends_on macos: :sonoma; Homebrew accepts 26.0.0-rc.1 as a version.

By hand, without CI: scripts/update-cask.sh <version> dist/MacroSIP-<version>.dmg writes dist/macrosip.rb and prints the git commands (with TAP_DIR=<clone> it commits and pushes); SPARKLE_PRIVATE_KEY=... APPCAST_IN=<current appcast.xml> scripts/make-appcast.sh <version> <dmg> dist produces dist/appcast.xml to upload to the release and to gh-pages. Without the environment variable, generate_appcast reads the key from the login keychain item that generate_keys --account macrosip created.

The update feed (ADR 0013)

The app reads https://raw.githubusercontent.com/HSMM/MacroSIP/gh-pages/appcast.xml (SUFeedURL, written by make-app.sh; no GitHub Pages setup needed) a few seconds after launch and then once a day, and asks before installing. Every item carries an EdDSA (ed25519) signature of the DMG; the public key is SPARKLE_PUBLIC_KEY in make-app.sh (SUPublicEDKey in Info.plist). The private key exists in exactly two places: the Actions secret SPARKLE_PRIVATE_KEY and the owner's login keychain ("MacroSIP Sparkle private key", plus Sparkle's own item, account macrosip). To restore the secret: security find-generic-password -s "MacroSIP Sparkle private key" -w | gh secret set SPARKLE_PRIVATE_KEY --repo HSMM/MacroSIP. Losing the key means installed copies can only be updated by a manual download; leaking it lets anyone who can also serve the feed push code to users.

Candidates sit on the beta channel, offered only when Settings, General, Updates, "Include release candidates" is on (default on when the running build is itself a candidate). Final releases have no channel and reach everyone. The appcast keeps the notes as they were at release time; to change them, edit the release and re-run the appcast job. A release whose appcast step failed is visible on GitHub but not offered in the app until the job is re-run.

Signing

Builds are signed ad hoc with the hardened runtime (codesign --options runtime), inside out: Sparkle's XPC services, Autoupdate, Updater.app, the framework, then the app with packaging/Entitlements.plist. The entitlements are com.apple.security.device.audio-input, com.apple.security.personal-information.addressbook and com.apple.security.cs.disable-library-validation; the last one exists because an ad hoc signature has no Team ID, so library validation would refuse the embedded Sparkle framework. Signing without the hardened runtime was rejected: it would also allow DYLD_INSERT_LIBRARIES into a process with microphone permission.

Consequences for users (ADR 0007): the first launch shows the unidentified-developer dialog (right-click, Open, or xattr -dr com.apple.quarantine), updates installed by Sparkle are trusted through the EdDSA signature and launch without it, and macOS ties the microphone permission to the exact build, so it asks again after each update. Developer ID signing and notarization are planned for later (docs/ROADMAP.md); when a Developer ID exists, remove the library-validation entitlement.

See also Building, Testing, Installation.

Clone this wiki locally