Skip to content

Latest commit

 

History

History
187 lines (130 loc) · 8.77 KB

File metadata and controls

187 lines (130 loc) · 8.77 KB

Distribution & Publishing

This document covers everything from "I just made a tag" to "users can brew install my app." The path that actually works for an unsandboxed menu-bar utility like this one is:

  1. GitHub Releases — primary download, source of truth.
  2. Homebrew Cask — most-discovered install method on macOS.
  3. (Optional) Notarization with Apple — removes the "unidentified developer" warning, requires a $99/yr Apple Developer account.

What doesn't work for this app:

  • Mac App Store. Requires the App Sandbox. We need to invoke /usr/bin/nettop and read /Applications metadata — both blocked. There is no workaround other than a fundamentally different (and worse) implementation. Don't try.
  • SetApp. Worth investigating later if you want a paid distribution channel — they accept unsandboxed apps but require an application + revenue share.

Step 1 — GitHub Releases (free, required)

Scripts/build_release.sh is the one true build path: it archives via xcodebuild (signing the embedded system extension inside-out), exports a Developer-ID app, notarizes + staples when credentials are present, and packages a signed DMG with a SHA-256 alongside.

1a. Manual release (good enough to start)

# from repo root — requires the Developer ID cert + profiles (see EXTENSION_SETUP.md)
NOTARY_KEYCHAIN_PROFILE="NetworkSpeed-Notary" ./Scripts/build_release.sh
# outputs: build/NetworkUsageMonitor-<version>.dmg (+ .sha256 — you'll need it for Homebrew)

Then on GitHub:

  1. Releases → Draft a new release.
  2. Tag: v2.0.0 (or whatever version). Match CHANGELOG.md.
  3. Title: v2.0.0 — short summary.
  4. Body: copy the ## [Unreleased] block from CHANGELOG.md. Move it under a real version heading at the same time.
  5. Attach the DMG + .sha256.
  6. Publish.

1b. Publish from the terminal (gh)

There is no release CI — the signing identity, Developer-ID cert, and notary credentials live only on the maintainer's Mac, so releases are built locally and pushed with the GitHub CLI. One build, one publish:

VER=2.0.1   # or 2.0.1-rc.1 for a pre-release
NOTARY_KEYCHAIN_PROFILE="NetworkSpeed-Notary" ./Scripts/build_release.sh
gh release create "v$VER" \
  "build/NetworkUsageMonitor-$VER.dmg" "build/NetworkUsageMonitor-$VER.dmg.sha256" \
  --title "v$VER" --generate-notes
# add  --prerelease  for a hyphenated -rc/-beta tag so it isn't marked "Latest"

gh release create makes the tag, the GitHub Release, and uploads the assets in one step — no separate git tag/git push needed. The DMG it attaches is already signed + notarized + stapled (app and container), so it clears Gatekeeper on download.


Step 2 — Code signing & notarization

Signing and notarization are built into Scripts/build_release.sh — there is no separate manual path. The v2 app embeds a NetworkExtension system extension (FilterExtension.systemextension) that must be signed inside-out as part of an xcodebuild archive → export; a single manual codesign of the bundle silently produces an app that can't install the extension or read the rules container — i.e. a dead firewall. See docs/dev/EXTENSION_SETUP.md for the one-time certificate / profile provisioning.

Without notarization, first-time users see a Gatekeeper warning. They can right-click → Open to bypass it, but a sizable fraction will assume the app is broken and walk away.

Notarization requires:

  • An Apple Developer Program membership ($99/year).
  • A "Developer ID Application" certificate installed in your keychain.
  • Stored notarytool credentials: xcrun notarytool store-credentials NetworkSpeed-Notary (one time; uses an app-specific password).

Then a fully signed + notarized + stapled DMG is one command:

NOTARY_KEYCHAIN_PROFILE="NetworkSpeed-Notary" ./Scripts/build_release.sh

The submit is bounded (NOTARY_TIMEOUT, default 30m) and prints the notarytool log on any non-Accepted outcome; a timed-out submission can be re-polled with xcrun notarytool info <id> without re-uploading.

