Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/jasonkneen/openclicky/llms.txt

Use this file to discover all available pages before exploring further.

OpenClicky is distributed outside the Mac App Store using a Developer ID signed DMG, notarized by Apple and delivered to users through Sparkle OTA updates. The project ships with a set of release scripts under scripts/ that automate the most error-prone steps — version bumping, archiving, packaging, notarizing, stapling, and appcast generation — while leaving the final GitHub upload and git push as intentional manual steps so you review the artifact before it reaches users.
Mac App Store builds: Sparkle does not apply to App Store submissions. If you ever publish through the App Store, updates must travel through Apple’s own review and delivery pipeline. The scripts/ExportOptions.plist file must always have method=developer-id; the release script enforces this and will refuse to run if it is set to any other value.

Prerequisites

Before your first release, complete these one-time machine setup steps:
  1. Developer ID certificate — install your Developer ID Application: Jason Kneen (SW75ZJJ5R6) certificate into the login keychain. Verify it is present with:
    security find-identity -v -p codesigning | grep 'Developer ID'
    
  2. App-specific password — generate one at appleid.apple.com under Sign-In and Security → App-Specific Passwords using the Apple ID associated with your developer account (jason.knen@bouncingfish.com). Label it something recognizable, for example OpenClicky Notary.
  3. Notary keychain profile — store the credentials once so the release script can submit without prompting:
    xcrun notarytool store-credentials \
      --apple-id "jason.knen@bouncingfish.com" \
      --team-id "SW75ZJJ5R6" \
      --password "abcd-efgh-ijkl-mnop" \
      OpenClickyNotary
    
    The profile name OpenClickyNotary matches the NOTARY_PROFILE constant in scripts/release.sh. If you choose a different name, update both.
  4. Sparkle private key — the sign_update tool reads the EdDSA private key from the openclicky account in your Keychain. Confirm the matching public key is set as SUPublicEDKey in cursor-buddy/Info.plist before your first signed release.
Sparkle key rotation: Existing installed builds can only migrate to a new Sparkle public key if they first receive a bridge update signed by the old private key. Never overwrite the Keychain key or rotate SUPublicEDKey mid-stream without publishing that bridge release first. Keep the private key outside the repository.

Full Release Flow

1
Bump the Version
2
Update MARKETING_VERSION and CURRENT_PROJECT_VERSION in the Xcode project. Use the helper script to do this from the terminal — it updates every occurrence of MARKETING_VERSION in cursor-buddy.xcodeproj/project.pbxproj and delegates build-number management to agvtool:
3
# Bump marketing version to 1.1.0 and auto-increment the build number
scripts/bump-version.sh 1.1.0

# Or supply both explicitly
scripts/bump-version.sh 1.1.0 7

# Check current values without changing anything
scripts/bump-version.sh --show
4
You can also change the values directly in Xcode’s Build Settings for the cursor-buddy target if you prefer a GUI.
5
Archive in Xcode with the cursor-buddy Scheme
6
In Xcode, select the cursor-buddy scheme and a Generic macOS Device destination, then choose Product → Archive. The archive will appear in the Xcode Organizer.
7
If you use scripts/release.sh (see Automated Pipeline below), xcodebuild handles the archive step for you:
8
xcodebuild \
  -project cursor-buddy.xcodeproj \
  -scheme cursor-buddy \
  -configuration Release \
  -destination "generic/platform=macOS" \
  -archivePath build/OpenClicky.xcarchive \
  MARKETING_VERSION="1.1.0" \
  CURRENT_PROJECT_VERSION="7" \
  archive
9
Export a Developer ID Signed App
10
From the Xcode Organizer, click Distribute App, choose Direct Distribution, and follow the export wizard. The resulting .app is signed with your Developer ID Application certificate.
11
When driven by script, xcodebuild -exportArchive reads scripts/ExportOptions.plist, which specifies method=developer-id. The script verifies this value and exits if it has been changed:
12
xcodebuild \
  -exportArchive \
  -archivePath build/OpenClicky.xcarchive \
  -exportPath build/Export \
  -exportOptionsPlist scripts/ExportOptions.plist
13
After export the script also runs a deep signature verification:
14
codesign --verify --deep --strict --verbose=2 build/Export/OpenClicky.app
spctl --assess --type execute --verbose=2 build/Export/OpenClicky.app
15
Package into a DMG
16
Stage the .app alongside an /Applications symlink and wrap everything with hdiutil:
17
mkdir -p build/dmg-stage
cp -R build/Export/OpenClicky.app build/dmg-stage/
ln -s /Applications build/dmg-stage/Applications

hdiutil create \
  -volname "OpenClicky 1.1.0" \
  -srcfolder build/dmg-stage \
  -ov \
  -format UDZO \
  dist/OpenClicky-1.1.0-7.dmg
18
The DMG is then signed with the Developer ID:
19
codesign --sign "Developer ID Application: Jason Kneen (SW75ZJJ5R6)" \
         --timestamp \
         dist/OpenClicky-1.1.0-7.dmg
20
Notarize and Staple the DMG
21
Submit the signed DMG to Apple’s notary service using the keychain profile created during setup. The --wait flag blocks until Apple returns a result:
22
xcrun notarytool submit dist/OpenClicky-1.1.0-7.dmg \
  --keychain-profile "OpenClickyNotary" \
  --wait
