Skyformac is distributed as a signed, notarized .dmg via GitHub Releases and a Homebrew Cask —
not through the Mac App Store. That decision (made explicitly, not by default) sidesteps the
two hardest blockers a Store submission would have required: App Sandbox (would need a Documents-
folder migration for existing users, and the ZWO camera SDK's behavior under sandboxing is
untested) and any question of GPLv3's compatibility with the Store's own terms. See
docs/app-store-readiness.md for that earlier investigation — kept for reference, not being
pursued.
License stays GPLv3 throughout; direct distribution outside the Store has no license- compatibility concern the way Store distribution might.
- Enroll in the Apple Developer Program ($99/year) at developer.apple.com if not already enrolled.
- Create a "Developer ID Application" certificate — Xcode → Settings → Accounts → your Apple
ID → Manage Certificates → "+" → "Developer ID Application". This installs the certificate (and
its private key) into your login keychain;
scripts/release.shsigns with whatever certificate of this type it finds there. - Find your Team ID — developer.apple.com → Account → Membership Details. You'll pass this as
SKYFORMAC_TEAM_IDevery time you run a release build (see below); it's not committed anywhere in the repo. - Store notarytool credentials once, so
scripts/release.shdoesn't need them passed in every time:The password is an app-specific password (appleid.apple.com → Sign-In and Security → App-Specific Passwords), not your real Apple ID password. This stores credentials under the keychain profile namexcrun notarytool store-credentials "skyformac-notarize" \ --apple-id "your-apple-id@example.com" \ --team-id "YOUR_TEAM_ID" \ --password "an-app-specific-password"skyformac-notarize, which the script uses by default (override withSKYFORMAC_NOTARY_PROFILEif you name it differently).
- Bump
MARKETING_VERSIONin the Xcode project (skyformac target's Build Settings, both Debug and Release configurations) to the new version number. - Update
CHANGELOG.md— move the[Unreleased]content under a new dated version heading. - Build, sign, and notarize:
This runs
export SKYFORMAC_TEAM_ID=YOUR_TEAM_ID make releasescripts/release.sh, which:- Archives a Release build (
xcodebuild archive) - Exports it signed with your Developer ID Application certificate
- Verifies the signature (
codesign --verify,spctl --assess) - Packages it as
build/release/Skyformac-<version>.dmg(the.appplus an/Applicationssymlink for drag-install) - Submits it to Apple for notarization and waits for the result (
notarytool submit --wait) - Staples the notarization ticket to the
.dmg(stapler staple) so Gatekeeper can verify it offline, without a network round-trip to Apple at first launch - Runs a final Gatekeeper check (
spctl -a -t open)
- Archives a Release build (
- Tag the release and push:
git tag v<version> git push origin v<version> - Create the GitHub Release for that tag (
gh release create v<version> build/release/Skyformac-<version>.dmg --title "..." --notes-file ..., or via the web UI) and attach the.dmgas a release asset. - Update the Homebrew Cask (
Casks/skyformac.rb) with the newversionand the.dmg's real SHA-256 (shasum -a 256 build/release/Skyformac-<version>.dmg), then copy that same file into the tap repo (see below) and push it there too.
Every release through v0.5.2 was actually built this way, not via scripts/release.sh above —
there's no Developer ID Application certificate or notarytool profile set up. The app is still
usable (ad-hoc signed, matching what "Fix Gatekeeper Warning.command" is for), but building this
way instead of via xcodebuild archive hits two real gotchas worth knowing about:
- Strip the XCTest/Testing frameworks before signing/packaging.
xcodebuild build -scheme skyformacbuilds every target in the shared scheme, and theskyformacapp target ends up withTesting.framework,XCTest.framework,XCUIAutomation.framework, and several more Apple testing frameworks embedded inContents/Frameworks— dead weight the main executable never links against (confirmed viaotool -L— none of them show up), roughly doubling the shipped app's size. Remove everything inContents/FrameworksexceptlibASICamera2.dylibbefore signing. - Use
zip, notditto -c -k, to build the.zipasset.ditto's zip archiver scatters AppleDouble resource-fork sidecar files (._Info.plist,._skyformac, ...) inside the app bundle's own directory tree, not just alongside it — extracting that zip elsewhere and runningcodesign --verifyon the result fails with "a sealed resource is missing or invalid" (Gatekeeper would refuse to launch it too), because those extra files aren't in the bundle's sealed resource manifest.hdiutil create -srcfolderfor the.dmgdoesn't have this problem — only the zip path does. Confirmed by actually extracting the built zip and runningcodesign --verify --deep --strictagainst the result, not just checking the pre-zip.app. - The
.zipneedsFix Gatekeeper Warning.commandcopied in too, same as the.dmg— it's easy to package just the.appalone by mistake (this shipped broken in the v0.5.0–v0.5.1.ziprelease assets: no script at all, so the README's own zip instructions had nothing to run).
Roughly:
xcodebuild build -scheme skyformac -configuration Release -destination 'platform=macOS' \
CONFIGURATION_BUILD_DIR="$PWD/build/release-app"
APP="build/release-app/skyformac.app"
rm -rf "$APP/Contents/PlugIns"
# Strip everything except libASICamera2.dylib from Contents/Frameworks (see above)
codesign --force --deep --sign - "$APP"
codesign --verify --deep --strict "$APP"
mkdir -p build/zip-staging && cp -R "$APP" build/zip-staging/ && cp "scripts/Fix Gatekeeper Warning.command" build/zip-staging/
(cd build/zip-staging && zip -r -X -y "../release-assets/skyformac-v<version>-macOS.zip" skyformac.app "Fix Gatekeeper Warning.command")
mkdir -p build/dmg-staging && cp -R "$APP" build/dmg-staging/ && ln -s /Applications build/dmg-staging/Applications && cp "scripts/Fix Gatekeeper Warning.command" build/dmg-staging/
hdiutil create -volname "Skyformac <version>" -srcfolder build/dmg-staging -ov -format UDZO build/release-assets/skyformac-v<version>-macOS.dmg
Before uploading either asset, extract it fresh into a scratch directory and run codesign --verify --deep --strict on the result — checking the pre-packaging .app isn't enough, since
packaging itself is exactly what can break the seal.
Casks/skyformac.rb in this repo is kept in sync with the live formula, not read by Homebrew
directly — the actual tap Homebrew installs from is
giulioroggero/homebrew-skyformac. After
updating the version/SHA-256 here, copy the same file to that repo's own Casks/skyformac.rb and
push it there; brew tap giulioroggero/skyformac / brew install --cask skyformac won't see a
new version until that copy lands.
Signing/notarization credentials never leave your machine — there's no GitHub Actions automation
for this (a deliberate choice: storing a code-signing certificate and notarization password as CI
secrets is a real security tradeoff, and this project has a comfortable "release when ready, on
the maintainer's own machine" cadence that doesn't need it). CI (.github/workflows/ci.yml) only
ever builds a debug, ad-hoc-signed build for running the test suite — it has nothing to do with
producing a real release artifact.