Add a second macOS shipping channel alongside the App Store build: - New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so the App Store binary never bundles a self-updater. arm64-only (core is arm64). - Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID. - build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple), generate-appcast.sh, and ExportOptions-DeveloperID.plist. - apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop. apple.yml gains a PR compile-check of the direct target. - CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both channels, replacing the hand-maintained build number. - Docs (RELEASE-MACOS.md, README), cask template + tap README, localized updates_check string, Makefile targets, gitignore for dist/ artifacts.
5.5 KiB
Releasing VniDrop for macOS
VniDrop ships on macOS through two independent channels:
| Channel | Target / config | Signing | Updates |
|---|---|---|---|
| Mac App Store / TestFlight | VniDrop / Release |
Apple Distribution | App Store |
| Direct download (.dmg) | VniDropDirect / Release-Direct |
Developer ID + notarization | Sparkle |
The direct build adds the Sparkle auto-updater, gated behind the
DIRECT_DISTRIBUTION compile flag so the App Store binary never links or ships a
self-updater (which Apple forbids). Because Swift Package Manager links products
per target, the isolation is a dedicated VniDropDirect target — not just a
build configuration.
This document covers the direct-download channel. The App Store channel goes
through Xcode Organizer / App Store Connect as before (see
RELEASE-TESTFLIGHT.fr.md).
Versioning
Both channels use Apple's two-field convention, shown as MARKETING_VERSION (build):
CFBundleShortVersionString=MARKETING_VERSION— the humanX.Y.Zversion (from the git tag for a release). This is what users, the DMG filename, the GitHub Release, and the Homebrew cask all use.CFBundleVersion= a UTCYYMMDD.HHMMtimestamp, stamped into the built Info.plist by the "Stamp build number" build phase (apple/project.yml). It fires for every build — Xcode Organizer archive and CLI alike — so each build is monotonic and self-describing. Sparkle compares this field to order updates, and App Store Connect requires each upload's build to exceed the previous one; a timestamp satisfies both automatically.build-dmg.shpins one timestamp per archive (viaVNIDROP_BUILD) so the app, DMG, and appcast agree.
No build number is maintained by hand.
One-time setup
1. Developer ID Application certificate
Direct distribution requires a Developer ID Application certificate (distinct from the Apple Distribution cert used for the App Store). In Xcode ▸ Settings ▸ Accounts ▸ Manage Certificates ▸ + ▸ Developer ID Application. Confirm it's in the keychain:
security find-identity -v -p codesigning | grep "Developer ID Application"
2. Notarization credentials (App Store Connect API key)
Create an API key at App Store Connect ▸ Users and Access ▸ Integrations ▸ Keys
(role: Developer). Download the AuthKey_XXXX.p8, and note the Key ID and
Issuer ID. Store a local notarytool profile:
xcrun notarytool store-credentials vnidrop-notary \
--key /path/to/AuthKey_XXXX.p8 \
--key-id <KEY_ID> \
--issuer <ISSUER_ID>
3. Sparkle EdDSA signing key
Generate the update-signing key once (private key stored in the login keychain):
# Sparkle's generate_keys, from the downloaded Sparkle release (bin/generate_keys)
./bin/generate_keys
It prints the public key. Put it in apple/VniDrop/Resources/Info.plist as
SUPublicEDKey, replacing REPLACE_WITH_SPARKLE_ED_PUBLIC_KEY. Export the
private key for CI:
./bin/generate_keys -x sparkle_ed_private_key # writes the private key file
4. Homebrew tap repo
Create an empty GitHub repo sudosylabs/homebrew-vnidrop (a Casks/ folder
is enough). The release workflow pushes Casks/vnidrop.rb there. Users install:
brew install --cask sudosylabs/vnidrop/vnidrop
5. CI secrets
Add these to the sudosylabs/vnidrop repo (Settings ▸ Secrets ▸ Actions):
| Secret | Value |
|---|---|
DEVELOPER_ID_CERT_P12 |
base64 of the exported Developer ID .p12 |
DEVELOPER_ID_CERT_PASSWORD |
password for that .p12 |
NOTARY_API_KEY |
base64 of AuthKey_XXXX.p8 |
NOTARY_KEY_ID |
App Store Connect key ID |
NOTARY_ISSUER |
App Store Connect issuer ID |
SPARKLE_ED_PRIVATE_KEY |
contents of the exported Sparkle private key file |
HOMEBREW_TAP_TOKEN |
PAT with write access to homebrew-vnidrop |
SUFeedURLin Info.plist points athttps://github.com/sudosylabs/vnidrop/releases/latest/download/appcast.xml. GitHub's/releases/latest/download/path always redirects to the newest non-prerelease release's asset, so no GitHub Pages or repo commits are needed.
Per-release flow
-
Bump
MARKETING_VERSION(andCURRENT_PROJECT_VERSION) inapple/project.ymlif needed, commit tomaster. -
Tag and push:
git tag v0.2.0 git push origin v0.2.0 -
.github/workflows/apple-release.ymlthen:- builds the Rust core (release), regenerates the project,
- archives + exports the
VniDropDirecttarget (Developer ID, hardened runtime), - builds, signs, notarizes and staples
VniDrop-<version>.dmg, - runs
generate_appcastto produceappcast.xml(enclosure → the release DMG), - creates the GitHub Release with both assets, and
- renders + pushes the Homebrew cask to the tap.
Existing installs pick up the new version automatically via Sparkle (feed →
latest release's appcast.xml); brew upgrade respects auto_updates true.
Building a DMG locally
# Signed DMG (skips notarization unless NOTARY_PROFILE is set):
make build-apple-dmg VERSION=0.2.0
# Full signed + notarized DMG:
NOTARY_PROFILE=vnidrop-notary make build-apple-dmg VERSION=0.2.0
Output: apple/dist/VniDrop-<version>.dmg. See apple/scripts/build-dmg.sh for
the environment variables (DEVELOPER_ID_APP, DEVELOPMENT_TEAM,
NOTARY_PROFILE). Generate the appcast with
apple/scripts/generate-appcast.sh <version>.
To compile-check the direct target without signing:
make build-apple-macos-direct