23
Once notarization succeeds, staple the ticket so Gatekeeper can verify the DMG offline:
24
xcrun stapler staple dist/OpenClicky-1.1.0-7.dmg
xcrun stapler validate dist/OpenClicky-1.1.0-7.dmg
25
Sign the DMG with Sparkle’s sign_update only after stapling. The EdDSA signature must cover the final stapled bytes. Signing an earlier, unstapled copy and then stapling will invalidate the Sparkle signature.
26
Sign for Sparkle with sign_update
27
Sparkle verifies every update DMG against the EdDSA signature in appcast.xml. Generate the signature using Sparkle’s sign_update binary, which reads the private key from the openclicky Keychain account:
28
sign_update --account openclicky dist/OpenClicky-1.1.0-7.dmg
29
sign_update prints an <enclosure> snippet containing sparkle:edSignature and length. Keep both values — you will need them in the next step.
30
If sign_update is not on your PATH, the generate-appcast.sh script searches for it automatically inside Xcode’s DerivedData for the cursor-buddy project. Build the project in Xcode at least once to populate DerivedData, or install via:
31
brew install --cask sparkle
32
To get the DMG byte length manually:
33
stat -f%z dist/OpenClicky-1.1.0-7.dmg
34
Upload to GitHub Releases
35
Create a GitHub release named v<MARKETING_VERSION> and attach the stapled DMG:
36
gh release create v1.1.0 dist/OpenClicky-1.1.0-7.dmg \
  --title "OpenClicky 1.1.0" \
  --notes "Release notes here"
37
The download URL will follow the pattern:
38
https://github.com/jasonkneen/openclicky/releases/download/v1.1.0/OpenClicky-1.1.0-7.dmg
39
Copy this exact URL — it must match the url attribute in appcast.xml.
40
Update appcast.xml
41
Add a new <item> block to appcast.xml in the repository root. Replace all placeholder values with the real version, build number, publication date, download URL, byte length, and Sparkle signature:
42
<item>
    <title>OpenClicky 1.1.0</title>
    <pubDate>Fri, 24 Apr 2026 16:10:08 +0000</pubDate>
    <sparkle:version>7</sparkle:version>
    <sparkle:shortVersionString>1.1.0</sparkle:shortVersionString>
    <sparkle:minimumSystemVersion>14.2</sparkle:minimumSystemVersion>
    <enclosure
        url="https://github.com/jasonkneen/openclicky/releases/download/v1.1.0/OpenClicky-1.1.0-7.dmg"
        length="12345678"
        type="application/octet-stream"
        sparkle:edSignature="SIGNATURE_FROM_SPARKLE_SIGN_UPDATE"/>
</item>
43
scripts/generate-appcast.sh automates this step (see Automated Pipeline below). It locates sign_update, generates the signature, reads version and minimum OS from the project file, and rewrites appcast.xml in one pass.
44
Commit and Push appcast.xml to main
45
Sparkle reads the feed directly from the main branch on GitHub. Pushing appcast.xml is what makes the update visible to installed builds:
46
git add appcast.xml cursor-buddy.xcodeproj/project.pbxproj
git commit -m "Release 1.1.0 (build 7)"
git tag v1.1.0
git push && git push --tags

Automated Pipeline

The scripts in scripts/ can run the entire flow with a single command.

scripts/ship.sh — end-to-end release

ship.sh chains bump-version.shrelease.shgenerate-appcast.sh and prints the remaining manual steps (GitHub upload and git push) at the end:
# Bump to 1.1.0, auto-increment build, archive, sign, notarize, generate appcast
scripts/ship.sh 1.1.0

# Specify build number explicitly
scripts/ship.sh 1.1.0 7

# Smoke-test the pipeline without contacting Apple's notary service
scripts/ship.sh 1.1.0 7 --skip-notarize

# Skip the version bump and build whatever is currently in the project
scripts/ship.sh --no-bump
After ship.sh completes, the output lists the exact gh release create and git commands to run.

scripts/release.sh — archive, sign, notarize

Run release.sh alone if you have already bumped the version and only need the build/notarize step:
# Uses MARKETING_VERSION and CURRENT_PROJECT_VERSION from the project
scripts/release.sh

# Override both values
scripts/release.sh 1.1.0 7

# Build and package without notarizing (useful for local testing)
scripts/release.sh 1.1.0 7 --skip-notarize
Output lands in dist/OpenClicky-<version>-<build>.dmg.

scripts/generate-appcast.sh — Sparkle signing and appcast

Run generate-appcast.sh after you have a notarized and stapled DMG:
scripts/generate-appcast.sh dist/OpenClicky-1.1.0-7.dmg

# Provide an explicit download URL instead of the default GitHub convention
scripts/generate-appcast.sh dist/OpenClicky-1.1.0-7.dmg \
  https://github.com/jasonkneen/openclicky/releases/download/v1.1.0/OpenClicky-1.1.0-7.dmg
The script reads MARKETING_VERSION, CURRENT_PROJECT_VERSION, and MACOSX_DEPLOYMENT_TARGET from the Xcode project file and writes a complete appcast.xml.

Post-Release Verification Checklist

Before declaring a release complete, confirm:
  • The DMG opens without Gatekeeper warnings on a clean Mac.
  • spctl accepts the DMG:
    spctl --assess --type open --context context:primary-signature -vv OpenClicky-1.1.0-7.dmg
    
  • The appcast URL is reachable over HTTPS.
  • The new appcast item has a higher sparkle:version (build number) than the currently installed build.
  • The url in the appcast <enclosure> exactly matches the uploaded GitHub Release asset URL.
  • sparkle:edSignature was generated from the final stapled DMG, not an earlier copy.
  • A clean installed build checks the feed and offers or stages the update without requiring the user to visit GitHub.

Build docs developers (and LLMs) love