Add Homebrew tap for safe-chain (closes #372)

Adds a publish-homebrew job to the release workflow that renders
Formula/safe-chain.rb from a template (substituting the released
version + per-platform SHA256s parsed from the install script asset)
and pushes it to AikidoSec/homebrew-tap on every non-prerelease.

Users can then install via:

  brew install AikidoSec/tap/safe-chain
  safe-chain setup

The formula downloads the existing prebuilt single-file binaries
from the GitHub release (the same ones the install script uses),
so there is no extra build work in this pipeline.

One-time maintainer setup (creating the AikidoSec/homebrew-tap repo
and adding HOMEBREW_TAP_TOKEN as a secret on safe-chain) is documented
in docs/homebrew.md.

Tested locally on macOS arm64 with Homebrew 5.1.11:
  - brew style: 0 offenses
  - brew install --build-from-source: success
  - brew test: 2 assertions pass (--version + help)
  - brew audit --new: 0 offenses

This PR addresses item 1 of #372 (Homebrew only). The integrity-check
piece in item 2 has already shipped — install-safe-chain.sh already
calls verify_checksum() against the baked-in SHA256s. winget and
Chocolatey are not in scope here; see docs/homebrew.md for notes on
why they belong in separate PRs.
This commit is contained in:
Ethan Setnik 2026-05-15 11:36:57 -04:00
parent 65a8075b0e
commit e976d100f3
3 changed files with 306 additions and 0 deletions

114
docs/homebrew.md Normal file
View file

@ -0,0 +1,114 @@
# Homebrew Tap
End-user install instructions live in the top-level [`README.md`](../README.md#macos--linux-homebrew). This page is for maintainers: how the tap is wired up and what one-time setup is required.
## Architecture
```
AikidoSec/safe-chain AikidoSec/homebrew-tap
┌──────────────────────────┐ ┌──────────────────────────┐
│ tag push (vX.Y.Z) │ │ Formula/ │
│ → create-binaries │ │ safe-chain.rb │
│ → publish-binaries │ │ ^ │
│ (draft release + │ │ │ │
│ install-*.sh with │ │ │ │
│ baked SHA256s) │ │ │ git push │
│ │ │ │ on each release │
│ release.published │ ────────► │ │ │
│ → publish-homebrew │ │ │
│ → publish-npm │ │ │
└──────────────────────────┘ └──────────────────────────┘
```
The `publish-homebrew` job in [`.github/workflows/build-and-release.yml`](../.github/workflows/build-and-release.yml) fires on `release.published` (i.e. when a maintainer publishes the draft release that `publish-binaries` created on the tag push). It:
1. Downloads the release's `install-safe-chain.sh` asset (already has per-platform SHA256s baked in by the earlier `publish-binaries` step — see lines 7686 of the workflow).
2. Parses the four SHA256s it needs (macOS x64/arm64, Linux x64/arm64) out of the install script.
3. Renders `Formula/safe-chain.rb` from a quoted heredoc template, substituting the new version and SHAs.
4. Pushes the rendered formula to the `main` branch of `AikidoSec/homebrew-tap`.
Prereleases are skipped (`if: ... && !github.event.release.prerelease`) so beta tags like `0.0.1-sha256-in-installer-beta` don't update the stable tap.
## One-time setup
This needs to happen exactly once. Until it's done, the `publish-homebrew` job will fail on the next release with a 404 on the tap-repo checkout (which is harmless — it doesn't block npm publish — but the job will appear red).
### 1. Create the tap repository
Create a new public repository under the AikidoSec organisation called **`homebrew-tap`** (the `homebrew-` prefix is required by `brew tap`; users will run `brew tap AikidoSec/tap` which expands to `AikidoSec/homebrew-tap`).
```
Owner: AikidoSec
Repository: homebrew-tap
Visibility: Public
Initialize: Yes (add a README — anything will do, the workflow only touches Formula/safe-chain.rb)
Default branch: main
```
No further repo configuration is required. The `Formula/` directory will be created by the workflow on the first publish.
### 2. Create the token used by the workflow
The workflow needs to push to `AikidoSec/homebrew-tap` from `AikidoSec/safe-chain`. The default `GITHUB_TOKEN` is scoped to the safe-chain repo only and cannot push cross-repo, so we need a token that grants write access to the tap repo.
**Recommended:** a fine-grained personal access token from a maintenance bot account (or a deploy-token-style PAT), scoped to `AikidoSec/homebrew-tap` only.
- Resource owner: `AikidoSec`
- Repository access: **Only select repositories**`AikidoSec/homebrew-tap`
- Repository permissions: **Contents: Read and write**
- Expiration: 1 year (set a calendar reminder; expired tokens silently break releases)
Alternative: a GitHub App installation with `contents:write` on the tap repo and a step that exchanges the App's private key for an installation token. More secure for an org-owned automation but more setup. The PAT path is fine for a low-blast-radius tap.
### 3. Add the token as a secret on `AikidoSec/safe-chain`
Settings → Secrets and variables → Actions → New repository secret:
- Name: `HOMEBREW_TAP_TOKEN`
- Value: the PAT (or App installation token) from step 2
That's it. The next release published from `main` will populate `Formula/safe-chain.rb` in the tap.
### 4. (Optional) Bootstrap with the current release
If you want users to be able to `brew install AikidoSec/tap/safe-chain` immediately rather than waiting for the next release, manually trigger the workflow against the current tag:
```sh
gh workflow run "Create Release" --repo AikidoSec/safe-chain --ref <current-tag>
```
Or simply cut a patch release; the next normal release will populate the tap.
## How users install
```sh
brew install AikidoSec/tap/safe-chain
safe-chain setup
```
`brew install` downloads the prebuilt platform-specific binary from the GitHub release (verified against the SHA256 in the formula) and places it on PATH as `safe-chain`. The user then runs `safe-chain setup` (or `safe-chain setup-ci`) to install the shell aliases that wrap `npm`, `yarn`, `pnpm`, `pip`, etc. This second step is the same as the one in the `curl | sh` install path; Homebrew doesn't (and shouldn't) modify user shell rc files at install time.
## How users upgrade
```sh
brew upgrade safe-chain
```
The formula's `livecheck` block uses GitHub's `releases/latest` strategy, so `brew outdated` and Homebrew's auto-bump tooling pick up new versions automatically once the tap repo has been updated by the release workflow.
## Testing the formula locally
```sh
brew tap AikidoSec/tap
brew install --build-from-source AikidoSec/tap/safe-chain
brew test AikidoSec/tap/safe-chain
brew style $(brew --repository)/Library/Taps/aikidosec/homebrew-tap/Formula/safe-chain.rb
brew audit --new --formula AikidoSec/tap/safe-chain
```
The PR that wired this up (#TBD) validated all four of these against the 1.5.3 release on macOS arm64 (Homebrew 5.1.11).
## Future work
- **Submission to `homebrew-core`:** A custom tap is the lowest-friction path. If we ever want `brew install safe-chain` (no tap prefix) to work, the formula needs to be submitted to Homebrew's central `homebrew-core` repo. That requires the project to meet [Homebrew's notability criteria](https://docs.brew.sh/Acceptable-Formulae) and the formula to build from source (no prebuilt binaries) — which would mean either compiling the Node + pkg bundling in-formula or rewriting the wrapper in a compiled language. Not in scope right now.
- **winget and Chocolatey:** Also requested in [#372](https://github.com/AikidoSec/safe-chain/issues/372). Each has its own manifest format and release-time automation; they should be separate PRs.