TapTick is distributed directly (outside the Mac App Store) via Developer ID signing and Apple Notarization. This document covers every path from local dev builds to a fully notarized DMG ready for users to download.
make setup # installs xcodegen, swift-format, xcbeautify via Homebrew
brew install create-dmg # needed for DMG packaging (not included in setup)You need a paid Apple Developer Program membership (USD 99/yr). Log in at developer.apple.com.
| Configuration | Signing Style | Identity | Used For |
|---|---|---|---|
| Debug | Automatic | Apple Development | Daily dev, make build |
| Release (local) | Manual (CLI override) | Developer ID Application | make archive / make dist |
| Release (CI) | Manual (CLI override) | Developer ID Application | GitHub Actions |
project.yml always stores Apple Development as the base identity.
The Developer ID Application identity is injected at build time via
xcodebuild command-line overrides — this is necessary because Xcode's
Automatic signing mode and Developer ID are mutually exclusive.
The iCloud sync entitlements (com.apple.developer.icloud-container-identifiers
and com.apple.developer.ubiquity-container-identifiers) are currently
commented out in Resources/TapTick.entitlements. Developer ID + iCloud
requires a provisioning profile explicitly created in the Developer Portal with
both the App ID and iCloud container registered. Until that profile exists,
enabling those entitlements will cause xcodebuild archive to fail.
To re-enable iCloud for distribution, see the iCloud section below.
No signing setup required. Xcode manages everything automatically.
The debug variant deliberately launches as TapTick Dev.app with bundle
identifier com.taptick.app.dev, so its TCC permissions and local state stay
separate from the notarized release app.
make build # Debug build via xcodebuild
make run # Debug build + launch app
open TapTick.xcodeproj # or open in Xcode and press ⌘RProduces a notarized, stapled DMG ready to hand to users.
make distPipeline: archive → export → notarize → staple → dmg
Output: build/TapTick.dmg
Requires the one-time Keychain profile setup described below.
Individual steps can also be run in isolation:
make archive # .xcarchive signed with Developer ID
make export # export .xcarchive → .app (uses Resources/exportOptions.plist)
make notarize # submit to Apple Notary Service, staple ticket
make dmg # package .app into DMGIf you prefer not to use the terminal for releases:
- Product → Archive
- Distribute App → Developer ID → Upload → Automatically notarize
- Export the stapled
.app - Run
create-dmgmanually or usemake dmg(assumes.appis atbuild/export/TapTick.app)
Triggered automatically on v* tag push or via manual workflow_dispatch.
See .github/workflows/build.yml.
git tag v1.0.0
git push origin v1.0.0The DMG artifact is uploaded to the Actions run page (retained 30 days) and can be downloaded from Actions → the run → Artifacts.
- Open Xcode → Settings → Accounts → select your Apple ID → Manage Certificates
- Click
+→ Developer ID Application - Xcode creates and installs the certificate in your login Keychain
Verify it is present:
security find-identity -v -p codesigning | grep "Developer ID Application"Apple Notarization requires an App-specific password (your main Apple ID password cannot be used).
Get an App-specific password:
- Go to appleid.apple.com
- Sign In → Sign-In and Security → App-Specific Passwords → Generate
- Name it
TapTick Notarization, copy the generated password (xxxx-xxxx-xxxx-xxxx)
Store it in Keychain:
xcrun notarytool store-credentials "TapTick" \
--apple-id you@example.com \
--team-id 3FKXTCP8JU \
--password "xxxx-xxxx-xxxx-xxxx"The profile name TapTick matches NOTARIZE_PROFILE in the Makefile.
To use a different profile name:
make dist NOTARIZE_PROFILE=MyProfileNavigate to: GitHub repo → Settings → Secrets and variables → Actions → New repository secret
| Secret | Description | How to get it |
|---|---|---|
APPLE_CERTIFICATE_BASE64 |
Developer ID Application certificate as Base64 | See below |
APPLE_CERTIFICATE_PASSWORD |
Password set when exporting the .p12 |
Set when exporting |
APPLE_TEAM_ID |
Apple Developer Team ID | 3FKXTCP8JU — or check developer.apple.com/account → Membership |
APPLE_ID |
Apple ID email for notarytool | Your Apple ID login email |
APPLE_APP_PASSWORD |
App-specific password for notarytool | Same as Step 2 above (xxxx-xxxx-xxxx-xxxx) |
SPARKLE_ED_PRIVATE_KEY |
EdDSA (ed25519) private key for signing updates | See Sparkle EdDSA Keys below |
SPARKLE_ED_PUBLIC_KEY |
EdDSA (ed25519) public key embedded in the app | Generated alongside the private key |
- Open Keychain Access
- Find
Developer ID Application: <your name>under My Certificates - Right-click → Export → save as
DeveloperID.p12, set a strong password - Base64-encode it:
base64 -i DeveloperID.p12 | pbcopy # copies to clipboard, paste into GitHub Secret- Delete the local
.p12file after uploading — it is sensitive.
Sparkle uses EdDSA (ed25519) signatures to verify that downloaded updates are authentic. You need to generate a keypair once and store both parts as GitHub Secrets.
Generate the keypair:
Download Sparkle's CLI tools from the latest release and run:
# Extract Sparkle tools
tar xJf Sparkle-2.7.5.tar.xz bin
# Generate a new EdDSA keypair (saved to your login Keychain)
./bin/generate_keysThe tool will:
- Save the private key in your Mac's login Keychain.
- Print the public key (a base64 string) to stdout.
Export the private key (for CI):
./bin/generate_keys -x sparkle_private_key
cat sparkle_private_key | pbcopy # copies to clipboardStore as GitHub Secrets:
| Secret | Value |
|---|---|
SPARKLE_ED_PRIVATE_KEY |
Contents of the exported private key file |
SPARKLE_ED_PUBLIC_KEY |
The base64 public key string printed by generate_keys |
Clean up:
rm sparkle_private_key # do NOT leave the private key on disk
⚠️ Keep your private key safe. If it is lost, you can still rotate keys for Developer ID-signed apps (Sparkle supports key rotation when the app is also code-signed with Apple's Developer ID). But it's much simpler to never lose it.
How it works in CI:
SPARKLE_ED_PUBLIC_KEYis injected into the app'sInfo.plistvia theSPARKLE_ED_PUBLIC_KEYXcode build setting at archive time.SPARKLE_ED_PRIVATE_KEYis read by Sparkle'sgenerate_appcasttool (via environment variable) to sign the DMG and produceappcast.xml.- The
appcast.xmlis deployed to GitHub Pages athttps://amio.github.io/TapTick/appcast.xml.
TapTick uses Sparkle 2 for automatic updates.
- The app checks
https://amio.github.io/TapTick/appcast.xmlfor new versions (by default every 24 hours, configurable by the user). - If a newer version is found, Sparkle shows its native update UI with release notes, download progress, and a restart prompt. TapTick disables Sparkle's automatic-download/install opt-in checkbox so update policy stays in the app settings.
- The DMG is downloaded from GitHub Releases, verified against the EdDSA signature in the appcast, extracted, and the app is replaced + relaunched.
archive → export → notarize → create DMG
↓
GitHub Release (DMG uploaded)
↓
generate_appcast (signs DMG, produces appcast.xml)
↓
Deploy to GitHub Pages (appcast.xml + landing page)
| File | Purpose |
|---|---|
Resources/Info.plist |
Contains SUFeedURL and SUPublicEDKey |
Sources/TapTickKit/Services/UpdateService.swift |
Wraps Sparkle's SPUUpdater |
public/appcast.xml |
Generated by CI, served via GitHub Pages |
.github/workflows/build.yml |
CI pipeline with appcast generation step |
When ready to ship iCloud sync in a notarized build:
-
Register App ID at developer.apple.com → Certificates, Identifiers & Profiles → Identifiers →
com.taptick.app→ enable iCloud capability -
Create iCloud container
iCloud.com.taptick.appunder Identifiers → iCloud Containers -
Associate container with the App ID under the iCloud capability settings
-
Create a provisioning profile: Profiles →
+→ Developer ID → select App IDcom.taptick.app→ select your Developer ID certificate → download and double-click to install -
Uncomment the entitlements in
Resources/TapTick.entitlements:
<key>com.apple.developer.icloud-container-identifiers</key>
<array>
<string>iCloud.com.taptick.app</string>
</array>
<key>com.apple.developer.ubiquity-container-identifiers</key>
<array>
<string>iCloud.com.taptick.app</string>
</array>- Update
Makefilearchive target to reference the profile by name or UUID:
PROVISIONING_PROFILE_SPECIFIER="TapTick Developer ID"- Update
Resources/exportOptions.plistto add the profile mapping:
<key>provisioningProfiles</key>
<dict>
<key>com.taptick.app</key>
<string>TapTick Developer ID</string>
</dict>- Update CI: store the
.mobileprovisionfile as an additional secret (APPLE_PROVISIONING_PROFILE_BASE64) and add a step to install it before the archive step (decode withbase64 --decode, copy to~/Library/MobileDevice/Provisioning Profiles/).
After make dist or make export, confirm the signing is correct:
# Verify Developer ID chain
codesign -dv --verbose=4 build/export/TapTick.app
# Verify notarization staple
xcrun stapler validate build/export/TapTick.app
# Check Gatekeeper would pass
spctl --assess --type exec --verbose build/export/TapTick.appExpected output for codesign:
Authority=Developer ID Application: Xiaowei Jin (3FKXTCP8JU)
Authority=Developer ID Certification Authority
Authority=Apple Root CA
Expected output for spctl:
build/export/TapTick.app: accepted
source=Notarized Developer ID