SwiftPM-only; package/sign/notarize manually (no Xcode project). Sparkle feed is served from GitHub Releases. Checklist below merges Trimmy’s release flow with Runic specifics.
Reference first: follow this Runic-specific guide as the source of truth for release steps.
Expectations
- When someone says “release Runic”, do the entire end-to-end flow: bump versions/CHANGELOG, build, sign and notarize, upload the zip to the GitHub release, generate/update the appcast with the new signature, publish the tag/release, and verify the enclosure URL responds with 200/OK and installs via Sparkle (no 404s or stale feeds).
Release automation notes (Scripts/release.sh)
- Always forces a fresh build/notarization (no cached artifacts) before publishing.
- Fails fast if: git tree is dirty, the top changelog section is still “Unreleased” or mismatched, the target version already exists in the appcast, or the build number is not greater than the latest appcast entry.
- Sparkle key probe runs before format/lint/test/build/notarization; appcast entry + signature verified automatically after generation.
- Release notes are extracted directly from the current changelog section and passed to the GitHub release (no manual notes flag needed).
- Sparkle appcast notes are generated as HTML from the same changelog section and embedded into the appcast entry.
- After publishing signed assets, the script opens a separate appcast PR. Merge that PR and verify the live feed before calling the release complete.
- Uses in-repo helpers (
Scripts/release-lib.sh) so release checks no longer depend on external/private script paths. - Requires tools/env on PATH:
swiftformat,swiftlint,swift,sign_update,generate_appcast,gh,python3,zip,curl, plusAPP_STORE_CONNECT_*andSPARKLE_PRIVATE_KEY_FILE.
Prereqs
- Xcode 26+ installed at
/Applications/Xcode.app(for ictool/iconutil and SDKs). - Developer ID Application cert installed (e.g.
Developer ID Application: YOUR_NAME (TEAMID)). - ASC API creds in env:
APP_STORE_CONNECT_API_KEY_P8,APP_STORE_CONNECT_KEY_ID,APP_STORE_CONNECT_ISSUER_ID. - Sparkle keys: public key already in Info.plist; private key path set via
SPARKLE_PRIVATE_KEY_FILEwhen generating appcast. - Ensure shell has release env vars loaded (usually
source ~/.profile) before runningScripts/release.sh.
Icon (glass .icon → .icns)
./Scripts/build_icon.sh Icon.icon Runic
Uses Xcode’s ictool + transparent padding + iconset → Icon.icns.
Build, sign, notarize (universal: arm64 + x86_64)
./Scripts/sign-and-notarize.sh
What it does:
swift build -c release --arch arm64andswift build -c release --arch x86_64- Packages
Runic.appwith Info.plist and Icon.icns - Embeds Sparkle.framework, Updater, Autoupdate, XPCs
- Codesigns everything with runtime + timestamp (deep) and adds rpath
- Zips to
Runic-<version>.zip - Submits to notarytool, waits, staples, validates
Gotchas fixed:
- Sparkle needs signing for framework, Autoupdate, Updater, XPCs (Downloader/Installer) or notarization fails.
- Use
--timestampand--deepwhen signing the app to avoid invalid signature errors. - Avoid
unzip— it can add AppleDouble._*files that break the sealed signature and trigger “app is damaged”. Use Finder orditto -x -k Runic-<ver>.zip /Applications. If Gatekeeper complains, delete the app bundle, re-extract withditto, thenspctl -a -t execto verify. - Manual sanity check before uploading:
find Runic.app -name '._*'should return nothing; thenspctl --assess --type execute --verbose Runic.appandcodesign --verify --deep --strict --verbose Runic.appshould both pass on the packaged bundle.
Appcast (Sparkle)
After notarization:
SPARKLE_PRIVATE_KEY_FILE=/path/to/ed25519-priv.key \
./Scripts/make_appcast.sh Runic-0.1.0.zip \
https://raw.githubusercontent.com/sriinnu/Runic/main/appcast.xml
Generates HTML release notes from CHANGELOG.md (via Scripts/changelog-to-html.sh) and embeds them into the appcast entry.
The appcast feed is the committed appcast.xml on main; the GitHub release carries the app zip and dSYM zip assets.
Tag & release
git tag --sign v<version> --message "Runic <version>"
./Scripts/make_appcast.sh ...
# create the release as a draft with the zip, dSYM zip and deltas, then publish it
# (published releases are immutable)
# open a PR with appcast.xml; it lands on main after CI passes
# approve the staged npm version (publishing the release stages it)
Homebrew (Cask)
Runic ships a Homebrew Cask in ../homebrew-tap. When installed via Homebrew, Runic disables Sparkle and the app
must be updated via brew.
After publishing the GitHub release, update the tap cask (see docs/releasing-homebrew.md).
Checklist (quick)
- Read this file end-to-end before starting.
- Update versions (scripts/Info.plist, CHANGELOG, About text) — changelog top section must be finalized; release script pulls notes from it automatically.
-
swiftformat Sources Tests --lint,swiftlint --strict,swift test(zero warnings/errors) -
./Scripts/build_icon.shif icon changed -
./Scripts/sign-and-notarize.sh - Generate Sparkle appcast with private key
- Set
SPARKLE_PRIVATE_KEY_FILEto the local ed25519 private key path. Do not commit or document the real private key location. - Upload the dSYM archive alongside the app zip on the GitHub release; the release script now automates this and will fail if it’s missing.
- After publishing the release, run
Scripts/check-release-assets.sh <tag>to confirm both the app zip and dSYM zip are present on GitHub. - Generate the appcast + HTML release notes:
./Scripts/make_appcast.sh Runic-<ver>.zip https://raw.githubusercontent.com/sriinnu/Runic/main/appcast.xml - Verify the enclosure signature + size:
SPARKLE_PRIVATE_KEY_FILE=... ./Scripts/verify_appcast.sh <ver>
- Set
- Upload the zip + dSYM zip to GitHub Releases; commit and push the generated
appcast.xmltomainso the Sparkle feed is live - Homebrew tap: update
../homebrew-tap/Casks/runic.rb(url + sha256), then verify:brew uninstall --cask runic || truebrew untap sriinnu/tap || true; brew tap sriinnu/tapbrew install --cask sriinnu/tap/runic && open -a Runic
- Version continuity: confirm the new version is the immediate next patch/minor (no gaps) and CHANGELOG has no skipped numbers (e.g., after 0.2.0 use 0.2.1, not 0.2.2)
- Changelog sanity: single top-level title, no duplicate version sections, versions strictly descending with no repeats
- Release pages: title format
Runic <version>, notes as Markdown list (no stray blank lines) - Changelog/release notes are user-facing: avoid internal-only bullets (build numbers, script bumps) and keep entries concise
- Download uploaded
Runic-<ver>.zip, unzip viaditto, run, and verify signature (spctl -a -t exec -vv Runic.app+stapler validate) - Confirm
appcast.xmlpoints to the new zip/version and renders the HTML release notes (not escaped tags) - Verify on GitHub Releases: assets present (zip, dSYM zip), release notes match changelog, version/tag correct
- Open the appcast URL in browser to confirm the new entry is visible and enclosure URL is reachable
- Manually visit the enclosure URL (curl -I) to ensure 200/OK (no 404) after publishing assets/release
- Ensure
sparkle:edSignatureis present for the enclosure in appcast (generated bygenerate_appcastwith the ed25519 key) - When creating the GitHub release, paste the CHANGELOG entry as Markdown list (one
-per line, blank line between sections); visually confirm bullets render correctly after publishing - Keep a previous signed build in
/Applications/Runic.appto test Sparkle delta/full update to the new release - Manual Gatekeeper sanity: after packaging,
find Runic.app -name '._*'is empty,spctl --assess --type execute --verbose Runic.appandcodesign --verify --deep --strict --verbose Runic.appsucceed - For Sparkle verification: if replacing
/Applications/Runic.app, quit first, replace, relaunch, and test update - Definition of “done” for a release: all of the above are complete, the appcast/enclosure link resolves, Homebrew cask installs, and a previous public build can update to the new one via Sparkle. Anything short of that is not a finished release.
Troubleshooting
- White plate icon: regenerate icns via
build_icon.sh(ictool) to ensure transparent padding. - Notarization invalid: verify deep+timestamp signing, especially Sparkle’s Autoupdate/Updater and XPCs; rerun package + sign-and-notarize.
- App won’t launch: ensure Sparkle.framework is embedded under
Contents/Frameworksand rpath added; codesign deep. - App “damaged” dialog after unzip: re-extract with
ditto -x -k, removing any._*files, then re-verify withspctl. - Update download fails (404): ensure the release asset referenced in appcast exists and is published in the corresponding GitHub release; verify with
curl -I <enclosure-url>.
Read the source ↗ Checked against Runic 2.13.5 · 9730f91