Sugarfree ships on two independent release tracks — they never share a tag or a GitHub release:
| Track | Tag prefix | Produced by | Assets |
|---|---|---|---|
| macOS app | app-v<version> |
./release.sh (local, notarized) |
Sugarfree-<version>.dmg |
| Cross-platform CLI | cli-v<version> |
.github/workflows/release.yml (CI) |
sugarfree-<version>-<platform> tarballs/zip + .sha256 |
The two are still independent tracks — separate tags, separate GitHub releases,
separate build pipelines — but as of 1.5.1 they share a single version number:
bump both together and release them at the same version (app-v<ver> + cli-v<ver>).
The app DMG still bundles a copy of the CLI inside it, but the standalone CLI binaries
are a separate, CI-built release. The rest of this doc covers the macOS app track;
the CLI track is just "push a cli-v* tag" (see the bottom).
How to cut a signed, notarized .dmg and publish it. The whole pipeline is one
command — ./release.sh — once the one-time setup below is in place.
All of this is done once per machine; none of it is in the repo.
- Apple Developer Program membership (paid — required for Developer ID + notarization).
- Developer ID Application certificate in your login keychain
(Xcode → Settings → Accounts → Manage Certificates → + → Developer ID Application).
Verify with:
security find-identity -v -p codesigning | grep "Developer ID Application"
- App Store Connect API key for notarization (passwordless). Create at
App Store Connect → Users and Access → Integrations → App Store Connect API
(role: Developer), then store the pieces in a directory — default
~/.secrets/notarytool/:Point elsewhere withAuthKey_XXXXXXXXXX.p8 # the downloaded key; key ID is read from the filename issuer_id # one line: the Issuer ID (UUID) team_id # one line: your 10-char Team IDNOTARY_KEY_DIR=/path ./release.sh. - Tooling:
brew install xcodegen create-dmg.
The release script needs no
LocalSigning.xcconfig— it passesDEVELOPMENT_TEAM(fromteam_id) toxcodebuildand signs with the keychain cert directly.
-
Bump the version in all places — the app and CLI share one version number, so bump them together:
Configs/Base.xcconfig→MARKETING_VERSION(and bumpCURRENT_PROJECT_VERSION)project.yml→MARKETING_VERSION/CURRENT_PROJECT_VERSIONSources/sugarfree/Sugarfree.swift→CommandConfiguration(version:)(CLI)
-
Build the notarized DMG:
./release.sh
This generates the project, archives the Release config (Developer ID signed, hardened runtime), exports the app, builds the universal
sugarfreeCLI and embeds it in the app bundle (Contents/Resources/sugarfree, signed; the app is re-signed to reseal its resources), packages a styled DMG withcreate-dmg, submits it to Apple's notary service (notarytool --wait), then staples and validates the ticket. Output:dist/Sugarfree-<version>.dmg.Because the CLI ships inside the app, a DMG install also provides the command line: the app symlinks it to
/usr/local/bin/sugarfreeon first launch (CLIInstaller, prompting for admin only if that directory isn't writable). Standalone CLI binaries for Linux/Windows (and a macOS-only tarball) are a separate track — pushed ascli-v*tags (see below), never mixed into the app'sapp-v*release.
# Ticket is attached to the DMG:
xcrun stapler validate dist/Sugarfree-<version>.dmg
# The app inside assesses as notarized (mount the DMG first):
spctl -a -t exec -vv "/Volumes/Sugarfree <version>/Sugarfree.app"
# expect: source=Notarized Developer IDNote: spctl -t open on the DMG reports "no usable signature" — that's expected
(the DMG isn't code-signed; it carries a stapled notarization ticket). The
authoritative checks are stapler validate and the app's Notarized Developer ID
assessment above.
gh release upload app-v<version> dist/Sugarfree-<version>.dmg --clobber
# or, for a brand-new tag:
git tag app-v<version> && git push origin app-v<version>
gh release create app-v<version> dist/Sugarfree-<version>.dmg \
--title "Sugarfree <version>" --generate-notesThe CLI is built entirely by CI — no local toolchain dance. Just push a cli-v* tag
and .github/workflows/release.yml builds macOS-universal, Linux x86_64/arm64, and
Windows, then attaches the archives (+ .sha256) to a cli-v<version> GitHub release:
# bump version: Sources/sugarfree/Sugarfree.swift (CommandConfiguration version:)
git tag cli-v<version> && git push origin cli-v<version>Watch it: gh run watch "$(gh run list --workflow=release.yml -L1 --json databaseId -q '.[0].databaseId')".
Windows is continue-on-error (the least-stable leg), so it never blocks the release.
- Notarization rejected — get the details with the submission ID printed by the run:
xcrun notarytool log <submission-id> \ --key ~/.secrets/notarytool/AuthKey_*.p8 \ --key-id <KEYID> --issuer "$(cat ~/.secrets/notarytool/issuer_id)"
- No Developer ID cert — the script stops early; (re)create it via step 2 above.
create-dmgnot found —brew install create-dmg.