diff --git a/.github/workflows/bump-homebrew-formula.yml b/.github/workflows/bump-homebrew-formula.yml index 53c7ae42af..ecdc431d54 100644 --- a/.github/workflows/bump-homebrew-formula.yml +++ b/.github/workflows/bump-homebrew-formula.yml @@ -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= --sha256=` 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= --sha256=` 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() diff --git a/.github/workflows/bump-homebrew-geode-formula.yml b/.github/workflows/bump-homebrew-geode-formula.yml index 3c38519c64..bb2e965b42 100644 --- a/.github/workflows/bump-homebrew-geode-formula.yml +++ b/.github/workflows/bump-homebrew-geode-formula.yml @@ -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= --sha256=` 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= --sha256=` 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() diff --git a/.github/workflows/bump-homebrew.yml b/.github/workflows/bump-homebrew.yml index df281a199a..483afcdead 100644 --- a/.github/workflows/bump-homebrew.yml +++ b/.github/workflows/bump-homebrew.yml @@ -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'] }); diff --git a/BUILDING.md b/BUILDING.md index 6cfc3ee639..f817b78a0e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -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 . +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=` 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) diff --git a/RELEASE_OPS.md b/RELEASE_OPS.md index ab641f769c..5eca4e3694 100644 --- a/RELEASE_OPS.md +++ b/RELEASE_OPS.md @@ -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 — diff --git a/scripts/bump-homebrew-cask.sh b/scripts/bump-homebrew-cask.sh new file mode 100755 index 0000000000..d0f68a4b42 --- /dev/null +++ b/scripts/bump-homebrew-cask.sh @@ -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 < 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"