Skip to content

Notarized DMG release script + Homebrew cask - #2

Open
shadowbrush wants to merge 1 commit into
mainfrom
feat/notarized-release
Open

Notarized DMG release script + Homebrew cask#2
shadowbrush wants to merge 1 commit into
mainfrom
feat/notarized-release

Conversation

@shadowbrush

Copy link
Copy Markdown
Member

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.sh rewrites its version + sha256 after each build.
  • README — documents the one-time prerequisites and the release/cask-publish flow.

Usage

Scripts/release.sh 0.1.0             # → dist/HadronMenuBar-0.1.0.dmg (notarized)
PUBLISH=1 Scripts/release.sh 0.1.0   # also creates the GitHub Release

Team ID V2NXQ22BM9 is the default; the signing identity is auto-detected from the Keychain (override with SIGN_IDENTITY / NOTARY_PROFILE / TEAM_ID).

Prerequisites (one-time, done by the maintainer)

  1. Create a Developer ID Application certificate (Xcode → Settings → Accounts → Manage Certificates → + → Developer ID Application). Enrolling in the Developer Program does not create one automatically.
  2. Store notarytool credentials: xcrun notarytool store-credentials "hadron-notary" --apple-id … --team-id V2NXQ22BM9 --password <app-specific-password>.

Notes for reviewers

  • Notarizes the app and the DMG separately and staples both, so the app passes Gatekeeper whether launched from the DMG or copied to /Applications.
  • The zap stanza 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.sh is the distribution path.

Verification

  • bash -n passes for both scripts; ruby -c passes for the cask.
  • Scripts/release.sh 0.1.0 on a machine with no Developer ID cert stops with a clear, actionable error (verified) — no partial/unsigned artifacts produced.
  • Full end-to-end signing/notarization can't run here (requires the not-yet-created Developer ID cert + notary credentials).

🤖 Generated with Claude Code

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>

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread Scripts/release.sh
Comment on lines +32 to +40
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}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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

Comment thread Scripts/release.sh
# 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)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
STAGE="$(mktemp -d)"
STAGE="$(mktemp -d)"
trap 'rm -rf "${STAGE}"' EXIT

Comment on lines +24 to +25
app "HadronMenuBar.app"
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant