Homebrew Tap Conventions
Overview
The forwardimpact/homebrew-tap repository holds two
package families. Seven Homebrew casks sit under
Casks/, one per product plus one shared
fit-gear
bundle. You install the casks on macOS. Seven matching formulae sit
under
Formula/. You install the formulae on Linux. The
tap job in the
publish-binaries.yml workflow builds and uploads the
release assets. The job then updates the affected cask and formula.
It pushes the change directly to the tap's main and
opens no pull request. A human edits every other field in the tap
repo. Those fields survive releases unchanged.
Sed contract
Each release rewrites exactly two lines in the cask file. The
formula's per-arch contract is separate. See the section below.
The tap job's "Update cask and formula, push
to tap main" step runs:
sed -i \
-e "s|^ version \".*\"| version \"${VERSION}\"|" \
-e "s|^ sha256 \".*\"| sha256 \"${SHA256}\"|" \
"tap/Casks/${CASK}.rb"
The two-space indent and the double-quoted value shape are
load-bearing. The sed patterns anchor on
^ version " and ^ sha256 ". Cask
authors must keep this shape. Otherwise the workflow's
substitutions silently miss.
The workflow never touches the other authored fields:
url, name, desc,
homepage, depends_on, app,
binary, livecheck, and zap.
Linux formulae
Linux has no cask, because Homebrew ignores casks there. So each
bundle ships a formula under
Formula/<token>.rb beside its cask. The formula
sits in the same tap and uses the same token. A formula installs the
bundle's per-architecture tarball
(fit-<bundle>-linux-<arch>.tar.gz). Its
install runs
bin.install Dir["*"], because the tarball
holds only the self-contained CLIs:
class FitGear < Formula
desc "Gear CLIs"
homepage "https://www.forwardimpact.team"
version "X.Y.Z"
on_linux do
on_intel do
url ".../gear@v#{version}/fit-gear-linux-x64.tar.gz"
sha256 "<x64>"
end
on_arm do
url ".../gear@v#{version}/fit-gear-linux-arm64.tar.gz"
sha256 "<arm64>"
end
end
def install
bin.install Dir["*"]
end
end
The install command differs by platform. On macOS the documented command installs the cask. On Linux it installs the formula:
brew install --cask <tap>/<bundle> # macOS — unchanged
brew install <tap>/<bundle> # Linux
This addition does not change the macOS --cask path.
The formula is on_linux-only. A bare
brew install <tap>/<bundle> on macOS
resolves to the formula. That formula loads no macOS artifact and
installs nothing. The result is a fail-safe. It is not a wrong
install.
Formula checksum contract
A cask carries one sha256. The flat two-line
sed above rewrites it. A formula carries two, one per
architecture, and both have the same shape. So
build/update-formula.sh keys each
sha256 to the arch token (linux-x64 or
linux-arm64) in the url line above it. The
on_intel and on_arm stanzas never
cross-assign. The same pass rewrites the top-level
version. Each url then picks up the new
version through #{version} interpolation.
Cask topology
The tap has six product casks and one shared bundle. No
depends_on cask: links them. You can install each one
on its own.
graph TD
subgraph Products
pathway[fit-pathway]
map[fit-map]
guide[fit-guide]
landmark[fit-landmark]
summit[fit-summit]
outpost[fit-outpost]
end
gear[fit-gear]
Binary stanza mapping
Each cask exposes only the executables bundled in its own
.app. build/cli-manifest.json is the
source of truth. This table mirrors the CLIs each cask places on
PATH. Regenerate the fit-gear row with
jq -r '.clis[] | select(.bundle == "gear") |
.name' build/cli-manifest.json
when the bundle set changes.
| Cask | Executables on PATH | Count |
|---|---|---|
fit-pathway |
fit-pathway |
1 |
fit-map |
fit-map |
1 |
fit-guide |
fit-guide |
1 |
fit-landmark |
fit-landmark |
1 |
fit-summit |
fit-summit |
1 |
fit-outpost |
fit-outpost |
1 |
fit-gear |
fit-svcgraph, fit-svcmcp,
fit-svcpathway, fit-svcspan,
fit-svcvector, fit-codegen,
fit-terrain, gemba-harness,
fit-doc, fit-rc,
gemba-xmr, fit-storage,
fit-logger, fit-svscan,
gemba-trace, fit-visualize,
fit-process, fit-rag,
fit-unary, fit-tiktoken,
gemba-wiki, gemba-benchmark,
fit-pack, jidoka
|
24 |
Both the .app assembly and the cask
binary block derive from
build/cli-manifest.json for
every bundle. There is one code path and no gear
special-case. build/build-app.sh <bundle> reads
.bundles[<bundle>] for the plist, entitlements,
version source, and any launcher or resources. It reads
.clis[] for the executables. The tap job
runs render-cask-binaries.sh for every cask, so the
linked binaries can never drift from the shipped set. A single-CLI
product yields its one stanza. Gear yields about 30. So when you add
or remove a library or service CLI, an update to
build/cli-manifest.json is enough. The cask block
regenerates on the next release. Mark a long-running service CLI
with "server": true. A long-running service
CLI is one whose bin starts a server. Its
bin does not print --help and then exit.
With that mark, the native build still compiles, checksums, uploads,
and bundles it. Its per-binary smoke gate then does not run it,
because the CLI would hang.
Livecheck regex pattern
Each cask uses the :github_releases strategy with the
cask's own download URL as the source. A per-cask regex anchors
to its tag prefix. Only the releases that match trigger a version
bump:
livecheck do
url :url
strategy :github_releases
regex(/^pathway@v(\d+(?:\.\d+)+)$/i)
end
Each cask substitutes its own tag prefix (pathway,
map, guide, landmark,
summit, outpost, gear).
The ^...$ anchors are essential. Without them, a
map@v2.0.0 release would also match
landmark@v2.0.0 on the shared monorepo releases page.
App install path
All casks install their .app to a
Forward Impact/ subdirectory under
/Applications/. They do not use the top-level folder:
app "fit-pathway.app", target: "Forward Impact/fit-pathway.app"
Binary stanzas reference this subdirectory:
binary "#{appdir}/Forward Impact/fit-pathway.app/Contents/MacOS/fit-pathway"
This subdirectory keeps the seven .app bundles visually
together in Finder. They do not scatter among unrelated
applications.
Zap and uninstall paths
Each cask declares a zap trash: stanza that removes its
preferences plist on brew zap:
| Cask | Zap path |
|---|---|
fit-pathway |
~/Library/Preferences/team.forwardimpact.pathway.plist
|
fit-map |
~/Library/Preferences/team.forwardimpact.map.plist
|
fit-guide |
~/Library/Preferences/team.forwardimpact.guide.plist
|
fit-landmark |
~/Library/Preferences/team.forwardimpact.landmark.plist
|
fit-summit |
~/Library/Preferences/team.forwardimpact.summit.plist
|
fit-outpost |
~/Library/Preferences/team.forwardimpact.outpost.plist
|
fit-gear |
~/Library/Preferences/team.forwardimpact.gear.plist
|
Verification commands
The automated version and sha256 updates do not count as structural changes. Before you merge a tap PR that modifies cask or formula structure, run:
brew style Casks/*.rb
brew audit --new-cask Casks/{cask}.rb
brew audit Formula/{token}.rb
To dry-run the workflow's sed contract locally against a cask:
sed -i \
-e "s|^ version \".*\"| version \"9.9.9\"|" \
-e "s|^ sha256 \".*\"| sha256 \"$(printf 'test' | shasum -a 256 | awk '{print $1}')\"|" \
"Casks/fit-pathway.rb"
On macOS, use gsed (GNU sed) instead of the default BSD
sed. The BSD sed requires a backup suffix
with -i.