Repository navigation
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.
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.
-
Everything green locally:
swift build,scripts/test.sh,scripts/selftest-call.sh,scripts/make-dmg.sh; mount the DMG and start the app. -
Bump
Product.versionto the version being released, updatedocs/ROADMAP.md, commit. -
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
-
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 whenTAP_TOKENexists. -
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.xmllists the new item; an installed older version finds it through MacroSIP, Check for Updates (a candidate only with "Include release candidates" on).
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):
-
Check the tag:
N.N.NorN.N.N-rc.N, and theSPARKLE_PRIVATE_KEYsecret must exist (without it installed apps would never see the release). -
Build and test as in CI: cached
build-pjsip.sh,swift build,scripts/test.sh,scripts/selftest-call.shnatively and under Rosetta 2. -
Build the DMG:
scripts/make-dmg.sh release;lipo -infoon the binary; the job fails ifCFBundleShortVersionStringdiffers from the tag. The DMG is uploaded as an artifact for the cask job. -
Check the appcast:
scripts/publish-appcast.sh --check <tag> dist/MacroSIP.appfetchesgh-pagesand refuses a build number the feed has under another version. -
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,--prereleasefor tags with a-. A re-run keeps the release and replaces the DMG with--clobber. -
Update the appcast: the release body is rendered to HTML with GitHub's Markdown API and
scripts/publish-appcast.sh <tag> <dmg>runsscripts/make-appcast.sh, which calls Sparkle'sgenerate_appcast(from.build/artifacts/sparkle/Sparkle/bin) with the EdDSA key fromSPARKLE_PRIVATE_KEY, the download URL of the release asset,--embed-release-notes,--channel betafor candidates, and every earlier item kept (--maximum-versions 0, no deltas). The result is committed to thegh-pagesbranch (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 asappcast.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 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.
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.
User guide
- Installation
- Accounts
- Calls
- Incoming calls
- Contacts and call log
- Messages
- Directory
- Quick keys
- Audio
- Settings
- Menu bar
- Troubleshooting
For developers
Русский
中文