Conversation
- EdDSA public key set in Info.plist - Sparkle auto-check enabled at startup - Build workflow signs ZIP and updates appcast.xml on release - SPARKLE_PRIVATE_KEY stored as GitHub Secret - Version 1.0.2 (build 3)
Sparkle auto-updates + signed appcast v1.0.2
There was a problem hiding this comment.
Pull request overview
Enables Sparkle-based auto-updates for AutoBrew and bumps the release version to v1.0.2, including CI automation to generate/sign the appcast used by Sparkle.
Changes:
- Bump
MARKETING_VERSION/CURRENT_PROJECT_VERSIONto1.0.2/3. - Enable Sparkle updater startup by default and configure the app’s Sparkle public key.
- Add GitHub Actions steps to sign the update ZIP and generate/push
appcast.xmlonmain.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
project.yml |
Version/build bump for the v1.0.2 release. |
Sources/Services/UpdaterService.swift |
Starts Sparkle updater immediately (startingUpdater: true). |
AutoBrew/Info.plist |
Sets SUPublicEDKey for Sparkle EdDSA signature verification. |
.github/workflows/build-and-release.yml |
Adds Sparkle signing + appcast generation/publish during release workflow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| git config user.email "hello@marcelrgberger.com" | ||
| git add appcast.xml | ||
| git commit -m "Update appcast for v$VERSION" || true | ||
| git push origin HEAD:main || true |
There was a problem hiding this comment.
git commit/git push are both silenced with || true. If the push fails (e.g., branch protections, token permissions, transient errors), the workflow will still proceed to create a GitHub Release but the feed at SUFeedURL won’t be updated, breaking auto-update discovery. It would be safer to fail the job when updating/pushing the appcast fails (or otherwise make the release conditional on a successful appcast update).
| git push origin HEAD:main || true | |
| git push origin HEAD:main |
| <sparkle:shortVersionString>$VERSION</sparkle:shortVersionString> | ||
| <sparkle:minimumSystemVersion>26.0</sparkle:minimumSystemVersion> | ||
| <enclosure | ||
| url="https://github.com/marcelrgberger/auto-brew/releases/download/v$VERSION/AutoBrew.zip" |
There was a problem hiding this comment.
The workflow updates and pushes appcast.xml before the GitHub Release (and its AutoBrew.zip asset) is created. During that window, the appcast enclosure url points to a release asset that doesn’t exist yet, which can produce transient 404s for clients checking updates. Consider creating/uploading the release assets first, then publishing the appcast update afterward (or otherwise ensuring the enclosure URL is valid when the appcast is pushed).
| url="https://github.com/marcelrgberger/auto-brew/releases/download/v$VERSION/AutoBrew.zip" | |
| url="https://github.com/marcelrgberger/auto-brew/releases/latest/download/AutoBrew.zip" |
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/latest/download/Sparkle-2.9.0.tar.xz" | tar xJ -C /tmp | ||
| SPARKLE_BIN="/tmp/bin/sign_update" |
There was a problem hiding this comment.
After downloading and extracting the Sparkle tools, SPARKLE_BIN is hard-coded to /tmp/bin/sign_update. The Sparkle release archive layout isn’t guaranteed to create /tmp/bin/…, so this path is likely incorrect and will cause the signing step to fail. Instead, locate sign_update inside the extracted archive (or extract into a known directory) and set SPARKLE_BIN to that actual path, and also ensure it’s executable before running it.
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/latest/download/Sparkle-2.9.0.tar.xz" | tar xJ -C /tmp | |
| SPARKLE_BIN="/tmp/bin/sign_update" | |
| TMP_SPARKLE_DIR="$(mktemp -d)" | |
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/latest/download/Sparkle-2.9.0.tar.xz" | tar xJ -C "$TMP_SPARKLE_DIR" | |
| SPARKLE_BIN=$(find "$TMP_SPARKLE_DIR" -name "sign_update" -type f 2>/dev/null | head -1) | |
| if [ -z "$SPARKLE_BIN" ]; then | |
| echo "Error: sign_update not found in downloaded Sparkle archive." | |
| exit 1 | |
| fi | |
| chmod +x "$SPARKLE_BIN" |
| set -e | ||
| # Get Sparkle tools from SPM cache | ||
| SPARKLE_BIN=$(find "$HOME/Library/Developer/Xcode/DerivedData" -name "sign_update" -path "*/Sparkle/bin/*" 2>/dev/null | head -1) | ||
| if [ -z "$SPARKLE_BIN" ]; then | ||
| echo "sign_update not found, downloading Sparkle tools..." | ||
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/latest/download/Sparkle-2.9.0.tar.xz" | tar xJ -C /tmp | ||
| SPARKLE_BIN="/tmp/bin/sign_update" | ||
| fi | ||
|
|
||
| # Sign the ZIP | ||
| echo "$SPARKLE_KEY" > /tmp/sparkle_key | ||
| SIGNATURE=$("$SPARKLE_BIN" "build/$APP_NAME.zip" --ed-key-file /tmp/sparkle_key 2>&1 | grep "sparkle:edSignature" | sed 's/.*"\(.*\)".*/\1/') | ||
| LENGTH=$(stat -f%z "build/$APP_NAME.zip") | ||
| rm -f /tmp/sparkle_key | ||
|
|
||
| echo "SPARKLE_SIGNATURE=$SIGNATURE" >> "$GITHUB_ENV" | ||
| echo "ZIP_LENGTH=$LENGTH" >> "$GITHUB_ENV" |
There was a problem hiding this comment.
This step uses set -e, but the signature extraction is done through a pipeline inside command substitution. Without set -o pipefail (and ideally -u), failures in sign_update/grep can be masked and result in an empty/invalid SPARKLE_SIGNATURE being exported. Consider using set -euo pipefail here and explicitly validating that SIGNATURE is non-empty before writing it to $GITHUB_ENV.
| echo "$SPARKLE_KEY" > /tmp/sparkle_key | ||
| SIGNATURE=$("$SPARKLE_BIN" "build/$APP_NAME.zip" --ed-key-file /tmp/sparkle_key 2>&1 | grep "sparkle:edSignature" | sed 's/.*"\(.*\)".*/\1/') | ||
| LENGTH=$(stat -f%z "build/$APP_NAME.zip") | ||
| rm -f /tmp/sparkle_key |
There was a problem hiding this comment.
The Sparkle private key is written to /tmp/sparkle_key without restricting file permissions. Since this is a signing key, it’s safer to create the file with a restrictive umask / chmod (and preferably use a unique temp path) to avoid accidental exposure to other processes on the runner.
| echo "$SPARKLE_KEY" > /tmp/sparkle_key | |
| SIGNATURE=$("$SPARKLE_BIN" "build/$APP_NAME.zip" --ed-key-file /tmp/sparkle_key 2>&1 | grep "sparkle:edSignature" | sed 's/.*"\(.*\)".*/\1/') | |
| LENGTH=$(stat -f%z "build/$APP_NAME.zip") | |
| rm -f /tmp/sparkle_key | |
| SPARKLE_KEY_FILE=$(mktemp) | |
| chmod 600 "$SPARKLE_KEY_FILE" | |
| printf '%s\n' "$SPARKLE_KEY" > "$SPARKLE_KEY_FILE" | |
| SIGNATURE=$("$SPARKLE_BIN" "build/$APP_NAME.zip" --ed-key-file "$SPARKLE_KEY_FILE" 2>&1 | grep "sparkle:edSignature" | sed 's/.*"\(.*\)".*/\1/') | |
| LENGTH=$(stat -f%z "build/$APP_NAME.zip") | |
| rm -f "$SPARKLE_KEY_FILE" |
Sparkle auto-updates