mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-07-30 19:46:17 +00:00
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:
@@ -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
@@ -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
@@ -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
@@ -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 —
|
||||
|
||||
Executable
+104
@@ -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"
|
||||
Reference in New Issue
Block a user