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:
- GitHub Releases — primary download, source of truth.
- Homebrew Cask — most-discovered install method on macOS.
- (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/nettopand read/Applicationsmetadata — 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.
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.
# 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:
- Releases → Draft a new release.
- Tag:
v2.0.0(or whatever version). MatchCHANGELOG.md. - Title:
v2.0.0 — short summary. - Body: copy the
## [Unreleased]block fromCHANGELOG.md. Move it under a real version heading at the same time. - Attach the DMG +
.sha256. - Publish.
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.
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.shThe 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.
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.
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.rbis 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 bytap-autobump.yml. Keep the seed'sversion/url/sha256in rough sync with the latest release so a fresh tap starts correct.
A starter formula is included at Distribution/network-monitor.rb. Update the version, url, and sha256 for each release; everything else is boilerplate.
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-monitorWhen you're confident in the release cadence:
- Fork homebrew/homebrew-cask.
- Add
Casks/n/network-monitor.rb(alphabetical folder layout). - Run
brew style --fix --cask network-monitorandbrew audit --new --cask network-monitor. - Open a PR. Expect reviewer feedback on naming, the
livecheckblock, and the homepage.
Homebrew-cask wants:
- A stable download URL pattern (
https://github.com/.../releases/download/v#{version}/NetworkUsageMonitor-#{version}.dmg—build_release.shproduces exactly this; note the asset name has novprefix, since the script strips it from the version). - A
livecheckblock 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).
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.
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
nettopblock-buffering investigation, the per-cycle architecture decision) rather than feature bullets. - r/macapps / r/MacOS — established communities, low expectations, easy first audience.
- Awesome lists —
awesome-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.
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.