Integrate Sparkle auto-updates for macOS Developer ID builds

New Release-DeveloperID configuration gates Sparkle behind a SPARKLE
compile flag so the App Store Release build stays Sparkle-free. Adds
SPUStandardUpdaterController wrapper, Check for Updates menu command,
Advanced Settings section with beta channel toggle, and a Ruby script
plus GitHub Actions job that signs each release and publishes the
appcast to gh-pages for consumption by Sparkle and Homebrew cask.
This commit is contained in:
Arkadiusz Fal
2026-04-23 04:51:00 +02:00
parent 29c67d3276
commit a2a4691957
14 changed files with 630 additions and 13 deletions

View File

@@ -15,9 +15,16 @@ on:
type: boolean
default: false
build_mac_notarized:
description: 'Build macOS (notarized)'
description: 'Build macOS (notarized Developer ID + Sparkle appcast)'
type: boolean
default: false
default: true
release_channel:
description: 'Sparkle / Developer ID channel (also toggles GitHub prerelease flag)'
type: choice
options:
- beta
- stable
default: beta
create_release:
description: 'Create GitHub release'
type: boolean
@@ -174,15 +181,18 @@ jobs:
- uses: maierj/fastlane-action@v3.0.0
with:
lane: mac build_and_notarize
- run: |
echo "APP_PATH=fastlane/builds/${{ env.VERSION_NUMBER }}-${{ env.BUILD_NUMBER }}/macOS/Yattee.app" >> $GITHUB_ENV
echo "ZIP_PATH=fastlane/builds/${{ env.VERSION_NUMBER }}-${{ env.BUILD_NUMBER }}/macOS/Yattee-${{ env.VERSION_NUMBER }}-macOS.zip" >> $GITHUB_ENV
- name: ZIP build
run: /usr/bin/ditto -c -k --keepParent ${{ env.APP_PATH }} ${{ env.ZIP_PATH }}
- name: Resolve artifact paths
run: |
DIR="fastlane/builds/${{ env.VERSION_NUMBER }}-${{ env.BUILD_NUMBER }}/macOS"
echo "APP_PATH=$DIR/Yattee.app" >> $GITHUB_ENV
echo "ZIP_PATH=$DIR/Yattee-${{ env.VERSION_NUMBER }}-macOS.zip" >> $GITHUB_ENV
echo "DMG_PATH=$DIR/Yattee-${{ env.VERSION_NUMBER }}-macOS.dmg" >> $GITHUB_ENV
- uses: actions/upload-artifact@v4
with:
name: mac-notarized-build
path: ${{ env.ZIP_PATH }}
path: |
${{ env.ZIP_PATH }}
${{ env.DMG_PATH }}
if-no-files-found: error
release:
@@ -192,9 +202,12 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: write
outputs:
tag: ${{ steps.compute_tag.outputs.tag }}
env:
BUILD_NUMBER: ${{ needs.determine_build_number.outputs.build_number }}
VERSION_NUMBER: ${{ needs.determine_build_number.outputs.version_number }}
RELEASE_CHANNEL: ${{ inputs.release_channel }}
steps:
- uses: actions/checkout@v4
with:
@@ -212,14 +225,121 @@ jobs:
- uses: actions/download-artifact@v4
with:
path: artifacts
- name: Compute release tag
id: compute_tag
run: |
if [ "$RELEASE_CHANNEL" = "beta" ]; then
echo "tag=${VERSION_NUMBER}-beta.${BUILD_NUMBER}" >> "$GITHUB_OUTPUT"
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "tag=${VERSION_NUMBER}-${BUILD_NUMBER}" >> "$GITHUB_OUTPUT"
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
- uses: ncipollo/release-action@v1
with:
artifacts: artifacts/**/*.ipa,artifacts/**/*.zip,artifacts/**/*.pkg
artifacts: artifacts/**/*.ipa,artifacts/**/*.zip,artifacts/**/*.pkg,artifacts/**/*.dmg
commit: ${{ github.ref_name }}
tag: ${{ env.VERSION_NUMBER }}-${{ env.BUILD_NUMBER }}
prerelease: true
tag: ${{ steps.compute_tag.outputs.tag }}
prerelease: ${{ steps.compute_tag.outputs.prerelease }}
bodyFile: CHANGELOG.md
publish_appcast:
if: ${{ inputs.build_mac_notarized && inputs.create_release && !cancelled() && !failure() }}
needs: [determine_build_number, mac_notarized, release]
name: Publish Sparkle appcast
runs-on: macos-26
permissions:
contents: write
env:
BUILD_NUMBER: ${{ needs.determine_build_number.outputs.build_number }}
VERSION_NUMBER: ${{ needs.determine_build_number.outputs.version_number }}
RELEASE_CHANNEL: ${{ inputs.release_channel }}
RELEASE_TAG: ${{ needs.release.outputs.tag }}
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
REPO: ${{ github.repository }}
steps:
- name: Guard — secret configured
run: |
if [ -z "$SPARKLE_ED_PRIVATE_KEY" ]; then
echo "::error::SPARKLE_ED_PRIVATE_KEY secret is not set. Configure it with the base64-encoded private key exported via 'generate_keys -x'."
exit 1
fi
- uses: actions/checkout@v4
with:
token: ${{ secrets.REPO_TOKEN }}
- name: Download notarized mac artifact
uses: actions/download-artifact@v4
with:
name: mac-notarized-build
path: mac-artifacts
- name: Locate sign_update binary
id: find_sign_update
run: |
# Sparkle's `sign_update` ships as a package artifact. We need SPM to
# resolve the Sparkle package so the binary is present on disk.
xcodebuild -resolvePackageDependencies -project Yattee.xcodeproj -scheme Yattee >/dev/null
SIGN=$(find "$HOME/Library/Developer/Xcode/DerivedData" -name sign_update -type f 2>/dev/null | head -1)
if [ -z "$SIGN" ]; then
SIGN=$(find ~ -name sign_update -type f 2>/dev/null | head -1)
fi
if [ -z "$SIGN" ]; then
echo "::error::Could not locate sign_update binary"
exit 1
fi
echo "sign_update=$SIGN" >> "$GITHUB_OUTPUT"
- name: Checkout gh-pages (create if missing)
run: |
git fetch origin gh-pages || true
if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then
git worktree add gh-pages origin/gh-pages
else
# First run — create orphan gh-pages with only appcast scaffolding.
git worktree add --detach gh-pages HEAD
cd gh-pages
git checkout --orphan gh-pages
git rm -rf . >/dev/null 2>&1 || true
cp ../scripts/sparkle/appcast_template.xml appcast.xml
cd ..
fi
- name: Write private key to a temp file
id: ed_key
run: |
KEY_FILE=$(mktemp)
printf '%s' "$SPARKLE_ED_PRIVATE_KEY" > "$KEY_FILE"
echo "path=$KEY_FILE" >> "$GITHUB_OUTPUT"
- name: Sign update and update appcast.xml
run: |
ZIP=$(find mac-artifacts -name '*.zip' | head -1)
if [ -z "$ZIP" ]; then
echo "::error::No .zip found in mac-artifacts"
exit 1
fi
./scripts/sparkle/update_appcast.rb \
--zip "$ZIP" \
--version "$VERSION_NUMBER" \
--build "$BUILD_NUMBER" \
--channel "$RELEASE_CHANNEL" \
--tag "$RELEASE_TAG" \
--sign-update-bin "${{ steps.find_sign_update.outputs.sign_update }}" \
--ed-key-file "${{ steps.ed_key.outputs.path }}" \
--appcast gh-pages/appcast.xml \
--repo "$REPO"
- name: Scrub private key
if: always()
run: rm -f "${{ steps.ed_key.outputs.path }}"
- name: Commit & push appcast.xml
run: |
cd gh-pages
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add appcast.xml
if git diff --cached --quiet; then
echo "No appcast changes to publish"
else
git commit -m "Publish Sparkle appcast: ${VERSION_NUMBER} (${BUILD_NUMBER}) [${RELEASE_CHANNEL}]"
git push origin gh-pages
fi
update_altstore:
needs: [release]
uses: ./.github/workflows/update-altstore.yml