ShipHook is a macOS app used to automate signing & deployment of other macOS apps. ShipHook monitors GitHub repositories and then builds, signs & notarises apps. Once notarised, ShipHook utilises the Sparkle framework to push app updates via appcast.xml hosted on GitHub pages. The main use case for ShipHook is to replace the manual step of signing & building apps when multiple collaborators are working on a macOS app. This allows everyone to test, build, and collaborate via git, but only automate the last step when a build is ready for deployment.
ShipHook.xcodeproj: app target and Xcode package configurationShipHook/Sources: SwiftUI app, polling, orchestration, Sparkle updater, and shell executionShipHook/Resources/SampleConfig.json: starter config copied to Application Support on first launchpublish_sparkle_release.sh: reusable Sparkle/appcast publishing scriptDEVELOPER_ID_SETUP.md: how to create and install aDeveloper ID Applicationcertificate
- Poll GitHub for new commits on configured branches.
- Ignore ShipHook-managed appcast commits marked with
[shiphook skip]. - Queue builds so only one repository pipeline runs at a time.
- Sync the local checkout to the latest GitHub branch state without detaching
HEAD. - Detect beta releases from commit markers and route them to a separate beta feed.
- Inspect the target project and plan the next Sparkle-safe build version.
- Build using
xcodebuild archiveor a custom shell command. - Publish the release artifact, appcast, and optional appcast commit push.
- Surface live pipeline phase, status, and log output in the app.
- Multiple repositories in a single config
- Guided add-repository wizard
- Default appcast URL inference using
https://<owner>.github.io/<repo>/appcast.xml - Release build planning against the latest appcast item
- Local signing identity detection
- Per-repo signing overrides
- Live log tailing from
.shiphook/logs/<repo-id>-latest.log - Reset for stale in-progress build state
- Organizer-visible Xcode archive output
- Automatic Sparkle release notes generated from the triggering commit message
- Beta channel support triggered by commit markers
On first launch, ShipHook writes config to:
~/Library/Application Support/ShipHook/config.json
You can manage that config from the ShipHook dashboard instead of editing JSON directly.
Each repository supports:
- GitHub owner, repo, branch
- local checkout path
- optional working directory and release notes path override
xcodeArchivebuild modeshellbuild mode- per-repo environment values
- Sparkle appcast URL and auto-increment build behavior
- signing overrides for team, identity, and sign style
- a publish command fed by ShipHook environment variables
ShipHook injects these environment variables into publish commands:
SHIPHOOK_WORKSPACE_ROOT
SHIPHOOK_REPO_ID
SHIPHOOK_REPO_NAME
SHIPHOOK_GITHUB_OWNER
SHIPHOOK_GITHUB_REPO
SHIPHOOK_BRANCH
SHIPHOOK_SHA
SHIPHOOK_SHORT_SHA
SHIPHOOK_VERSION
SHIPHOOK_RELEASE_CHANNEL
SHIPHOOK_LOCAL_CHECKOUT_PATH
SHIPHOOK_RELEASE_NOTES_PATH
SHIPHOOK_ARTIFACT_PATH
SHIPHOOK_APPCAST_URL
SHIPHOOK_BUNDLED_PUBLISH_SCRIPT
If Release Notes Path Override is empty, ShipHook generates an HTML release-notes page from the commit title and body for the SHA being published, then sets SHIPHOOK_RELEASE_NOTES_PATH to that generated file automatically.
If the commit message contains [beta], [shiphook beta], [pre-release], or [prerelease], ShipHook sets SHIPHOOK_RELEASE_CHANNEL=beta and publishes to the beta channel instead of stable.
bash "$SHIPHOOK_BUNDLED_PUBLISH_SCRIPT" \
--version "$SHIPHOOK_VERSION" \
--artifact "$SHIPHOOK_ARTIFACT_PATH" \
--app-name "ExampleApp" \
--repo-owner "$SHIPHOOK_GITHUB_OWNER" \
--repo-name "$SHIPHOOK_GITHUB_REPO" \
--channel "$SHIPHOOK_RELEASE_CHANNEL" \
--release-notes "$SHIPHOOK_RELEASE_NOTES_PATH" \
--docs-dir "$SHIPHOOK_LOCAL_CHECKOUT_PATH/docs" \
--releases-dir "$SHIPHOOK_LOCAL_CHECKOUT_PATH/release-artifacts" \
--working-dir "$SHIPHOOK_LOCAL_CHECKOUT_PATH"Beta builds publish to:
docs/beta/appcast.xmldocs/beta/release-notes/...- GitHub prereleases tagged like
v1.5.1-beta
publish_sparkle_release.sh can commit and push appcast.xml updates back to the repo. Those commits are tagged like:
chore(shiphook): update appcast for AppName 1.2.3 [shiphook skip]
ShipHook ignores commits containing [shiphook skip] or [skip shiphook], which prevents infinite rebuild loops.
For Sparkle-distributed macOS apps, ShipHook expects Developer ID Application signing for release archives.
Use:
DEVELOPER_ID_SETUP.mdto create and install the certificate- the signing section in the dashboard to pick the local identity
- manual signing overrides when the target project does not already archive correctly on its own
ShipHook itself now includes a Sparkle updater entry point using SPUStandardUpdaterController.
You can trigger it from:
- the app menu:
Check for Updates... - the menu bar extra:
Check for Updates...
ShipHook currently ships with these Sparkle Info.plist-style build settings:
Important:
- ShipHook's own release feed must exist and be signed correctly for Sparkle to work
The updater implementation lives in:
xcodebuild -project /Users/max/Developer/ShipHook/ShipHook.xcodeproj \
-scheme ShipHook \
-configuration Debug \
-derivedDataPath /Users/max/Developer/ShipHook/.build-xcode \
buildShipHook uses the same Sparkle pattern you would use in a normal SwiftUI app:
import Sparkle
@MainActor
final class AppUpdater: ObservableObject {
private let updaterController: SPUStandardUpdaterController?
init() {
updaterController = SPUStandardUpdaterController(
startingUpdater: true,
updaterDelegate: nil,
userDriverDelegate: nil
)
}
func checkForUpdates() {
updaterController?.checkForUpdates(nil)
}
}