Files
ci/bin/pin
T
zeekay 0dc1e28db1 pin: hold a bump back for a REGRESSION, not for a red suite
"Tests failed" was the wrong bar. commerce fails 20 packages on its own main
and failed exactly those 20 after the bump, so holding it back on that reading
would pin a repo to a ten-minor-old library forever — the bar could never be
met, and nothing would say why.

The question is whether the bump made it redder. Compare WHICH packages failed,
not how many, and pay for the baseline only when the first run is red, so a
green repo costs one test run as before. A regression names the packages it
added; an unchanged red suite ships and says how red main already was.

Both paths have tests, one of which drives a stub that fails identically either
way and one that fails an extra package only once the bump is in go.mod. The
earlier version of this block shipped with none, and the sweep found out by
printing a syntax error.
2026-08-22 16:14:07 -07:00

160 lines
7.6 KiB
Bash
Executable File

#!/usr/bin/env bash
# pin — report, and optionally close, the gap between what a repo DEPENDS ON and
# what that dependency has released. One implementation, every repo.
#
# pin <module> <repo>... # report: current -> latest, per repo
# pin --apply <module> <repo>... # bump, build, test; push only what is green
#
# WHY THIS IS A TOOL AND NOT A LOOP SOMEBODY TYPES
#
# A shared library's fix reaches nobody until every consumer moves, and nothing
# in the estate reports who has not. Measured on github.com/zap-proto/zip: 22
# repos depend on it and 2 were current. Twelve were more than ten minors
# behind, one by twenty-six. Each of those is a fix somebody wrote, tested and
# tagged, that no running service has.
#
# The gap is invisible because nothing fails. A stale pin compiles, passes and
# serves; it just serves the OLD behaviour. That is the failure mode this exists
# for — not a broken build, a silent one.
#
# READ THE PIN FROM THE BRANCH, NOT THE CHECKOUT
#
# Every read here is `git show <remote-main>:go.mod`. A working copy is whatever
# somebody last left it as: on the sweep that prompted this, four repos' local
# go.mod disagreed with their own main, two of them by enough to invert the
# answer — a repo reported ten minors behind was already current. A checkout is
# a claim; the branch is the fact.
#
# The remote is `forge` where it exists and `origin` otherwise, because the
# forge is canonical for the repos that have one and a build reads from there.
#
# GREEN MEANS BUILT AND TESTED, NOT EDITED
#
# --apply refuses to push a repo whose build or tests fail after the bump, and
# says which. A bump that compiles is not a bump that works: a library can widen
# behaviour without changing a signature, which is exactly what a consumer's
# tests are for. Repos are independent, so one failure never blocks the rest.
set -uo pipefail
apply=0
[ "${1:-}" = "--apply" ] && { apply=1; shift; }
mod=${1:-}; shift || true
[ -z "$mod" ] || [ $# -eq 0 ] && {
echo "usage: pin [--apply] <module> <repo>..." >&2; exit 2; }
# latest asks the module proxy, so the answer is what a consumer would actually
# resolve — not what a local clone of the library happens to have tagged.
latest=$(go list -m -f '{{.Version}}' "$mod@latest" 2>/dev/null)
[ -z "$latest" ] && { echo "pin: cannot resolve $mod@latest" >&2; exit 1; }
echo "$mod latest $latest"
# A repo that does not depend on the module is not a finding, and neither is a
# path that is not a repo. Printing them buries the answer under the corpus —
# the first sweep printed 26 rows that mattered among 400 that did not.
behind=0 moved=0 broke=0 seen=0
for repo in "$@"; do
name=$(basename "$repo")
[ -d "$repo/.git" ] || continue
# WHICH REMOTE IS THIS REPO'S HOME. Measured across the workspace, nothing
# answers it reliably on its own: a repo may have four remotes, two of them on
# the forge with one archived (iam), or none named origin at all (cloud, ai),
# and branch tracking points at a GitHub mirror as often as not.
#
# So: the forge is canonical, `origin` is the conventional name for it, and a
# forge URL under any other name is the fallback for a repo that has no origin.
# Right on all ten laid out by hand — and PRINTED beside every row, because a
# rule this shape will eventually be wrong somewhere, and a wrong reading that
# is visible costs a glance where a silent one costs a release.
forge() { git -C "$repo" remote get-url "$1" 2>/dev/null | grep -q 'git\.hanzo\.ai'; }
if forge origin; then
remote=origin
else
remote=$(git -C "$repo" remote -v 2>/dev/null |
awk '/git\.hanzo\.ai.*\(fetch\)/ {print $1; exit}')
[ -z "$remote" ] && remote=origin
fi
# Ask the ref we already have BEFORE fetching. Most of a workspace does not
# depend on any one module — the first sweep fetched 400 repos to answer about
# 28 and ran past ten minutes. A repo with no such require line cannot become
# one by fetching.
version() {
git -C "$repo" show "$remote/main:go.mod" 2>/dev/null |
grep -m1 -oE "${mod//\//\\/} v[0-9][0-9.]*" | awk '{print $2}'
}
[ -z "$(version)" ] && continue
git -C "$repo" fetch -q "$remote" 2>/dev/null
have=$(version)
[ -z "$have" ] && continue
seen=$((seen + 1))
[ "$have" = "$latest" ] && continue
behind=$((behind + 1))
[ $apply -eq 0 ] && { printf '%-16s %-10s -> %-10s (%s)\n' "$name" "$have" "$latest" "$remote"; continue; }
# Work on a detached worktree of the branch, never the checkout: a sweep must
# not touch whatever somebody has in progress.
w=$(mktemp -d)/wt
git -C "$repo" worktree add -q --detach "$w" "$remote/main" 2>/dev/null || {
printf '%-16s %-10s WORKTREE FAILED\n' "$name" "$have"; broke=$((broke + 1)); continue; }
# ship commits whatever the bump left in the worktree and pushes it, rebasing
# once if main moved while the build and tests ran — it did, on the first sweep.
ship() {
# Nothing staged means the bump changed no file: not a push failure, and
# reporting it as one sent a reader looking at git credentials for a module
# another requirement already satisfied.
if ! (cd "$w" && git add -A && ! git diff --cached --quiet); then
printf '%-16s %-10s no change at %s\n' "$name" "$have" "$latest"; return
fi
local out
if out=$(cd "$w" &&
git -c user.name=hanzo-dev -c user.email=[email protected] commit -q -m "Depend on $mod $latest" &&
{ git push "$remote" HEAD:main ||
{ git fetch -q "$remote" &&
git -c user.name=hanzo-dev -c user.email=[email protected] rebase "$remote/main" &&
git push "$remote" HEAD:main; }; } 2>&1); then
printf '%-16s %-10s -> %s %s\n' "$name" "$have" "$latest" "${1:-pushed}"
moved=$((moved + 1))
else
printf '%-16s %-10s PUSH FAILED: %s\n' "$name" "$have" \
"$(grep -viE 'lefthook|^remote:|^To |^ ' <<<"$out" | grep -m1 .)"
broke=$((broke + 1))
fi
}
# red lists the packages a run reported failing, so two runs can be compared by
# WHICH failed rather than how many.
red() { grep -oE '^FAIL[[:space:]]+[^[:space:]]+' "$1" 2>/dev/null | awk '{print $2}' | sort -u; }
runs=$(dirname "$w")
if ! (cd "$w" && go get "$mod@$latest" >/dev/null 2>&1 && go mod tidy >/dev/null 2>&1); then
printf '%-16s %-10s GET FAILED\n' "$name" "$have"; broke=$((broke + 1))
elif ! (cd "$w" && go build ./... >/dev/null 2>&1); then
printf '%-16s %-10s BUILD FAILED at %s\n' "$name" "$have" "$latest"; broke=$((broke + 1))
elif (cd "$w" && go test ./... >"$runs/after" 2>&1); then
ship
else
# A RED SUITE IS NOT A REASON TO HOLD BACK A BUMP. It is a reason to ask
# whether the bump made it redder. commerce fails 20 packages on its own main
# and failed the same 20 after; holding it back on "tests failed" would pin a
# repo to a ten-minor-old library forever, because the bar could never be met.
# The baseline is only paid for here, where the answer changes the decision.
(cd "$w" && git checkout -q -- . && git clean -qfd && go test ./... >"$runs/before" 2>&1)
if diff -q <(red "$runs/before") <(red "$runs/after") >/dev/null 2>&1; then
(cd "$w" && go get "$mod@$latest" >/dev/null 2>&1 && go mod tidy >/dev/null 2>&1)
ship "pushed (main already red in $(red "$runs/before" | wc -l | tr -d ' '), unchanged)"
else
printf '%-16s %-10s TESTS REGRESSED at %s: %s\n' "$name" "$have" "$latest" \
"$(comm -13 <(red "$runs/before") <(red "$runs/after") | tr '\n' ' ')"
broke=$((broke + 1))
fi
fi
git -C "$repo" worktree remove --force "$w" 2>/dev/null
rm -rf "$(dirname "$w")"
done
if [ $apply -eq 1 ]; then
echo "$moved moved, $broke held back"
[ $broke -gt 0 ] && exit 1
else
echo "$behind of $seen behind"
fi
exit 0