Release Workflow
June 11, 2026 · View on GitHub
This document describes how to cut a release of the Whisky app and how to publish a new Wine Libraries archive.
The fork uses two parallel artifact streams:
- App releases (
app-vX.Y.Z) —Whisky-X.Y.Z.dmg, signed and notarized for direct distribution. - Wine Libraries releases (
vX.Y.Z) —Libraries.tar.gzcontaining the Wine/DXVK runtime that the app downloads on first launch.
Both live on GitHub Releases. Static metadata (version plist, Sparkle appcast) is served from GitHub Pages, which is workflow-deployed through .github/workflows/Documentation.yml. The gh-pages branch is unused; static files go in dist/pages/.
One-time setup
These only need to be done once per maintainer machine.
Apple Developer ID Application certificate
A Developer ID Application certificate is required to ship a Gatekeeper-friendly DMG. Apple Development and Apple Distribution certs are not sufficient.
- Xcode → Settings → Accounts → select your Apple ID → Manage Certificates…
- Click + → Developer ID Application
- The cert is installed in your login keychain automatically.
notarytool credentials
Apple's notary service needs an app-specific password.
- Generate one at https://appleid.apple.com → Sign-In and Security → App-Specific Passwords.
- Store it as a notarytool keychain profile:
xcrun notarytool store-credentials AC_PASSWORD \ --apple-id <your-apple-id-email> \ --team-id Z7JS58F8U3 \ --password <app-specific-password> - The release script reads this profile by name (
AC_PASSWORD).
Sparkle EdDSA keys
A keypair is needed to sign appcast entries. Sparkle's generate_keys tool ships with the Sparkle SPM package; after building Whisky once, find it at:
~/Library/Developer/Xcode/DerivedData/Whisky-*/SourcePackages/artifacts/sparkle/Sparkle/bin/generate_keys
Run it once. The private key is stored automatically in your login keychain. The public key is printed to stdout — it is already committed in Whisky/Info.plist under SUPublicEDKey. If you regenerate keys you will invalidate the existing public key in the bundled app and need to re-release.
Credential continuity (backup & recovery)
Three secrets gate the release pipeline. Losing the Sparkle key is unrecoverable for existing installs: SUPublicEDKey is baked into every shipped Info.plist, so a regenerated key permanently strands all installed copies off auto-update. Keep current, restore-tested backups of all three. (A fourth credential, the BREW_TOKEN repository secret used by the release step that publishes to frankea/homebrew-whisky, is regenerable and so needs no backup — the three-secrets focus here is on the non-regenerable ones.)
| Secret | Where it lives | If lost |
|---|---|---|
| Sparkle EdDSA private key | login keychain ("Private key for signing Sparkle updates") | Existing installs never see another auto-update |
| Developer ID Application identity | login keychain | Re-issue from the Apple Developer portal; releases blocked until done |
| notarytool app-specific password | appleid.apple.com; cached as keychain profile AC_PASSWORD | Regenerate at appleid.apple.com, re-run store-credentials |
Backup procedure
-
Export the Sparkle private key (Keychain will prompt for access):
SPARKLE_BIN=$(ls -dt ~/Library/Developer/Xcode/DerivedData/Whisky-*/SourcePackages/artifacts/sparkle/Sparkle/bin 2>/dev/null | head -1) "$SPARKLE_BIN/generate_keys" -x sparkle_ed25519_private.keyIf several
Whisky-*DerivedData directories exist this picks the most recently modified one. -
Export the Developer ID identity: Keychain Access → My Certificates → right-click Developer ID Application: … → Export as
.p12with a strong password. -
Encrypt both files before they leave the machine:
age -p sparkle_ed25519_private.key > sparkle_ed25519_private.key.age # or: gpg -c <file> age -p developer_id.p12 > developer_id.p12.ageNeither
agenorgpgships with macOS; installagewithbrew install age. -
Store the encrypted files — plus the app-specific password itself — in two off-machine locations (e.g. a password-manager secure note and one offline medium). Delete the plaintext exports afterwards.
-
Record the certificate expiry date and set reminders at T-60 and T-14 days:
security find-certificate -c "Developer ID Application" -p | openssl x509 -noout -enddateThis prints the first matching certificate only. During a renewal window, when the old and new certificates coexist in the keychain, check every match with
security find-certificate -a -c "Developer ID Application" -p(each-----BEGINblock is one certificate) — and re-run this step after the renewal so the reminder tracks the new expiry, not the old one.
Restore test (do this the day the backup is made)
A backup that has never been restored from is a hope, not a backup. sign_update can sign directly from a key file, so the test never touches the keychain:
"$SPARKLE_BIN/sign_update" --ed-key-file sparkle_ed25519_private.key build/release/Whisky-X.Y.Z.dmg
The printed sparkle:edSignature and the printed length must both exactly match that release's entry in dist/pages/appcast.xml — Ed25519 signatures are deterministic, so a mismatch in either means a wrong key or a non-canonical DMG: the exported key is wrong or the DMG you signed is not the exact published artifact (rebuilt locally, partially downloaded, wrong file). If in doubt, download the release asset from the appcast enclosure URL and sign that.
Recovery on a new machine
-
Build Whisky once first so the Sparkle tools exist in DerivedData (see Sparkle EdDSA keys under One-time setup) —
generate_keysships inside the Sparkle SPM artifact, not onPATH. Re-declareSPARKLE_BINin this fresh shell (it is only set earlier in the Backup section):SPARKLE_BIN=$(ls -dt ~/Library/Developer/Xcode/DerivedData/Whisky-*/SourcePackages/artifacts/sparkle/Sparkle/bin 2>/dev/null | head -1)If several
Whisky-*DerivedData directories exist this picks the most recently modified one. -
Decrypt the backup (
age -d sparkle_ed25519_private.key.age > sparkle_ed25519_private.key, entering the backup passphrase), then import the Sparkle key:"$SPARKLE_BIN/generate_keys" -f sparkle_ed25519_private.key. If an existing item named Private key for signing Sparkle updates is already in the login keychain, the import may fail until that item is removed via Keychain Access. Delete the decrypted plaintext key once the import succeeds. -
Open the
.p12to install the Developer ID identity into the login keychain. -
Re-create the notary profile with
xcrun notarytool store-credentials AC_PASSWORD …(see One-time setup).
App release
1. Bump versions
Update both fields in Whisky.xcodeproj/project.pbxproj (every occurrence):
MARKETING_VERSION = X.Y.Z;— user-visible version.CURRENT_PROJECT_VERSION = N;— Sparkle build number, must increment monotonically.
2. Update the changelog
Move items from [Unreleased] to a new [X.Y.Z] - YYYY-MM-DD (App) section in CHANGELOG.md.
3. Build, sign, notarize, package
scripts/release.sh X.Y.Z
The script:
- Archives the app (Apple Development signing during archive — automatic provisioning handles cert resolution).
- Re-signs on export with Developer ID Application per
scripts/exportOptions.plist. - Verifies the signature with
codesign --verify --deep --strict. - Builds a UDZO disk image with
hdiutil. - Signs the DMG with the Developer ID Application certificate.
- Submits the DMG to Apple's notary service and waits for the verdict (typically 5–15 minutes).
- Staples the notarization ticket.
- Verifies the stapled DMG passes
spctl --assess.
The artifact lands at build/release/Whisky-X.Y.Z.dmg.
4. Sign the DMG for Sparkle
~/Library/Developer/Xcode/DerivedData/Whisky-*/SourcePackages/artifacts/sparkle/Sparkle/bin/sign_update build/release/Whisky-X.Y.Z.dmg
Capture the printed sparkle:edSignature and length.
5. Add an appcast entry
Edit dist/pages/appcast.xml and add a new <item> near the top of <channel>. Use the signature and length from the previous step:
<item>
<title>Whisky X.Y.Z</title>
<pubDate>RFC822 date here</pubDate>
<sparkle:version>BUILD_NUMBER</sparkle:version>
<sparkle:shortVersionString>X.Y.Z</sparkle:shortVersionString>
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
<description><![CDATA[<p>Release notes...</p>]]></description>
<enclosure
url="https://github.com/frankea/Whisky/releases/download/app-vX.Y.Z/Whisky-X.Y.Z.dmg"
sparkle:edSignature="..."
length="..."
type="application/octet-stream" />
</item>
6. Commit, tag, and release
git add Whisky.xcodeproj/project.pbxproj CHANGELOG.md dist/pages/appcast.xml
git commit -m "release: X.Y.Z"
git push
git tag -a app-vX.Y.Z -m "Whisky X.Y.Z"
git push origin app-vX.Y.Z
gh release create app-vX.Y.Z \
--repo frankea/Whisky \
--title "Whisky X.Y.Z" \
--notes "..." \
build/release/Whisky-X.Y.Z.dmg
The push to main triggers .github/workflows/Documentation.yml, which redeploys Pages with the updated appcast within ~1–2 minutes. Sparkle clients pick up the update on next launch.
7. Homebrew tap (automatic)
Publishing the app-vX.Y.Z release fires .github/workflows/UpdateHomebrewTap.yml,
which downloads the DMG, computes its sha256, and bumps the
frankea/homebrew-whisky cask so
brew install --cask frankea/whisky/whisky tracks the new version. No manual edit needed.
This requires a one-time repository secret BREW_TOKEN — a personal access token
(classic repo, or fine-grained with Contents: write) for frankea/homebrew-whisky;
the default GITHUB_TOKEN cannot push to another repository. If the secret is missing the
workflow fails loudly so the drift is visible. You can also re-sync any tag manually via the
workflow's workflow_dispatch input.
Wine Libraries release
The runtime (Libraries.tar.gz) is assembled from upstream binaries, not built from source — see
DEPENDENCIES.md for the authoritative list of components,
their pinned versions, and where each comes from. When the runtime needs to change:
- Assemble
Libraries.tar.gzfrom the pinned upstream binaries. The archive unpacks to aLibraries/tree the app expects (Libraries/Wine/bin/…is the Wine binary dir perWhiskyWineInstaller.binFolder). To reproduce a build:- Download the pinned Wine build (Gcenx
macOS_Wine_builds) and unpack it asLibraries/Wine/. - Add the pinned DXVK-macOS DLLs and DXMT prebuilt release into the runtime per the Gcenx layout.
- Place D3DMetal as extracted from Apple's Game Porting Toolkit. ⚠️ Redistribution is governed by Apple's GPTK license — confirm terms before publishing.
- Record the exact versions you used back into
docs/DEPENDENCIES.md, and bump the matching*_PINNEDtags in.github/workflows/RuntimeTrack.ymlso drift detection stays accurate. tar -czf Libraries.tar.gz Libraries/(mind theTarpipe-drain pitfall noted below).
- Download the pinned Wine build (Gcenx
- Tag with the bare version
vX.Y.Z(noapp-prefix). gh release create vX.Y.Z --title "Wine Libraries vX.Y.Z" Libraries.tar.gz.- Compute the SHA-256 of the exact published asset — the app verifies the download against this
and fails closed on a mismatch, so an incorrect value blocks every fresh install:
shasum -a 256 Libraries.tar.gz - Update
dist/pages/WhiskyWineVersion.plistwith the version, bundled DXVK version, and the digest from the previous step. Record the same digest inDEPENDENCIES.md:<dict> <key>version</key> <dict> <key>major</key><integer>X</integer> <key>minor</key><integer>Y</integer> <key>patch</key><integer>Z</integer> </dict> <key>dxvkVersion</key> <string>1.10.3</string> <key>sha256</key> <string>…64-hex-digest…</string> </dict> - Commit and push. The Documentation workflow republishes Pages, and existing app installs prompt to update on their next Wine version check.
URLs the app depends on
https://frankea.github.io/Whisky/WhiskyWineVersion.plist— Wine version metadatahttps://frankea.github.io/Whisky/appcast.xml— Sparkle update feedhttps://github.com/frankea/Whisky/releases/download/vX.Y.Z/Libraries.tar.gz— Wine binary archivehttps://github.com/frankea/Whisky/releases/download/app-vX.Y.Z/Whisky-X.Y.Z.dmg— app DMG
Pitfalls
- Don't override
CODE_SIGN_IDENTITYat archive time. The project is configured for Apple Development with automatic signing; overriding it conflicts with provisioning. The release script letsxcodebuild archiveuse the project default and re-signs on export. - Don't bundle the WhiskyKit folder as Resources. Doing so packages the package's
.builddirectory (DocC plugin executables) into the app, and Apple's notary rejects the archive because those plugin executables don't have hardened runtime. The PBXResourcesBuildPhase entry forWhiskyKitwas removed from the project for this reason; do not add it back. The PBXFileReference and PBXGroup entries must remain or Xcode's SPM workspace integration crashes on CI. - Pipe deadlocks in
Tar.process.waitUntilExit()must come after draining the pipe, not before. The verbose tar listing for a multi-hundred-megabyte archive will overflow the OS pipe buffer. See the fix in 3.0.1.