Releasing Pindrop
April 20, 2026 ยท View on GitHub
This document describes the release process for Pindrop, including code signing and update distribution via Sparkle.
EdDSA Signing Keys
Pindrop uses Sparkle for automatic updates. Updates are signed using EdDSA (Ed25519) for security.
Key Storage
- Public Key: Embedded in
Pindrop/Info.plistasSUPublicEDKey - Private Key: Stored securely in the macOS Keychain (automatically managed by Sparkle)
IMPORTANT: The private key is NEVER committed to the repository. It is stored only in the macOS Keychain of the machine that generated it.
Current Public Key
TCU0MwULuIK6y0ubIossVr+61PGh/wHZfFrRFc9F2Is=
This key is already configured in Pindrop/Info.plist:
<key>SUPublicEDKey</key>
<string>TCU0MwULuIK6y0ubIossVr+61PGh/wHZfFrRFc9F2Is=</string>
Generating New Keys (if needed)
If you need to regenerate the signing keys (e.g., if the private key is lost):
-
Download the Sparkle release:
curl -L -o Sparkle.tar.xz "https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz" tar -xf Sparkle.tar.xz -
Run the key generator:
./bin/generate_keys -
The tool will:
- Generate a new EdDSA keypair
- Store the private key in your macOS Keychain
- Output the public key to stdout
-
Update
Pindrop/Info.plistwith the new public key:<key>SUPublicEDKey</key> <string>YOUR_NEW_PUBLIC_KEY_HERE</string> -
CRITICAL: Users with older versions will NOT be able to update to versions signed with the new key. This should only be done for major releases or security incidents.
Release Process
Prerequisites
- macOS development machine with the private key in Keychain
- Xcode installed
justcommand runner:brew install just
Steps
-
Update version numbers in Xcode project settings
-
Build and sign the release:
just release 1.0.0
This will:
- Clean build artifacts
- Build the release version
- Sign the app with your Developer ID
- Create a DMG in
dist/Pindrop.dmg - Notarize the DMG with Apple
- Staple the notarization ticket to the DMG
- Render version-specific release notes HTML from
release-notes/vX.Y.Z.md - Generate
appcast.xmlfrom the final stapled DMG and attach release-notes links
- Upload the release:
- Upload
dist/Pindrop.dmgto GitHub Releases - Upload
appcast.xmlas a GitHub Release asset (or host it at your feed URL) - Upload the rendered
dist/release-notes-vX.Y.Z.htmlasset so Sparkle can show notes in the update window
- Tag the release:
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0
Appcast Generation Workflow
The just appcast command automates the appcast generation process:
What It Does
- Validates the DMG exists at the specified path
- Downloads Sparkle tools (if not already present):
- Downloads Sparkle 2.8.1 release
- Extracts
generate_appcastandsign_updatetobin/
- Renders release notes HTML from
release-notes/vX.Y.Z.mdintodist/release-notes-vX.Y.Z.html - Generates the appcast:
- Copies DMG to a temporary directory
- Runs
generate_appcastto create signatures - Injects
sparkle:releaseNotesLinkandsparkle:fullReleaseNotesLink - Outputs
appcast.xmlin the project root
Usage
# Generate appcast for the default DMG location
just appcast dist/Pindrop.dmg
# The appcast.xml will be created in the current directory
Appcast Structure
The generated appcast.xml follows Sparkle's RSS format:
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Pindrop Updates</title>
<item>
<title>Version 1.0.0</title>
<link>https://github.com/watzon/pindrop/releases/tag/v1.0.0</link>
<sparkle:version>100</sparkle:version>
<sparkle:shortVersionString>1.0.0</sparkle:shortVersionString>
<sparkle:releaseNotesLink>https://github.com/watzon/pindrop/releases/download/v1.0.0/release-notes-v1.0.0.html</sparkle:releaseNotesLink>
<sparkle:fullReleaseNotesLink>https://github.com/watzon/pindrop/releases/tag/v1.0.0</sparkle:fullReleaseNotesLink>
<enclosure url="..."
sparkle:edSignature="SIGNATURE"
length="SIZE"
type="application/octet-stream"/>
</item>
</channel>
</rss>
Manual Appcast Updates
If you need to manually edit appcast.xml:
- Use a generated appcast file as a starting point
- Update version numbers, release notes links, and download URL
- Generate the EdDSA signature using:
./bin/sign_update /path/to/Pindrop.dmg - Add the signature to the
<enclosure>element
Hosting the Appcast
The appcast can be hosted:
- GitHub Pages: Commit
appcast.xmltogh-pagesbranch - GitHub Releases: Upload as a release asset
- Custom server: Any HTTPS URL accessible to users
Update SUFeedURL in Pindrop/Info.plist to point to your hosted appcast.
Security Notes
- Never share the private key
- Never commit the private key to version control
- The private key is tied to your Mac's Keychain and cannot be exported easily
- If you lose the private key, you will need to generate new keys and users will need to manually download updates
Troubleshooting
"Update is improperly signed" error
This means the update was signed with a different key than what's in the app's Info.plist. Ensure:
- You're using the correct private key (check Keychain)
- The public key in
Info.plistmatches the private key used for signing
Lost private key
If you've lost the private key:
- Generate new keys using
generate_keys - Update
Info.plistwith the new public key - Notify users they'll need to manually download the update
- Future updates will work normally with the new key