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 delivers over-the-air updates to directly distributed builds using Sparkle 2. Installed apps poll a signed appcast.xml feed hosted in this repository on GitHub, discover newer releases, download the notarized DMG from GitHub Releases, and install it in the background — all without requiring the user to visit a website or run any manual steps. This update mechanism applies only to direct-distribution builds; a hypothetical future Mac App Store build would receive updates exclusively through Apple’s own delivery pipeline and would not use Sparkle at all.

How Sparkle Is Configured

The following Info.plist keys control update behavior in OpenClicky:
KeyBehavior
SUEnableAutomaticChecksEnables automatic feed checks on a schedule.
SUAllowsAutomaticUpdatesSurfaces the option for the user to opt into automatic updates in the Sparkle UI.
SUAutomaticallyUpdateDefaults to background download and silent install behavior when the user has opted in.
SUScheduledCheckIntervalSets the polling cadence to roughly once per day.
These keys are set in cursor-buddy/Info.plist. When a direct-distribution build is running and the machine is online, Sparkle checks the feed on launch and on the configured interval, compares sparkle:version (the build number) against the installed CFBundleVersion, and triggers a download if a higher version is found.

Update Feed

SettingValue
Feed URLhttps://raw.githubusercontent.com/jasonkneen/openclicky/main/appcast.xml
Feed file in repoappcast.xml (repo root)
Release asset hostGitHub Releases at https://github.com/jasonkneen/openclicky/releases
Public EdDSA keySUPublicEDKey in cursor-buddy/Info.plist
Sparkle Keychain accountopenclicky
Sparkle reads SUFeedURL from Info.plist to locate the appcast. The feed is served over raw GitHub HTTPS so that the content is available to Sparkle without authentication. Pushing a new commit to main that updates appcast.xml is what makes a release visible to installed builds.
Key verification before first release: Before publishing any release, confirm that the Sparkle private key stored under the openclicky account in your Keychain corresponds to the SUPublicEDKey currently in cursor-buddy/Info.plist. If the keys do not match, Sparkle will reject all update signatures and users will never receive updates.Keep the private key outside the repository. Never commit it. If you need to rotate the key, you must first ship a bridge update signed by the old private key that carries the new public key in SUPublicEDKey. Existing installed builds can only trust a new key if they first receive and install that bridge update signed by the key they already trust.

Appcast Structure

The appcast.xml file at the repository root is an RSS 2.0 feed with Sparkle namespace extensions. Each release is represented by one <item> element. Below is the full item template from the project documentation — replace every placeholder with real values after you have a notarized and stapled DMG:
<?xml version="1.0" standalone="yes"?>
<rss xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" version="2.0">
    <channel>
        <title>OpenClicky Updates</title>
        <link>https://github.com/jasonkneen/openclicky</link>
        <description>OpenClicky direct-distribution updates.</description>
        <language>en</language>
        <item>
            <title>OpenClicky 1.0.1</title>
            <pubDate>Fri, 24 Apr 2026 16:10:08 +0000</pubDate>
            <sparkle:version>7</sparkle:version>
            <sparkle:shortVersionString>1.0.1</sparkle:shortVersionString>
            <sparkle:minimumSystemVersion>14.2</sparkle:minimumSystemVersion>
            <enclosure
                url="https://github.com/jasonkneen/openclicky/releases/download/v1.0.1/OpenClicky-1.0.1.dmg"
                length="12345678"
                type="application/octet-stream"
                sparkle:edSignature="SIGNATURE_FROM_SPARKLE_SIGN_UPDATE"/>
        </item>
    </channel>
</rss>

Appcast Field Reference

FieldDescription
<title>Human-readable release name, e.g. OpenClicky 1.0.1.
<pubDate>RFC 2822 timestamp in UTC, e.g. Fri, 24 Apr 2026 16:10:08 +0000.
sparkle:versionThe build number (CURRENT_PROJECT_VERSION). Sparkle compares this integer against CFBundleVersion to determine whether an update is available. Must be strictly greater than the installed build.
sparkle:shortVersionStringThe human-readable marketing version (MARKETING_VERSION), e.g. 1.0.1.
sparkle:minimumSystemVersionMinimum macOS version required to install the update, e.g. 14.2.
enclosure urlDirect download URL to the notarized, stapled DMG on GitHub Releases. Must match the uploaded asset exactly.
enclosure lengthByte size of the DMG file. Sparkle uses this to show download progress.
enclosure typeAlways application/octet-stream for a DMG.
sparkle:edSignatureEdDSA signature produced by Sparkle’s sign_update tool using the private key for Keychain account openclicky. Must be generated from the final stapled DMG.

