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
Updates AutoBrew’s release configuration to enable Sparkle-based in-app updates for production builds by adding signing metadata, enabling the updater, and generating a signed appcast during CI releases.
Changes:
- Bump
MARKETING_VERSION/CURRENT_PROJECT_VERSIONfor the next release. - Enable Sparkle updater startup and add
SUPublicEDKeyto support signed appcasts. - Extend the GitHub Actions release workflow to sign the update ZIP and generate/push an
appcast.xmlentry.
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 release. |
Sources/Services/UpdaterService.swift |
Starts Sparkle updater automatically. |
AutoBrew/Info.plist |
Adds Sparkle public key for EdDSA verification. |
.github/workflows/build-and-release.yml |
Signs Sparkle update and generates/pushes appcast.xml during main releases. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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 |
There was a problem hiding this comment.
The Sparkle tools download URL mixes releases/latest with a hard-coded asset name (Sparkle-2.9.0.tar.xz). When Sparkle releases a newer version, latest will no longer have a Sparkle-2.9.0.tar.xz asset and this step will start failing. Use a tag-pinned URL (e.g., /releases/download/<tag>/Sparkle-<tag>.tar.xz) or fetch the actual latest asset name dynamically.
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/latest/download/Sparkle-2.9.0.tar.xz" | tar xJ -C /tmp | |
| curl -sL "https://github.com/sparkle-project/Sparkle/releases/download/2.9.0/Sparkle-2.9.0.tar.xz" | tar xJ -C /tmp |
| # 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 SIGNATURE=$( ... | grep ... | sed ...) pipeline can silently produce an empty signature if sign_update fails or grep finds no match, because set -e doesn’t fail on errors inside pipelines unless pipefail is enabled. Add set -o pipefail (or set -euo pipefail) and explicitly validate SIGNATURE is non-empty before writing it to $GITHUB_ENV.
| git commit -m "Update appcast for v$VERSION" || true | ||
| git push origin HEAD:main || true | ||
|
|
There was a problem hiding this comment.
The appcast is committed/pushed with || true, which can hide real failures (e.g., branch protections, auth issues) and result in releasing a build without updating the feed. Consider letting these commands fail the job (remove || true) or at least detect failure and exit 1 so releases don’t proceed with a stale appcast.
| git commit -m "Update appcast for v$VERSION" || true | |
| git push origin HEAD:main || true | |
| if git diff --cached --quiet; then | |
| echo "No changes to appcast.xml; skipping commit and push." | |
| else | |
| git commit -m "Update appcast for v$VERSION" | |
| git push origin HEAD:main | |
| fi |
| - name: Update appcast.xml | ||
| if: github.ref == 'refs/heads/main' | ||
| env: | ||
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
| VERSION: ${{ steps.version.outputs.version }} | ||
| BUILD: ${{ steps.version.outputs.build }} | ||
| run: | | ||
| set -e | ||
| cat > appcast.xml << EOF | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle" xmlns:dc="http://purl.org/dc/elements/1.1/"> | ||
| <channel> | ||
| <title>AutoBrew</title> | ||
| <link>https://github.com/marcelrgberger/auto-brew</link> | ||
| <description>AutoBrew update feed</description> | ||
| <language>en</language> | ||
| <item> | ||
| <title>Version $VERSION</title> | ||
| <pubDate>$(date -R)</pubDate> | ||
| <sparkle:version>$BUILD</sparkle:version> | ||
| <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" | ||
| length="$ZIP_LENGTH" | ||
| type="application/octet-stream" | ||
| sparkle:edSignature="$SPARKLE_SIGNATURE" | ||
| /> | ||
| </item> | ||
| </channel> | ||
| </rss> | ||
| EOF |
There was a problem hiding this comment.
Update appcast.xml runs before the GitHub Release is created/uploaded. If the release step fails, the workflow will still have pushed an appcast entry pointing at a (potentially) non-existent v$VERSION/AutoBrew.zip, breaking update checks. Reorder so the release is successfully created (and assets uploaded) before updating/pushing appcast.xml, or gate the appcast update on release success.
| 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 and only removed after signing. If the step exits early (e.g., sign_update fails once pipefail is enabled), the key file may remain on disk for the rest of the job. Use a trap to delete it on exit and consider storing it under $RUNNER_TEMP with restrictive permissions (e.g., chmod 600).
| 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="${RUNNER_TEMP:-/tmp}/sparkle_key" | |
| trap 'rm -f "$SPARKLE_KEY_FILE"' EXIT | |
| printf '%s\n' "$SPARKLE_KEY" > "$SPARKLE_KEY_FILE" | |
| chmod 600 "$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") |
.