build_release.sh also accepts the Apple-ID credentials (NOTARY_APPLE_ID, NOTARY_TEAM_ID, NOTARY_PASSWORD) or an App Store Connect API key (NOTARY_KEY_*) via the environment, if you prefer those to a stored keychain profile.

Required GitHub secrets

Releases are built locally (§1b), so no Apple signing secrets live in GitHub — the cert and notary credentials never leave the maintainer's Mac. The only workflow that needs a secret is the license issuer:

license.yml (issue-triggered) needs one secret: LICENSE_SIGNING_KEY — the base64 raw Ed25519 private key whose public half is embedded in Sources/MonitorCore/LicenseTrust.swift. Generate a keypair with the committed signer (LICENSEGEN_KEY_FILE=tools/.keys/prod.key swift run --package-path tools/licensegen licensegen new-key prints the public key, writes the private to the file), embed the printed public key + flip trustedKeysConfigured = true, then gh secret set LICENSE_SIGNING_KEY < tools/.keys/prod.key. Back the private key file up off-machine — it's the only way to mint new keys that validate against the embedded public key. To rotate, ship the old + new public keys, then swap the secret.


Step 3 — Homebrew Cask

The cask formula is short. Once you have at least one notarized GitHub Release with a .dmg attached, you can submit it to the homebrew/homebrew-cask repo. Or self-host as a tap if you want to iterate without waiting on review.

The seed cask at Distribution/network-monitor.rb is only a bootstrap for a brand-new tap; the live cask lives in the tap repo (Light-House-Group/Homebrew-Taps) and is kept current automatically by tap-autobump.yml. Keep the seed's version/url/sha256 in rough sync with the latest release so a fresh tap starts correct.

Sample cask

A starter formula is included at Distribution/network-monitor.rb. Update the version, url, and sha256 for each release; everything else is boilerplate.

Self-hosted tap (fastest path)

If you don't want to wait for homebrew-cask review, publish as your own tap:

# 1. Create a repo named `homebrew-taps` under your GitHub account (already done:
#    Light-House-Group/Homebrew-Taps).
# 2. Add Distribution/network-monitor.rb as Casks/network-monitor.rb in that repo.
# 3. Users can install with:
brew tap light-house-group/taps
brew install --cask light-house-group/taps/network-monitor

Official homebrew-cask submission

When you're confident in the release cadence:

  1. Fork homebrew/homebrew-cask.
  2. Add Casks/n/network-monitor.rb (alphabetical folder layout).
  3. Run brew style --fix --cask network-monitor and brew audit --new --cask network-monitor.
  4. Open a PR. Expect reviewer feedback on naming, the livecheck block, and the homepage.

Homebrew-cask wants:

  • A stable download URL pattern (https://github.com/.../releases/download/v#{version}/NetworkUsageMonitor-#{version}.dmgbuild_release.sh produces exactly this; note the asset name has no v prefix, since the script strips it from the version).
  • A livecheck block that points at the Releases page so they can auto-bump.
  • The app to launch and exit cleanly on a clean macOS (no installer prompts they can't auto-approve).

Step 4 — Sparkle (in-app update, optional)

For a polished UX, integrate Sparkle. It's the de-facto Mac auto-updater and works fine with unsandboxed apps. Requires:

  • A signing key pair (Sparkle ed25519, separate from your Developer ID).
  • A publicly-hosted appcast.xml.

Not required for a first release. Add it once the release cadence stabilises.


Step 5 — Promote

Once you have a notarized DMG and a working Homebrew Cask:

  • Show HN — Hacker News tolerates one self-promo post per project. Lead with the technical story (the nettop block-buffering investigation, the per-cycle architecture decision) rather than feature bullets.
  • r/macapps / r/MacOS — established communities, low expectations, easy first audience.
  • Awesome listsawesome-macos, awesome-mac, etc. accept PRs.
  • Product Hunt — works best with a polished screenshot and a launch-day tweet thread.

Don't bother with paid ads. Word-of-mouth + Homebrew discoverability beats anything you'd pay for at this stage.


Versioning rule

Use SemVer:

  • MAJOR — breaks user data file format (e.g. lifetime store schema change).
  • MINOR — new feature, no migration required.
  • PATCH — bug fix, no behaviour change.

Update CHANGELOG.md as the same PR that introduces a change. Don't let it drift.