Skip to content

Latest commit

 

History

History
214 lines (161 loc) · 6.87 KB

File metadata and controls

214 lines (161 loc) · 6.87 KB

Snapzy Release Workflow

Prerequisites

  1. EdDSA private key for Sparkle (SPARKLE_PRIVATE_KEY secret)
  2. One of the following signing strategies (in priority order):
Strategy Required Secrets Notarization
Developer ID DEVELOPER_ID_P12, DEVELOPER_ID_PASSWORD Yes (with APPLE_ID, APPLE_ID_PASSWORD, APPLE_TEAM_ID)
Self-signed cert SELF_SIGNED_CERT_P12, SELF_SIGNED_CERT_PASSWORD No
Ad-hoc ALLOW_ADHOC_RELEASE=true (repo variable) No

CI Signing Architecture

The release workflow uses manual codesign for all signing strategies. The binary is extracted from the Xcode archive via ditto and signed component-by-component (inside-out for Sparkle framework).

  • Developer ID builds use --timestamp (secure timestamp from Apple) and hardened runtime (-o runtime) — both required for notarization.
  • Self-signed and ad-hoc builds use --timestamp=none (no Apple server access needed) but still enable hardened runtime.

Release Steps

1. Build & Archive

# In Xcode:
# Product > Archive > Distribute App > Developer ID
# Wait for notarization to complete

2. Create Update Archive

# Navigate to exported app location
cd /path/to/exported

# Create ZIP archive (preserves code signature)
zip -r ~/Snapzy-Updates/Snapzy-X.Y.Z.zip Snapzy.app

# Optional: Create release notes HTML
cat > ~/Snapzy-Updates/Snapzy-X.Y.Z.html << 'EOF'
<html>
<body>
<h2>What's New in X.Y.Z</h2>
<ul>
  <li>Feature 1</li>
  <li>Bug fix 2</li>
</ul>
</body>
</html>
EOF

3. Generate Appcast

# Locate Sparkle tools
SPARKLE_BIN=~/Library/Developer/Xcode/DerivedData/Snapzy-*/SourcePackages/artifacts/sparkle/Sparkle/bin

# Generate appcast (auto-signs and creates deltas)
$SPARKLE_BIN/generate_appcast ~/Snapzy-Updates

# Output:
# - appcast.xml (updated)
# - *.delta files (for incremental updates)

4. Upload to GitHub Releases

# Create and push tag
git tag -a vX.Y.Z -m "Version X.Y.Z"
git push origin vX.Y.Z

# Create release with assets
gh release create vX.Y.Z \
  ~/Snapzy-Updates/Snapzy-X.Y.Z.zip \
  ~/Snapzy-Updates/Snapzy-X.Y.Z.html \
  --title "Snapzy X.Y.Z" \
  --notes "See release notes for details"

# Upload appcast.xml to repo root or GitHub Pages
cp ~/Snapzy-Updates/appcast.xml ./appcast.xml
git add appcast.xml
git commit -m "chore: update appcast for vX.Y.Z"
git push

Key Management

Backup Private Key

$SPARKLE_BIN/generate_keys -x sparkle_private_key.pem
# Store securely (password manager, encrypted backup)
# NEVER commit to git!

Restore on New Machine

$SPARKLE_BIN/generate_keys -f sparkle_private_key.pem

View Public Key

$SPARKLE_BIN/generate_keys -p

SUFeedURL Configuration

Current URL in Info.plist:

https://raw.githubusercontent.com/duongductrong/Snapzy/master/appcast.xml

Update this value in Snapzy/Resources/Info.plist if your release repository changes.

Testing Updates

# Clear last check time to force update check
defaults delete com.trongduong.snapzy SULastCheckTime

# Run app and click "Check for Updates..."

Distribution Tiers

Developer ID (recommended)

  • Signed with Apple Developer ID certificate
  • Hardened runtime enabled
  • Notarized by Apple (if credentials configured)
  • Users can install without Gatekeeper warnings

Self-signed Certificate

  • Preserves TCC permissions across Sparkle updates
  • Not notarized — users must right-click > Open on first launch
  • Move the app to /Applications before first launch

Ad-hoc (emergency fallback)

  • Requires ALLOW_ADHOC_RELEASE=true repository variable
  • TCC permissions and Keychain trust lost after every update
  • Not suitable for regular distribution

Release Notifications

Release notifications are handled by a separate workflow (release-notify.yml) that triggers automatically after release-publish.yml completes successfully. This keeps the publish workflow focused on build/sign/release, and makes it easy to add new notification channels.

Architecture:

release-publish.yml (build → sign → release)
        ↓ workflow_run trigger
release-notify.yml
  ├── prepare  (fetch release metadata from GitHub API)
  ├── discord  (parallel)
  ├── slack    (parallel, add when needed)
  └── telegram (parallel, add when needed)

Discord

1. Create a Discord Webhook

  1. Open your Discord server
  2. Go to Server Settings → Integrations → Webhooks
  3. Click New Webhook
  4. Choose the target channel for release announcements
  5. (Optional) Set the webhook name (e.g., "Snapzy Releases") and avatar
  6. Click Copy Webhook URL — it looks like:
    https://discord.com/api/webhooks/123456789012345678/abcdefg...
    

2. Add the Secret to GitHub

  1. Go to your GitHub repository → Settings → Secrets and variables → Actions
  2. Click New repository secret
  3. Name: DISCORD_WEBHOOK_URL
  4. Value: paste the webhook URL from step 1
  5. Click Add secret

What Gets Posted

Each release notification includes:

  • Title: version number with link to the GitHub release page
  • Body: full changelog from CHANGELOG.md (features, bug fixes, etc.)
  • Quick links: DMG download and release page
  • Timestamp: when the release was published

If DISCORD_WEBHOOK_URL is not configured, the job is silently skipped — no failures.

Adding a New Channel

To add a notification channel (e.g., Slack, Telegram):

  1. Open .github/workflows/release-notify.yml
  2. Add a new job that depends on prepare
  3. Use ${{ needs.prepare.outputs.version }}, .release_url, .download_url, and .body for release data
  4. Add the required secrets (e.g., SLACK_WEBHOOK_URL) to GitHub repository settings

See the commented examples at the bottom of release-notify.yml.

Troubleshooting

  1. Button always disabled: Check Info.plist has SUFeedURL and SUPublicEDKey
  2. Signature errors: Ensure private key matches public key in app
  3. No updates found: Verify appcast.xml sparkle:version > current CFBundleVersion
  4. Notification not sent: Verify the channel secret (e.g., DISCORD_WEBHOOK_URL) is set correctly in GitHub repository settings. Check the release-notify workflow run logs for HTTP status warnings.
  5. Notarization rejected: Check the notarization log in the GitHub Actions output (printed automatically on failure). Common causes:
    • Missing hardened runtime (flags= line doesn't show runtime)
    • Missing secure timestamp (--timestamp=none was used)
    • com.apple.security.get-task-allow entitlement present in release build
  6. Notarization timeout: Apple service can be slow. The workflow uses a 15-minute timeout. If consistently timing out, check DMG size and Apple system status.
  7. Stapling failed: Ensure the DMG was notarized successfully first. stapler staple only works after Apple issues a ticket.