Skip to content

Auto-Update & Build Distribution via Sparkle 2 + GitHub Actions #1

@omarshahine

Description

@omarshahine

Auto-Update & Build Distribution for ShellCraft

Context

ShellCraft is a native macOS SwiftUI app with no existing update mechanism or CI/CD pipeline. The goal is to add Sparkle 2 for auto-updates, distributed via GitHub Releases, with a full GitHub Actions workflow for building, signing, notarizing, and publishing releases on tag push.

Overview

Three components:

  1. Sparkle 2 integration — SPM dependency, updater controller, "Check for Updates" menu item
  2. GitHub Actions workflow — Build, sign, notarize, create DMG, generate appcast, publish release
  3. One-time setup instructions — EdDSA key generation, GitHub secrets, Developer ID certificate export

1. Add Sparkle 2 via SPM in project.yml

File: project.yml

  • Add packages section with Sparkle from https://github.com/sparkle-project/Sparkle, version from: 2.8.1
  • Add - package: Sparkle to the ShellCraft target's dependencies
  • Add Info.plist keys via INFOPLIST_KEY_ build settings:
    • SUFeedURLhttps://github.com/omarshahine/ShellCraft/releases/latest/download/appcast.xml
    • SUPublicEDKey → placeholder (filled after key generation)
  • Set DEVELOPMENT_TEAM to N9DRSTM2U6 (already in the generated xcodeproj)
  • Run xcodegen generate + the icon sed fix afterward

2. Add Sparkle Updater to the App

File: ShellCraft/App/ShellCraftApp.swift

  • Import Sparkle
  • Create SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) as a property
  • Add a CommandGroup(after: .appInfo) with a "Check for Updates..." button wired to updaterController.updater.checkForUpdates()
  • Use a small helper view CheckForUpdatesView that observes updater.canCheckForUpdates via Combine publisher

New file: ShellCraft/Views/Shared/CheckForUpdatesView.swift

A small SwiftUI view + observable class:

import SwiftUI
import Sparkle

struct CheckForUpdatesView: View {
    @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel

    init(updater: SPUUpdater) {
        self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater)
    }

    var body: some View {
        Button("Check for Updates...", action: checkForUpdatesViewModel.checkForUpdates)
            .disabled(!checkForUpdatesViewModel.canCheckForUpdates)
    }
}

This follows Sparkle's official SwiftUI integration pattern.

3. Update Entitlements (if needed)

File: ShellCraft/ShellCraft.entitlements

ShellCraft is not sandboxed, so no special entitlements are needed for Sparkle. The entitlements file stays empty (or add com.apple.security.network.client if we want to be explicit, but it's not required for non-sandboxed apps).

4. GitHub Actions Release Workflow

New file: .github/workflows/release.yml

Triggered on: push with tags matching v*

Steps:

  1. Checkout code
  2. Install XcodeGen via Homebrew
  3. Run xcodegen generate + icon sed fix
  4. Import Developer ID certificate from MACOS_CERTIFICATE secret (base64 .p12)
  5. Build with xcodebuild in Release configuration, signed with Developer ID
  6. Create DMG using hdiutil create (simple, no extra tools needed)
  7. Notarize the DMG with xcrun notarytool submit + wait for completion
  8. Staple notarization ticket with xcrun stapler staple
  9. Sign DMG with Sparkle EdDSA using Sparkle's sign_update tool
  10. Generate appcast.xml with version info and EdDSA signature
  11. Create GitHub Release using softprops/action-gh-release with DMG + appcast.xml

5. Claude GitHub App Workflow

New file: .github/workflows/claude.yml

Standard @claude mention workflow, same as other repos. Copy from ~/GitHub/Claude/.github/workflows/claude.yml.

6. GitHub Secrets Needed

Secret Purpose
MACOS_CERTIFICATE Base64-encoded Developer ID Application .p12
MACOS_CERTIFICATE_PWD Password for the .p12
APPLE_ID Apple ID email for notarization
APPLE_APP_PASSWORD App-specific password for notarytool
APPLE_TEAM_ID Team ID (N9DRSTM2U6)
SPARKLE_PRIVATE_KEY EdDSA private key for Sparkle signing

7. One-Time Setup Steps (Manual)

  1. Generate Sparkle EdDSA keys — run generate_keys from Sparkle, copy public key into project.yml, store private key as GitHub secret
  2. Export Developer ID certificate — from Keychain Access, export as .p12, base64-encode, store as GitHub secret
  3. Create app-specific password at appleid.apple.com for notarytool
  4. Set GitHub secrets via gh secret set

Files Modified/Created

File Action
project.yml Modify — add Sparkle SPM package, dependency, Info.plist keys, team ID
ShellCraft/App/ShellCraftApp.swift Modify — add Sparkle import, updater controller, menu command
ShellCraft/Views/Shared/CheckForUpdatesView.swift Create — SwiftUI "Check for Updates" button + observable
.github/workflows/release.yml Create — full CI/CD build+sign+notarize+release pipeline
.github/workflows/claude.yml Create — @claude mention support

Verification

  1. Build locally: xcodegen generate, then xcodebuild — verify Sparkle resolves and app compiles
  2. Launch app: Verify "Check for Updates..." appears in the ShellCraft menu
  3. Menu state: The button should be enabled after the updater initializes
  4. CI dry run: Push a v0.1.0 tag to trigger the workflow (will need secrets configured first)
  5. End-to-end: After first release is published, build a second release and verify the app detects and offers the update

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions