Skip to content

ci: sign and notarize releases with Developer ID - #17

Merged
TerrifiedBug merged 3 commits into
masterfrom
ci/notarize-releases
Aug 2, 2026
Merged

ci: sign and notarize releases with Developer ID#17
TerrifiedBug merged 3 commits into
masterfrom
ci/notarize-releases

Conversation

@TerrifiedBug

Copy link
Copy Markdown
Owner

Releases are now signed with the Developer ID Application certificate and notarized by Apple, so Homebrew and manual installs open without a Gatekeeper prompt.

Signing

Signing moves from a hand-rolled codesign --deep to xcodebuild archive + -exportArchive. This is not cosmetic. TickerBar is sandboxed and its entitlements reference $(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer XPC mach-lookup exceptions. codesign does not expand build settings, so the old command baked the literal string $(PRODUCT_BUNDLE_IDENTIFIER)-spks into the shipped app instead of com.tickerbar.app-spks. xcodebuild expands it, and signs Sparkle.framework, Updater.app, Installer.xpc and Downloader.xpc inside-out with their own entitlements rather than stamping the main app's entitlements onto every nested binary.

Verified locally against a real Developer ID build: every nested binary carries flags=0x10000(runtime), a secure timestamp, and the Developer ID authority, and the main app's entitlements now resolve to com.tickerbar.app-spks / com.tickerbar.app-spki.

No more silent ad-hoc fallback

The old workflow fell back to ad-hoc signing when secrets were absent, which is how shipped builds ended up needing xattr -dr com.apple.quarantine. The release now fails loudly if signing or notary credentials are missing, and asserts spctl --assess reports source=Notarized Developer ID before publishing.

Secret scoping

Signing secrets live in a release GitHub environment restricted to v* tags, so no other workflow or ref can read the Developer ID key.

Shared build script

scripts/build-release.sh holds build, sign, notarize, staple and package, and CI calls it, so a local run and a CI run produce the same artifact. SKIP_NOTARIZE=1 allows local testing.

Rename

Repository is now TerrifiedBug/tickerbar and the release asset is tickerbar.zip. GitHub redirects the old paths, so existing installs keep resolving their appcast and updating. The TickerBar.app bundle name is unchanged.

Verification

  • 86 tests pass (xcodebuild test)
  • Widget extension builds
  • scripts/build-release.sh completes end to end with SKIP_NOTARIZE=1
  • codesign --verify --deep --strict passes on the exported app

TerrifiedBug added 3 commits August 2, 2026 18:24
Signing moves from a hand-rolled 'codesign --deep' to xcodebuild archive and
export. The app is sandboxed and its entitlements reference
$(PRODUCT_BUNDLE_IDENTIFIER) for Sparkle's installer XPC mach-lookup
exceptions. codesign does not expand build settings, so the old command
signed a literal '$(PRODUCT_BUNDLE_IDENTIFIER)-spks' into the shipped app.
xcodebuild expands it and signs Sparkle.framework, Updater.app, Installer.xpc
and Downloader.xpc inside-out with their own entitlements.

Releases now fail loudly when signing or notary credentials are missing,
instead of falling back to ad-hoc signing and leaving users to clear
quarantine by hand. Signing secrets are scoped to a 'release' environment
that only v* tags can read.

scripts/build-release.sh holds the build so local and CI runs produce the
same artifact. Repository is now TerrifiedBug/tickerbar and the asset is
tickerbar.zip.
The release job imports the Developer ID private key, so a mutable action tag
is a live exfiltration path: whoever controls the tag controls code running in
the same job as the key. Pin every action to a full commit SHA and let
Dependabot keep the pins current.
No released build has ever been sandboxed. The old release workflow only
reached its '--deep --entitlements' path when signing secrets were present,
and they never were, so every shipped build came from the ad-hoc fallback
'codesign --force --deep --sign -', which applies no entitlements at all.

Signing correctly would therefore have enabled the sandbox for the first
time. That relocates preferences from ~/Library/Preferences into
~/Library/Containers, so every existing user would have opened v1.5.0 to an
empty watchlist with their holdings and alerts gone.

TickerBar ships through Developer ID for Homebrew, never the App Store, so
the sandbox is optional. Remove the entitlements file, its build setting and
the sandbox-only SUEnableInstallerLauncherService key. Hardened runtime stays
on, which is what notarization actually requires.

Verified: the notarized build reports no entitlements, creates no container,
and reads and writes the existing unsandboxed preferences.
@TerrifiedBug
TerrifiedBug merged commit d2b2747 into master Aug 2, 2026
1 check passed
@TerrifiedBug
TerrifiedBug deleted the ci/notarize-releases branch August 2, 2026 17:47
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