Local Commands

Notarize the DMG

Submit the signed DMG to Apple’s notary service. The keychain profile OpenClickyNotary is created once per machine using xcrun notarytool store-credentials (see Building and Releasing for setup steps):
# Submit and wait for Apple's verdict
xcrun notarytool submit OpenClicky-1.0.1.dmg \
  --keychain-profile "OpenClickyNotary" \
  --wait

# Staple the notarization ticket into the DMG
xcrun stapler staple OpenClicky-1.0.1.dmg

Generate the Sparkle EdDSA Signature

Run Sparkle’s sign_update against the stapled DMG. The tool reads the private key from the openclicky Keychain account and prints the sparkle:edSignature and length values you need for the appcast <enclosure>:
sign_update --account openclicky OpenClicky-1.0.1.dmg
Get the byte length independently if needed:
stat -f%z OpenClicky-1.0.1.dmg
Always run sign_update after stapling. Stapling changes the file, so a signature produced before stapling will not match the bytes Sparkle downloads and verifies. If you sign before stapling, repeat the signing step on the final stapled file.

Automate with generate-appcast.sh

scripts/generate-appcast.sh handles signature generation, length calculation, and appcast writing in a single step. It automatically locates sign_update in Xcode’s DerivedData for the cursor-buddy project and falls back to any sign_update on PATH:
# Use the GitHub Releases URL inferred from the project version
scripts/generate-appcast.sh dist/OpenClicky-1.0.1-7.dmg

# Supply an explicit download URL
scripts/generate-appcast.sh dist/OpenClicky-1.0.1-7.dmg \
  https://github.com/jasonkneen/openclicky/releases/download/v1.0.1/OpenClicky-1.0.1-7.dmg
The script reads MARKETING_VERSION, CURRENT_PROJECT_VERSION, and MACOSX_DEPLOYMENT_TARGET directly from cursor-buddy.xcodeproj/project.pbxproj and writes a complete, valid appcast.xml.

Local OTA Testing

Before publishing to GitHub, you can verify the full Sparkle update loop on your own machine using a local HTTP feed.

Install the Baseline Build

Set the project to a lower test version, archive and export or install a Release build, then copy it to /Applications:
# Point the installed build at a local feed
defaults write com.jkneen.openclicky OpenClickySparkleFeedURLOverride \
  -string "http://127.0.0.1:8808/appcast.xml"
The override accepts https, file, or local http feeds. When present, OpenClicky triggers a Sparkle background check immediately on launch.

Build the Update and Serve a Local Feed

Bump the version, build the update DMG, sign it, and use the helper script to generate a local appcast:
scripts/create-local-appcast.sh /path/to/OpenClicky-1.0.1.dmg 1.0.1 101 "SIGNATURE_FROM_SIGN_UPDATE"
That writes /tmp/openclicky-ota/appcast.xml with the higher build number. Serve the folder:
cd /tmp/openclicky-ota
python3 -m http.server 8808 --bind 127.0.0.1

Trigger and Verify the Update

  1. Launch /Applications/OpenClicky.app.
  2. Sparkle reads the local feed override and detects the higher build number.
  3. Accept or wait for the update flow to complete.
  4. After relaunch, confirm the displayed app version and build number changed.

Clean Up After Testing

defaults delete com.jkneen.openclicky OpenClickySparkleFeedURLOverride

Pre-Release Checklist

Before committing an updated appcast.xml and pushing to main:
  • SUPublicEDKey in cursor-buddy/Info.plist matches the private key stored under Keychain account openclicky.
  • The DMG is notarized and stapled — confirmed with xcrun stapler validate.
  • sparkle:edSignature was generated from the final stapled DMG, not an earlier copy.
  • sparkle:version (build number) is strictly greater than any build number already in appcast.xml.
  • The enclosure url in appcast.xml exactly matches the GitHub Release asset URL (including filename and version tag).
  • The enclosure length matches the actual byte size of the uploaded DMG.
  • spctl --assess --type open --context context:primary-signature -vv accepts the DMG on a clean Mac.
  • A clean installed build checks the feed on launch and offers the update without requiring a manual visit to GitHub.

Build docs developers (and LLMs) love