From cf38a17015d858e05f9c7304148a2319c215da42 Mon Sep 17 00:00:00 2001 From: Etan Joseph Heyman Date: Wed, 15 Jul 2026 14:34:23 +0300 Subject: [PATCH] fix(release): hermeticize release verification --- .githooks/pre-push | 2 +- docs/releases-and-brew.md | 19 ++++++++++++++++--- scripts/release-verify.sh | 22 ++++++++++++++++++++++ scripts/release.sh | 6 +++--- tests/pre-pr-scripts.test.ts | 32 +++++++++++++++++++++++++++++++- 5 files changed, 73 insertions(+), 8 deletions(-) create mode 100755 scripts/release-verify.sh diff --git a/.githooks/pre-push b/.githooks/pre-push index ab68eed8..7e954cab 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -7,7 +7,7 @@ set -uo pipefail [ ! -f scripts/run_tests.sh ] && { echo "⚠️ no scripts/run_tests.sh"; exit 0; } -bash scripts/run_tests.sh +env -u CMUX_SOCKET_PATH -u CMUX_DAEMON_SOCKET bash scripts/run_tests.sh exit_status=$? if [ "$exit_status" -ne 0 ]; then diff --git a/docs/releases-and-brew.md b/docs/releases-and-brew.md index 73ac9904..16a0c950 100644 --- a/docs/releases-and-brew.md +++ b/docs/releases-and-brew.md @@ -92,12 +92,21 @@ One command does the whole pipeline: It will: verify a clean tree + green build/tests, bump `package.json`, commit, push `main`, tag `vX.Y.Z`, then update the Homebrew formula's `url` + `sha256` -in `~/Gits/homebrew-layers` and push the tap. Afterwards: +in `~/Gits/homebrew-layers` and push the tap. Afterwards, run the verification +helper with the released version: ```bash -brew update && brew upgrade etanhey/layers/cmuxlayer +~/Gits/cmuxlayer/scripts/release-verify.sh 0.3.0 ``` +The helper fetches and hard-resets the tap clone Homebrew actually reads at +`$(brew --repository)/Library/Taps/etanhey/homebrew-layers` to `origin/main`, +runs `brew upgrade etanhey/layers/cmuxlayer`, and fails unless +`brew list --versions cmuxlayer` prints exactly the released version. This +explicit sync is required because that tap's `origin` can be the local +`~/Gits/homebrew-layers` checkout while its `main` has no upstream tracking, so +`brew update` alone does not guarantee a fast-forward. + Manual equivalent, if you prefer: 1. `package.json` → bump `version`. @@ -107,7 +116,10 @@ Manual equivalent, if you prefer: 5. In `~/Gits/homebrew-layers/Formula/cmuxlayer.rb` set `url` to the new tag and `sha256` to the value from step 4; `brew audit etanhey/layers/cmuxlayer`; commit + push. -6. `brew upgrade etanhey/layers/cmuxlayer`. +6. Sync the tap Homebrew reads: + `git -C "$(brew --repository)/Library/Taps/etanhey/homebrew-layers" fetch origin && git -C "$(brew --repository)/Library/Taps/etanhey/homebrew-layers" reset --hard origin/main`. +7. `brew upgrade etanhey/layers/cmuxlayer`. +8. Assert `brew list --versions cmuxlayer` is exactly `cmuxlayer X.Y.Z`. The formula also carries a `head` block, so `--HEAD` installs always track `main` with **no** sha/tag bump — that is the on-the-go dogfood path. @@ -197,3 +209,4 @@ explicit `workspace` only when you deliberately want a different one. | `~/.golems/bin/cmuxlayer-mcp` | launcher: brew (default) vs live source (`CMUXLAYER_DEV=1`) | | `EtanHey/homebrew-layers` → `Formula/cmuxlayer.rb` | the brew formula (stable tag + `head`) | | `scripts/release.sh` | one-command release: bump → tag → formula bump → push | +| `scripts/release-verify.sh` | sync Homebrew's tap clone → upgrade → assert installed version | diff --git a/scripts/release-verify.sh b/scripts/release-verify.sh new file mode 100755 index 00000000..4b82cab6 --- /dev/null +++ b/scripts/release-verify.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Sync the tap clone Homebrew actually reads, upgrade cmuxlayer, and verify it. +set -euo pipefail + +die() { echo "release-verify: $*" >&2; exit 1; } + +VERSION="${1:-}" +[ -n "$VERSION" ] || die "usage: release-verify.sh " +[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must be semver X.Y.Z, got '$VERSION'" +command -v brew >/dev/null 2>&1 || die "brew is not installed" + +BREW_TAP_DIR="$(brew --repository)/Library/Taps/etanhey/homebrew-layers" +[ -d "$BREW_TAP_DIR/.git" ] || die "Homebrew tap clone not found at $BREW_TAP_DIR" + +git -C "$BREW_TAP_DIR" fetch origin +git -C "$BREW_TAP_DIR" reset --hard origin/main +brew upgrade etanhey/layers/cmuxlayer + +INSTALLED="$(brew list --versions cmuxlayer)" +[ "$INSTALLED" = "cmuxlayer $VERSION" ] || die "expected cmuxlayer $VERSION, got '${INSTALLED:-not installed}'" + +echo "release-verify: cmuxlayer $VERSION is installed from the synced tap" diff --git a/scripts/release.sh b/scripts/release.sh index d14df18e..2ac3ebd6 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -8,7 +8,7 @@ # # Steps: clean-tree + green build/tests gate → bump package.json → commit + # push main → tag vX.Y.Z + push tag → update formula url+sha256 in the -# homebrew-layers tap → push tap → tell you to `brew upgrade`. +# homebrew-layers tap → push tap → tell you to run release verification. # # See docs/releases-and-brew.md. set -euo pipefail @@ -50,7 +50,7 @@ fi echo "release: gating on typecheck + tests…" run "bun run typecheck" -run "bun run test" +run "env -u CMUX_SOCKET_PATH -u CMUX_DAEMON_SOCKET bun run test" echo "release: gating on real-cmux contracts (warn-only skip when no live CMUX_SOCKET_PATH is reachable)…" run "bun run test:contract" @@ -98,7 +98,7 @@ cat < { it("runs the opt-in contract lane from the release preflight", () => { const release = readFileSync(join(repoRoot, "scripts", "release.sh"), "utf8"); - const hermeticGate = release.indexOf('run "bun run test"'); + const hermeticGate = release.indexOf( + 'run "env -u CMUX_SOCKET_PATH -u CMUX_DAEMON_SOCKET bun run test"', + ); const contractGate = release.indexOf('run "bun run test:contract"'); expect(hermeticGate).toBeGreaterThan(-1); expect(contractGate).toBeGreaterThan(hermeticGate); }); + it("removes ambient cmux socket pins from the pre-push regression gate", () => { + const hook = readFileSync( + join(repoRoot, ".githooks", "pre-push"), + "utf8", + ); + + expect(hook).toContain( + "env -u CMUX_SOCKET_PATH -u CMUX_DAEMON_SOCKET bash scripts/run_tests.sh", + ); + }); + + it("syncs the brew tap clone and verifies the installed release version", () => { + const verify = readFileSync( + join(repoRoot, "scripts", "release-verify.sh"), + "utf8", + ); + const release = readFileSync(join(repoRoot, "scripts", "release.sh"), "utf8"); + + expect(verify).toContain( + 'BREW_TAP_DIR="$(brew --repository)/Library/Taps/etanhey/homebrew-layers"', + ); + expect(verify).toContain('git -C "$BREW_TAP_DIR" fetch origin'); + expect(verify).toContain('git -C "$BREW_TAP_DIR" reset --hard origin/main'); + expect(verify).toContain("brew upgrade etanhey/layers/cmuxlayer"); + expect(verify).toContain("brew list --versions cmuxlayer"); + expect(release).toContain('scripts/release-verify.sh "$VERSION"'); + }); + it("refuses the live harness unless CMUX_LIVE_HARNESS=1 is set", () => { const result = spawnSync( process.execPath,