Notarized DMG release script + Homebrew cask - #2
Conversation
Add Scripts/release.sh to cut a distributable build without users building from source: build → sign (Developer ID + hardened runtime) → notarize the app → staple → package a DMG → sign/notarize/staple the DMG → refresh the Homebrew cask → optionally publish a GitHub Release (PUBLISH=1). Team ID V2NXQ22BM9 is the default; identity is auto-detected from the Keychain. Add packaging/homebrew/hadron-menu-bar.rb as the cask source of truth (release.sh rewrites version + sha256), and document the one-time prerequisites (Developer ID cert, notarytool credentials) plus the release and cask-publish flow in the README. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a release automation script (Scripts/release.sh), a Homebrew cask template (packaging/homebrew/hadron-menu-bar.rb), and updates the README.md with instructions for cutting a notarized release. The review feedback suggests robust improvements to the release script, including an early check for the gh CLI when publishing is enabled and using a trap to ensure temporary directories are cleaned up on failure. Additionally, it recommends adding a zap stanza to the Homebrew cask to ensure standard configuration and cache files are properly removed upon uninstallation.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| VERSION="${1:-${VERSION:-}}" | ||
| if [[ -z "${VERSION}" ]]; then | ||
| VERSION="$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)" | ||
| fi | ||
| if [[ -z "${VERSION}" ]]; then | ||
| echo "usage: Scripts/release.sh <version> e.g. Scripts/release.sh 0.1.0" >&2 | ||
| exit 1 | ||
| fi | ||
| VERSION="${VERSION#v}" |
There was a problem hiding this comment.
If PUBLISH=1 is set but the gh CLI is not installed or not in the PATH, the script will run the entire build, signing, and notarization pipeline (which takes several minutes) before failing at the very end when attempting to run gh release create.
Adding an early check for the gh CLI at the start of the script prevents this wasted effort and provides a clear, actionable error message immediately.
| VERSION="${1:-${VERSION:-}}" | |
| if [[ -z "${VERSION}" ]]; then | |
| VERSION="$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)" | |
| fi | |
| if [[ -z "${VERSION}" ]]; then | |
| echo "usage: Scripts/release.sh <version> e.g. Scripts/release.sh 0.1.0" >&2 | |
| exit 1 | |
| fi | |
| VERSION="${VERSION#v}" | |
| VERSION="${1:-${VERSION:-}}" | |
| if [[ -z "${VERSION}" ]]; then | |
| VERSION="$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || true)" | |
| fi | |
| if [[ -z "${VERSION}" ]]; then | |
| echo "usage: Scripts/release.sh <version> e.g. Scripts/release.sh 0.1.0" >&2 | |
| exit 1 | |
| fi | |
| VERSION="${VERSION#v}" | |
| if [[ "${PUBLISH:-0}" == "1" ]] && ! command -v gh &>/dev/null; then | |
| echo "error: 'gh' CLI is required to publish the release but was not found in PATH." >&2 | |
| exit 1 | |
| fi |
| # 4. Package a DMG (app + drag-to-Applications shortcut) from the stapled app. | ||
| echo "==> Building DMG" | ||
| DMG="${DIST}/${APP_NAME}-${VERSION}.dmg" | ||
| STAGE="$(mktemp -d)" |
There was a problem hiding this comment.
If the script fails or is interrupted after creating the temporary directory STAGE (e.g., during hdiutil create or signing/notarization), the directory is leaked in the system's temporary folder because set -e aborts execution immediately.
Using a trap to clean up the temporary directory ensures it is always deleted upon exit, regardless of success or failure.
| STAGE="$(mktemp -d)" | |
| STAGE="$(mktemp -d)" | |
| trap 'rm -rf "${STAGE}"' EXIT |
| app "HadronMenuBar.app" | ||
| end |
There was a problem hiding this comment.
Although the sign-in token is stored in the macOS Keychain (which cannot be removed via a cask zap stanza), the application still generates standard configuration, cache, and state files on disk (such as plist preferences and saved application state).
Adding a zap stanza is a Homebrew best practice to ensure a clean uninstallation of these leftover files when a user runs brew uninstall --zap hadron-menu-bar.
app "HadronMenuBar.app"
zap trash: [
"~/Library/Caches/com.hadron.macapp",
"~/Library/Preferences/com.hadron.macapp.plist",
"~/Library/Saved Application State/com.hadron.macapp.savedState",
]
end
What
Adds tooling to distribute the app without users building from source — a local, one-command notarized release plus a Homebrew cask. (Chosen setup: local release script + DMG + cask.)
Scripts/release.sh— build → sign (Developer ID + hardened runtime) → notarize the app → staple → package a DMG → sign/notarize/staple the DMG → refresh the cask → optionally publish the GitHub Release.packaging/homebrew/hadron-menu-bar.rb— the cask source of truth;release.shrewrites itsversion+sha256after each build.Usage
Team ID
V2NXQ22BM9is the default; the signing identity is auto-detected from the Keychain (override withSIGN_IDENTITY/NOTARY_PROFILE/TEAM_ID).Prerequisites (one-time, done by the maintainer)
xcrun notarytool store-credentials "hadron-notary" --apple-id … --team-id V2NXQ22BM9 --password <app-specific-password>.Notes for reviewers
/Applications.zapstanza is intentionally omitted — the sign-in token is in the Keychain (removed via the app's Sign Out), which a cask can't reach.make-app.sh(ad-hoc signing) remains for local/dev use;release.shis the distribution path.Verification
bash -npasses for both scripts;ruby -cpasses for the cask.Scripts/release.sh 0.1.0on a machine with no Developer ID cert stops with a clear, actionable error (verified) — no partial/unsigned artifacts produced.🤖 Generated with Claude Code