refactor(ci): move the Homebrew cask bump out of CI onto a maintainer machine

`brew bump-cask-pr` forks homebrew-cask into the token owner's account, which
requires a CLASSIC PAT with the `repo` scope. That scope cannot be narrowed: it
grants write to every repository the owning account can reach, including this
one. As an Actions secret it would be usable by anyone with push access here,
because a pushed branch containing a workflow runs with repo secrets — a strict
escalation for a channel that ships one DMG a month.

Split the work so the token never becomes a CI secret:

- CI (bump-homebrew.yml, now "Sync Homebrew Cask Reference") does the
  error-prone bookkeeping with GITHUB_TOKEN only: downloads the DMG, asserts it
  is notarized + stapled, computes the sha256, and opens an in-repo PR syncing
  desktopApp/packaging/homebrew/amethyst-nostr.rb. This makes it a third sibling
  of the amy/geode formula-sync workflows rather than a special case.
- scripts/bump-homebrew-cask.sh does the upstream PR from a maintainer's shell,
  re-verifying sha256 and the notarization ticket against the live asset first,
  and refusing to submit if either fails. Verified against v1.13.1: the sha256
  matches and it correctly refuses on the missing notarization ticket.

Drop HOMEBREW_TOKEN from the secret inventory, and rewrite the two formula
TODO(bootstrap) notes that proposed reintroducing it so a future homebrew-core
submission follows the same local pattern.
This commit is contained in:
Vitor Pamplona
2026-07-29 10:19:57 -04:00
parent 2e47e9cd56
commit db529444c4
6 changed files with 296 additions and 148 deletions
+8 -6
View File
@@ -134,12 +134,14 @@ jobs:
Opened by `.github/workflows/bump-homebrew-formula.yml`. Merge to keep
the reference formula ready for the homebrew-core submission/bump.
# TODO(bootstrap): once `amy` is accepted into Homebrew/homebrew-core, add a
# step here that opens the homebrew-core bump PR automatically — symmetric to
# the cask bump in bump-homebrew.yml (a pinned macauley/action-homebrew-bump-
# formula, or `brew bump-formula-pr amy --url=<url> --sha256=<sha>` with a
# HOMEBREW_TOKEN). It is intentionally omitted until then because
# bump-formula-pr errors on a formula that is not yet in the tap.
# TODO(bootstrap): once `amy` is accepted into Homebrew/homebrew-core, add
# a LOCAL script mirroring scripts/bump-homebrew-cask.sh that runs `brew
# bump-formula-pr amy --url=<url> --sha256=<sha>` from a maintainer's
# machine. Do NOT wire that into CI: bump-formula-pr forks homebrew-core
# into the token owner's account, which needs a classic `repo`-scoped PAT,
# and that scope grants write to every repo the account can reach —
# including this one — to anyone with push access here. See
# BUILDING.md § Homebrew cask for the full reasoning.
- name: Report failure
if: failure()
@@ -132,12 +132,12 @@ jobs:
Opened by `.github/workflows/bump-homebrew-geode-formula.yml`. Merge to
keep the reference formula ready for the homebrew-core submission/bump.
# TODO(bootstrap): once `geode` is accepted into Homebrew/homebrew-core, add
# a step here that opens the homebrew-core bump PR automatically (a pinned
# macauley/action-homebrew-bump-formula, or `brew bump-formula-pr geode
# --url=<url> --sha256=<sha>` with a HOMEBREW_TOKEN). It is intentionally
# omitted until then because bump-formula-pr errors on a formula that is not
# yet in the tap.
# TODO(bootstrap): once `geode` is accepted into Homebrew/homebrew-core,
# add a LOCAL script mirroring scripts/bump-homebrew-cask.sh that runs
# `brew bump-formula-pr geode --url=<url> --sha256=<sha>` from a
# maintainer's machine. Do NOT wire that into CI — it needs a classic
# `repo`-scoped PAT, which as a CI secret would hand write access to this
# repo to anyone with push access. See BUILDING.md § Homebrew cask.
- name: Report failure
if: failure()
+113 -105
View File
@@ -1,21 +1,30 @@
name: Bump Homebrew Cask
name: Sync Homebrew Cask Reference
# Fires after "Create Release Assets" finishes successfully for a tag push.
# Sibling of bump-homebrew-formula.yml (amy) and bump-homebrew-geode-formula.yml
# (geode). Same mechanism, third artifact:
# - this workflow -> Cask `amethyst-nostr` (the desktop GUI app / DMG)
#
# NOT `release: types: [released]`. That event never fires here: the release is
# created by create-release.yml using GITHUB_TOKEN, and GitHub deliberately does
# not raise workflow-triggering events for actions taken by GITHUB_TOKEN (the
# recursion guard). This workflow sat silently dead through every release up to
# v1.13.1 for exactly that reason.
# What it does: after a stable release, download the published macOS DMG, assert
# it is notarized + stapled, compute its sha256, and open a PR syncing
# `desktopApp/packaging/homebrew/amethyst-nostr.rb` to that release.
#
# `workflow_run` has no such restriction, and it is also strictly better timed:
# it fires once ALL upload legs have finished, whereas `released` fired while
# assets were still uploading (hence the download retry loops in the sibling
# formula workflows).
# What it does NOT do: open a PR against Homebrew/homebrew-cask. That step is
# deliberately MANUAL and runs on a maintainer's machine —
# `scripts/bump-homebrew-cask.sh`. Reason: `brew bump-cask-pr` forks
# homebrew-cask into the token owner's account, which requires a CLASSIC PAT
# with the `repo` scope; that scope grants write to every repository the account
# can reach, and stored as a CI secret it would be usable by anyone with push
# access to this repo. Keeping it in a maintainer's shell instead of a CI secret
# removes that blast radius entirely, at the cost of one command per release.
# See BUILDING.md § Homebrew cask.
#
# Draft/prerelease state is not in the workflow_run payload, so it is read back
# from the API by .github/actions/resolve-release and enforced by
# .github/actions/assert-stable-release.
# Consequence: this workflow needs NO external token. GITHUB_TOKEN is enough,
# exactly like the two formula workflows.
#
# Trigger: after "Create Release Assets" succeeds for a tag push. NOT
# `release: types: [released]` — that event never fires, because the release is
# created by create-release.yml under GITHUB_TOKEN and GitHub suppresses
# workflow-triggering events for it. See the note in bump-homebrew-formula.yml.
on:
workflow_run:
workflows: ["Create Release Assets"]
@@ -23,43 +32,36 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag to bump (for manual recovery)'
description: 'Release tag to sync (for manual recovery)'
required: true
type: string
permissions:
contents: read
# The "Report failure" step below opens a [release-ops] issue via
# github.rest.issues.create; that needs issues:write. Without it the failure
# reporter itself 403s and no alert is ever filed.
contents: write
pull-requests: write
# The "Report failure" step opens a [release-ops] issue via
# github.rest.issues.create, which needs issues:write.
issues: write
concurrency:
# Serialize bumps per tag; do not cancel in-progress bumps.
# Serialize per tag; do not cancel in-progress runs.
group: bump-homebrew-${{ github.event.workflow_run.head_branch || inputs.tag }}
cancel-in-progress: false
jobs:
# Split in two on purpose. Cask commands are macOS-only (Homebrew on Linux
# refuses them), but the bump is gated on a cask that does not exist yet — so
# the cheap ubuntu `check` job decides, and the 10x-billed macOS `bump` job is
# only created when there is actually something to bump.
check:
# On workflow_run, filter down to a SUCCESSFUL TAG build. `event == 'push'`
# excludes create-release.yml's workflow_dispatch dry-runs (which carry a
# synthetic test_tag and publish nothing), and the head_branch prefix keeps
# non-tag runs out. Exact tag format is still enforced downstream.
sync-cask:
# See bump-homebrew-formula.yml for why these three conditions: successful,
# tag-push (not a dry-run dispatch), v-prefixed. Format enforced downstream.
if: >-
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
startsWith(github.event.workflow_run.head_branch, 'v'))
runs-on: ubuntu-latest
timeout-minutes: 15
outputs:
tag: ${{ steps.rel.outputs.tag }}
ver: ${{ steps.rel.outputs.ver }}
bootstrapped: ${{ steps.cask.outputs.bootstrapped }}
# macOS runner: `xcrun stapler` is the only way to verify the notarization
# ticket, and shipping an unnotarized DMG to the cask is the failure mode
# this whole channel is most exposed to.
runs-on: macos-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@v7
@@ -78,23 +80,81 @@ jobs:
is_prerelease: ${{ steps.rel.outputs.is_prerelease }}
is_draft: ${{ steps.rel.outputs.is_draft }}
# `brew bump-cask-pr` can only bump a cask that ALREADY EXISTS in the tap.
# `amethyst-nostr` has never been submitted to Homebrew/homebrew-cask, so
# until the one-time bootstrap PR lands (BUILDING.md § Bootstrap) this
# workflow must no-op rather than fail — otherwise every single release
# files a spurious [release-ops] issue.
- name: Check the cask exists in homebrew-cask
id: cask
- name: Download DMG, verify notarization, compute sha256
id: asset
run: |
set -euo pipefail
if curl -fsSL -o /dev/null https://formulae.brew.sh/api/cask/amethyst-nostr.json; then
echo "bootstrapped=true" >> "$GITHUB_OUTPUT"
echo "cask amethyst-nostr found in homebrew-cask; proceeding with bump"
else
echo "bootstrapped=false" >> "$GITHUB_OUTPUT"
echo "::warning::Cask 'amethyst-nostr' is not in Homebrew/homebrew-cask yet, so there is nothing to bump for ${{ steps.rel.outputs.tag }}. Amethyst is NOT shipping via Homebrew. Submit the one-time new-cask PR (BUILDING.md § Bootstrap) to activate this channel."
TAG="${{ steps.rel.outputs.tag }}"
VER="${{ steps.rel.outputs.ver }}"
URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/amethyst-desktop-${VER}-macos-arm64.dmg"
echo "Fetching $URL"
# workflow_run fires only after every upload leg has finished, so the
# asset should already be there. Retry anyway for release-CDN
# propagation (mirrors the repo's push/pull retry ethos).
ok=0
for i in 1 2 3 4 5; do
if curl -fsSL -o amethyst.dmg "$URL"; then ok=1; break; fi
wait=$(( 2 ** i ))
echo "attempt $i failed; retrying in ${wait}s"
sleep "$wait"
done
[[ "$ok" == 1 ]] || { echo "::error::could not download $URL"; exit 1; }
test -s amethyst.dmg
# A cask must point at a notarized+stapled DMG or every user hits a
# Gatekeeper block. Refuse to advertise one that is not.
if ! xcrun stapler validate amethyst.dmg; then
echo "::error::${TAG} DMG has no stapled notarization ticket -- refusing to sync the cask. Check the notarizeReleaseDmg step in create-release.yml."
exit 1
fi
SHA=$(shasum -a 256 amethyst.dmg | awk '{print $1}')
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
echo "amethyst-desktop-${VER}-macos-arm64.dmg -> $SHA"
- name: Update reference cask
run: |
set -euo pipefail
CASK=desktopApp/packaging/homebrew/amethyst-nostr.rb
VER="${{ steps.rel.outputs.ver }}"
SHA="${{ steps.asset.outputs.sha256 }}"
# Anchor on the 2-space indent so the header comment's example lines
# are never touched.
sed -i '' -E "s|^( version ).*|\1\"${VER}\"|" "$CASK"
sed -i '' -E "s|^( sha256 ).*|\1\"${SHA}\"|" "$CASK"
echo "----- $CASK -----"
grep -E "^ (version|sha256) " "$CASK"
- name: Open or update the cask-sync PR
# peter-evans/create-pull-request is MIT-licensed CI-only tooling (not
# linked into any shipped artifact). It no-ops when there is no diff.
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
base: main
branch: chore/bump-amethyst-cask-${{ steps.rel.outputs.tag }}
add-paths: desktopApp/packaging/homebrew/amethyst-nostr.rb
commit-message: 'chore: sync amethyst-nostr cask to ${{ steps.rel.outputs.tag }}'
title: 'chore: sync amethyst-nostr cask to ${{ steps.rel.outputs.tag }}'
body: |
Auto-synced `desktopApp/packaging/homebrew/amethyst-nostr.rb` to the
`${{ steps.rel.outputs.tag }}` release:
- `version` -> `${{ steps.rel.outputs.ver }}`
- `sha256` -> `${{ steps.asset.outputs.sha256 }}`
The DMG was verified notarized + stapled before this PR was opened.
**Merge this, then push it upstream from a maintainer machine:**
```bash
scripts/bump-homebrew-cask.sh ${{ steps.rel.outputs.tag }}
```
That step is manual on purpose — it needs a classic PAT with the
`repo` scope, which is deliberately NOT stored as a CI secret. See
BUILDING.md § Homebrew cask.
- name: Report failure
if: failure()
uses: actions/github-script@v9
@@ -105,69 +165,17 @@ jobs:
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `[release-ops] bump-homebrew failed for ${tag}`,
title: `[release-ops] sync-homebrew-cask failed for ${tag}`,
body: [
`Homebrew cask bump failed for release \`${tag}\`.`,
`amethyst-nostr cask sync failed for release \`${tag}\`.`,
``,
`- Run: ${runUrl}`,
`- Channel: Homebrew Cask (\`amethyst-nostr\`)`,
``,
`Recovery options:`,
`1. Re-run the workflow once underlying issue is fixed`,
`2. Manually run \`brew bump-cask-pr amethyst-nostr --version ${tag.replace(/^v/, '')}\``,
`3. File PR directly against Homebrew/homebrew-cask`
].join('\n'),
labels: ['release-ops', 'bug']
});
bump:
needs: check
if: needs.check.outputs.bootstrapped == 'true'
# Casks are a macOS concept; Homebrew on Linux rejects `bump-cask-pr`.
runs-on: macos-latest
timeout-minutes: 30
steps:
# Previously this used macauley/action-homebrew-bump-cask pinned to a SHA
# that does not exist in that repo (its only release is v1 from 2021, and
# the pin was commented "v4.0.0"). Because GitHub resolves every `uses:`
# during "Set up job", that bogus pin failed the whole job before any step
# ran — invisible until the trigger was fixed, since the workflow had never
# executed. Call brew directly instead: it is the same command BUILDING.md
# documents for manual recovery, and it drops an unmaintained third-party
# action that would hold a PAT with write access to homebrew-cask.
- name: Bump cask (opens PR against Homebrew/homebrew-cask)
env:
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_TOKEN }}
TAG: ${{ needs.check.outputs.tag }}
VER: ${{ needs.check.outputs.ver }}
run: |
set -euo pipefail
URL="https://github.com/${{ github.repository }}/releases/download/${TAG}/amethyst-desktop-${VER}-macos-arm64.dmg"
echo "Bumping amethyst-nostr to ${VER} from ${URL}"
brew developer on
brew bump-cask-pr amethyst-nostr --version "${VER}" --url "${URL}" --no-browse
- name: Report failure
if: failure()
uses: actions/github-script@v9
with:
script: |
const tag = '${{ needs.check.outputs.tag }}' || 'unknown';
const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `[release-ops] bump-homebrew failed for ${tag}`,
body: [
`Homebrew cask bump failed for release \`${tag}\`.`,
``,
`- Run: ${runUrl}`,
`- Channel: Homebrew Cask (\`amethyst-nostr\`)`,
``,
`Recovery options:`,
`1. Re-run the workflow once underlying issue is fixed`,
`2. Manually run \`brew bump-cask-pr amethyst-nostr --version ${tag.replace(/^v/, '')}\``,
`3. File PR directly against Homebrew/homebrew-cask`
`1. Re-run the workflow once the underlying issue is fixed`,
`2. Check the DMG is notarized: \`xcrun stapler validate\` on the release asset`,
`3. Manually update \`desktopApp/packaging/homebrew/amethyst-nostr.rb\` (version + sha256)`
].join('\n'),
labels: ['release-ops', 'bug']
});
+39 -23
View File
@@ -403,7 +403,7 @@ provided automatically; everything else you set yourself.)
| `MAC_NOTARY_APPLE_ID` | Apple ID email of the notarization account | Apple notarization (`notarytool`) |
| `MAC_NOTARY_PASSWORD` | **App-specific** password for that Apple ID (not the login password) | Same |
| `MAC_NOTARY_TEAM_ID` | 10-char Apple Developer **Team ID** | Same |
| `HOMEBREW_TOKEN` | PAT for `Homebrew/homebrew-cask` | Desktop cask bump (stable tags) |
| ~~`HOMEBREW_TOKEN`~~ | *Not used.* The cask bump runs on a maintainer's machine — see § Homebrew cask | — |
| `WINGET_TOKEN` | PAT for `microsoft/winget-pkgs` | Desktop winget bump (stable tags) |
| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Crowdin API creds | Translation sync (separate workflow, not the release) |
@@ -529,38 +529,54 @@ their token type and scope:
| Secret | Purpose | Scope |
|---|---|---|
| `HOMEBREW_TOKEN` | Bump Homebrew cask | **Classic** PAT — `repo` scope — 90d expiry (dedicated bot account strongly preferred; see below) |
| `WINGET_TOKEN` | Submit Winget manifests | Classic PAT — `public_repo` — 90d expiry (dedicated bot account preferred; `vedantmgoyal9/winget-releaser` does not support fine-grained) |
**Why `HOMEBREW_TOKEN` must be a classic PAT, not fine-grained.** `brew
bump-cask-pr` forks `Homebrew/homebrew-cask` **into the token owner's account**
(`POST /repos/Homebrew/homebrew-cask/forks`), pushes a branch to that fork, and
opens the PR upstream. Two consequences:
**There is deliberately no `HOMEBREW_TOKEN`.** The cask bump is the one release
step that runs on a maintainer's machine rather than in CI. The reasoning is
worth keeping, because the same trade-off applies to `WINGET_TOKEN`:
- A fine-grained PAT cannot express this. Its "Repository access" selector only
`brew bump-cask-pr` forks `Homebrew/homebrew-cask` **into the token owner's
account** (`POST /repos/Homebrew/homebrew-cask/forks`), pushes a branch to that
fork, then opens the PR upstream. That shape forces a **classic** PAT with the
`repo` scope:
- A fine-grained PAT cannot express it. Its "Repository access" selector only
lists repos owned by the resource owner, so `Homebrew/homebrew-cask` can never
be selected — and Homebrew's API layer authorises against classic OAuth scopes
(`x-oauth-scopes`), which fine-grained tokens do not emit.
- Homebrew declares the requirement in source as
`CREATE_ISSUE_FORK_OR_PR_SCOPES = ["repo"]` (`utils/github.rb`), so the scope
is `repo`.
`CREATE_ISSUE_FORK_OR_PR_SCOPES = ["repo"]` (`utils/github.rb`).
Create it at
And `repo` cannot be narrowed: it grants write to *every* repository the owning
account can reach — including `vitorpamplona/amethyst` itself. Stored as an
Actions secret it would be usable by **anyone with push access to this repo**,
since a pushed branch containing a workflow runs with repo secrets. That is a
strict escalation for a channel that ships one DMG a month.
So the split is:
- **CI** (`bump-homebrew.yml`, `GITHUB_TOKEN` only) does the error-prone
bookkeeping: downloads the DMG, asserts it is notarized + stapled, computes
the sha256, and opens an in-repo PR syncing
`desktopApp/packaging/homebrew/amethyst-nostr.rb`.
- **A maintainer** merges that PR and runs `scripts/bump-homebrew-cask.sh`,
which re-verifies the sha256 and the notarization ticket against the live
asset before calling `brew bump-cask-pr`.
The token then lives only in that maintainer's shell:
```bash
export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope
scripts/bump-homebrew-cask.sh v1.13.2
```
Create one at
<https://github.com/settings/tokens/new?scopes=repo&description=Homebrew%20cask%20bump>.
Prefer a dedicated bot account whose only asset is a fork of `homebrew-cask`, so
a leak reaches nothing else.
**Use a dedicated bot account.** The `repo` scope grants write to *every*
repository the account can reach — including `vitorpamplona/amethyst` itself. A
bot account whose only asset is a fork of `homebrew-cask` bounds the blast
radius of a leaked CI secret to that fork. (`WINGET_TOKEN` has the same
property, which is why a bot account is already recommended for it.) The bot
must have forked `Homebrew/homebrew-cask`, or be able to.
Rotate both on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md`
or equivalent issue tracker. On rotation, paste the new token and run
`gh workflow run bump-homebrew.yml -f tag=<most-recent-stable-tag>` to verify
(the `tag` input is required — that dispatch path exists for exactly this).
Note the verification is only meaningful once the cask exists upstream;
before that the run skips the token-using step entirely.
Rotate `WINGET_TOKEN` on a 90-day cadence. Owner: assigned via `RELEASE_OPS.md`
or equivalent issue tracker.
### Homebrew cask (one-time initial PR)
+26 -8
View File
@@ -210,10 +210,24 @@ Until someone does that bootstrap, a green release run means the bump workflows
*skipped cleanly* — not that Homebrew/Winget shipped. Check the run's warnings
if you want to confirm which case you're in.
The two `amy` / `geode` Homebrew **formula** workflows are different: they only
sync the in-repo reference `.rb` files and open a PR against *this* repo, so
they do real work on every release. Merge those PRs to keep the formulae ready
for their eventual homebrew-core submission.
**The cask bump is half-manual by design.** CI (`bump-homebrew.yml`) does the
bookkeeping with `GITHUB_TOKEN` only — verifies the DMG is notarized + stapled,
computes the sha256, and opens an in-repo PR syncing the reference cask. Pushing
that upstream needs a classic `repo`-scoped PAT, which is deliberately *not* a
CI secret (it would grant write to this repo to anyone with push access), so a
maintainer runs it:
```bash
export HOMEBREW_GITHUB_API_TOKEN=ghp_... # classic PAT, `repo` scope
scripts/bump-homebrew-cask.sh v1.13.2
```
The script re-verifies sha256 + notarization against the live asset and refuses
to submit otherwise. See BUILDING.md § Homebrew cask.
The three in-repo sync workflows (`amy` formula, `geode` formula, `amethyst-nostr`
cask) all open PRs against *this* repo on every release. Merge them to keep the
reference packaging files current.
---
@@ -255,7 +269,7 @@ ownership:
| `SIGNING_KEY`, `KEY_ALIAS`, `KEY_STORE_PASSWORD`, `KEY_PASSWORD` | The **Android upload keystore** — losing/leaking it is the worst case; Play app signing identity | Keep the keystore backed up offline; never rotate casually (Play upload key reset is a support process) |
| `SONATYPE_USERNAME`, `SONATYPE_PASSWORD` | Maven Central namespace `com.vitorpamplona` | On compromise |
| `SIGNING_PRIVATE_KEY`, `SIGNING_PASSWORD` | The **GPG key** signing Maven artifacts | Per GPG key expiry |
| `HOMEBREW_TOKEN`, `WINGET_TOKEN` | Cask + winget bump PRs | **90-day cadence** (see BUILDING.md § Bootstrap) |
| `WINGET_TOKEN` | Winget bump PRs | **90-day cadence** (see BUILDING.md § Bootstrap). No `HOMEBREW_TOKEN` exists — the cask bump runs locally via `scripts/bump-homebrew-cask.sh`, so the `repo`-scoped PAT it needs never becomes a CI secret |
| `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Translation sync | On compromise |
Owner assignments and rotation reminders live with the team (issue tracker).
@@ -274,9 +288,13 @@ Owner assignments and rotation reminders live with the team (issue tracker).
- [ ] Play Console: rollout started, no policy rejection.
- [ ] Zapstore: release event visible.
- [ ] F-Droid: new version detected (may lag days).
- [ ] `amy` / `geode` Homebrew formula-sync PRs opened against this repo — merge them.
- [ ] Homebrew + Winget: **expected to skip** until bootstrapped (§ 3). If they
ever start opening real PRs, that means someone landed the bootstrap.
- [ ] `amy` / `geode` formula + `amethyst-nostr` cask sync PRs opened against
this repo — merge them.
- [ ] Cask pushed upstream: `scripts/bump-homebrew-cask.sh vX.Y.Z` (manual, needs
`HOMEBREW_GITHUB_API_TOKEN` in your shell). Skips with a clear error until
the one-time new-cask PR has been bootstrapped (§ 3).
- [ ] Winget: **expected to skip** until bootstrapped (§ 3). If it ever starts
opening real PRs, that means someone landed the bootstrap.
- [ ] In-app "Release Notes" link opens the note matching `RELEASE_NOTES_ID`
(only bumped on minor releases — patches keep pointing at the x.y.0 note).
- [ ] Push still works on a `play` build (only if the push contract changed —
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
#
# Push the amethyst-nostr cask upstream to Homebrew/homebrew-cask.
#
# This is the ONE release step that is deliberately not automated. `brew
# bump-cask-pr` forks homebrew-cask into the token owner's account, which needs
# a CLASSIC PAT with the `repo` scope — and that scope grants write to every
# repository the account can reach. As a CI secret it would be usable by anyone
# with push access to this repo; in your shell it is not. See BUILDING.md
# § Homebrew cask.
#
# Everything error-prone (version, sha256, notarization check) is already done
# in CI by .github/workflows/bump-homebrew.yml, which opens a PR syncing
# desktopApp/packaging/homebrew/amethyst-nostr.rb. Merge that PR first; this
# script reads the merged values so the two can never disagree.
#
# Usage:
# scripts/bump-homebrew-cask.sh # uses the cask file as-is
# scripts/bump-homebrew-cask.sh v1.13.2 # asserts the cask matches this tag
# DRY_RUN=1 scripts/bump-homebrew-cask.sh # print what would happen, do nothing
#
# Requires: macOS, brew, and HOMEBREW_GITHUB_API_TOKEN exported (classic PAT,
# `repo` scope). Create one at:
# https://github.com/settings/tokens/new?scopes=repo&description=Homebrew%20cask%20bump
set -euo pipefail
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
CASK="$REPO_ROOT/desktopApp/packaging/homebrew/amethyst-nostr.rb"
EXPECTED_TAG="${1:-}"
DRY_RUN="${DRY_RUN:-}"
die() { echo "error: $*" >&2; exit 1; }
[[ "$(uname -s)" == "Darwin" ]] || die "casks are macOS-only; run this on a Mac"
command -v brew >/dev/null || die "brew not found"
[[ -f "$CASK" ]] || die "cask not found at $CASK"
VERSION=$(grep -E '^ version "' "$CASK" | sed -E 's/.*"(.*)".*/\1/')
SHA=$(grep -E '^ sha256 "' "$CASK" | sed -E 's/.*"(.*)".*/\1/')
[[ -n "$VERSION" && -n "$SHA" ]] || die "could not parse version/sha256 out of $CASK"
if [[ -n "$EXPECTED_TAG" && "v$VERSION" != "$EXPECTED_TAG" ]]; then
die "cask is at v$VERSION but you asked for $EXPECTED_TAG.
Merge the 'chore: sync amethyst-nostr cask to $EXPECTED_TAG' PR first, then pull."
fi
URL="https://github.com/vitorpamplona/amethyst/releases/download/v${VERSION}/amethyst-desktop-${VERSION}-macos-arm64.dmg"
echo "cask : amethyst-nostr"
echo "version : $VERSION"
echo "sha256 : $SHA"
echo "url : $URL"
echo
# Re-verify against the live asset. CI already checked this, but the whole point
# of a manual gate is that a human confirms what is about to be advertised to
# every macOS user.
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
echo "==> downloading and verifying the release DMG"
curl -fsSL -o "$TMP/amethyst.dmg" "$URL" || die "could not download $URL"
ACTUAL_SHA=$(shasum -a 256 "$TMP/amethyst.dmg" | awk '{print $1}')
[[ "$ACTUAL_SHA" == "$SHA" ]] || die "sha256 mismatch!
cask says : $SHA
actual : $ACTUAL_SHA"
echo " sha256 matches"
xcrun stapler validate "$TMP/amethyst.dmg" >/dev/null 2>&1 \
|| die "the DMG has NO stapled notarization ticket.
Users would hit a Gatekeeper block. Do not submit this to homebrew-cask.
Check the notarizeReleaseDmg step in .github/workflows/create-release.yml."
echo " notarization ticket stapled"
echo
if [[ -n "$DRY_RUN" ]]; then
echo "DRY_RUN set — would now run:"
echo " brew bump-cask-pr amethyst-nostr --version $VERSION --sha256 $SHA --url $URL"
exit 0
fi
[[ -n "${HOMEBREW_GITHUB_API_TOKEN:-}" ]] || die "HOMEBREW_GITHUB_API_TOKEN is not set.
Create a CLASSIC PAT with the 'repo' scope:
https://github.com/settings/tokens/new?scopes=repo&description=Homebrew%20cask%20bump
then: export HOMEBREW_GITHUB_API_TOKEN=ghp_...
Prefer a dedicated bot account — 'repo' reaches every repo that account can see."
if ! brew info --cask amethyst-nostr >/dev/null 2>&1; then
cat >&2 <<EOF
error: cask 'amethyst-nostr' does not exist in homebrew-cask yet.
\`brew bump-cask-pr\` can only bump an EXISTING cask. The first submission is a
manual, human-reviewed new-cask PR. See BUILDING.md § Homebrew cask
(one-time initial PR) for that flow; come back to this script for every
release after it merges.
EOF
exit 1
fi
echo "==> opening the homebrew-cask PR"
brew bump-cask-pr amethyst-nostr --version "$VERSION" --sha256 "$SHA" --url "$URL" --no-browse
echo
echo "Done. Watch the PR at https://github.com/Homebrew/homebrew-cask/pulls"