Files
boshu2__agentops/scripts/verify-pushed-commit-builds.sh
Bo 5d61bf8a3f fix: kill the shared-config gremlin class (hook-safe repo-root, git-env scrubbing, config-hygiene gate) (#922)
Fixes the three gremlins observed 2026-07-18 (bead
age-gate-scripts-worktree-gitdir-p62wo):

1. **Gate scripts broke under hook env from linked worktrees** (#919
regression): new scripts/lib/repo-root.sh (GIT_*-scrubbed resolution,
BASH_SOURCE anchor, worktree-correct) swept across 32 scripts. Proof:
this PR's own push ran the pre-push gates from a linked worktree and
passed.
2. **core.bare=true recurrence**: mechanism proven (leaked GIT_DIR +
no-arg git init --bare rewrites the SHARED config); writer traced with
high confidence to a live Gas City city process (concrete suspect:
gascity cmd_rig_test.go runGitInTest, unscrubbed env + no-arg init
--bare — fork-side fix filed separately). Defense: new
always.git-config-hygiene gate fails fast on core.bare/test identities
with the repair command inline (--self-test proves fail-closed).
3. **Test/test@test.com identity leak**: writer convicted — our own
pre-push chain leaked GIT_DIR into go test, whose helpers wrote the
shared config via git -C (mechanism proven). Cut at both ends:
validate-go-fast scrubs env for children; every test git-helper set-site
scrubs; check-test-isolation gains a WARN-ratchet rule (baseline 3 →
FAIL at 0).

Gates: shellcheck rc=0 across 36 scripts; go build/vet clean;
655+379+149 tests; hygiene self-test; both originally-failing validators
pass under the repro env.
2026-07-18 18:20:16 -04:00

135 lines
6.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# verify-pushed-commit-builds.sh — close the partial-commit-lands-broken escape (age-yy24).
#
# The pre-push gate (`ao gate check`, the worktree `go build ./...`) validates the
# WORKING TREE, not the COMMIT being pushed. A commit that silently omitted files
# (a multi-path `git add` that aborted on a deleted-file pathspec, dropping the rest)
# builds in the worktree but is build-broken as committed — it passed the gate + a
# cross-family pawl and landed origin/main red once (2026-06-22). This judges the
# COMMIT, not the tree: it builds each pushed commit in an ISOLATED temp worktree.
#
# It only does work when the worktree DIFFERS from HEAD for tracked-or-untracked
# source — a clean tree means worktree==HEAD, so the gate's worktree build already
# validated the commit (zero cost on the common clean-tree push, zero false alarms:
# it never fails merely because the user kept editing after committing).
#
# Usage:
# verify-pushed-commit-builds.sh < <git-pre-push-stdin> # lines: <lref> <lsha> <rref> <rsha>
# verify-pushed-commit-builds.sh <sha> [<sha> ...] # explicit shas (tests / manual)
#
# Exit: 0 = all pushed commits build (or skipped clean / infra-skip); 1 = a commit does
# NOT build (fail-closed — refuse the push). Fail-OPEN on infrastructure errors
# (mktemp / `git worktree add` failure) so a broken check never wedges a push.
#
# Env:
# AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD=1 skip entirely (emergency bypass)
# AGENTOPS_COMMIT_BUILD_CMD build command run inside the temp worktree
# (default: "cd cli && go build ./...")
# AGENTOPS_COMMIT_BUILD_PATHS pathspecs for the dirty check (default: "cli scripts")
set -u
if [ "${AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD:-0}" = "1" ]; then
exit 0
fi
# shellcheck disable=SC1007,SC1091
. "$(CDPATH= cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/repo-root.sh"
REPO_ROOT="$(resolve_repo_root)"
BUILD_CMD="${AGENTOPS_COMMIT_BUILD_CMD:-cd cli && go build ./...}"
# Intentional word-split of the space-separated pathspec list into array elements.
# shellcheck disable=SC2206
DIRTY_PATHS=(${AGENTOPS_COMMIT_BUILD_PATHS:-cli scripts})
ZERO="0000000000000000000000000000000000000000"
# worktree_matches_head: true when the worktree equals HEAD for the watched source
# paths — both tracked changes AND untracked source files. In that case the gate's
# worktree build already validated the committed state, so there is nothing to add.
worktree_matches_head() {
git -C "$REPO_ROOT" diff --quiet HEAD -- "${DIRTY_PATHS[@]}" 2>/dev/null || return 1
local others
others="$(git -C "$REPO_ROOT" ls-files --others --exclude-standard -- "${DIRTY_PATHS[@]}" 2>/dev/null | grep -cE '\.(go|sh)$')"
[ "${others:-0}" -eq 0 ]
}
# build_commit_isolated <sha>: 0 = builds (or infra-skip), 1 = definitively does NOT build.
build_commit_isolated() {
local sha="$1" tmpwt rc=0
tmpwt="$(mktemp -d "${TMPDIR:-/tmp}/agentops-commitbuild.XXXXXX" 2>/dev/null)" || return 0
if ! git -C "$REPO_ROOT" worktree add --detach --quiet "$tmpwt" "$sha" 2>/dev/null; then
echo >&2 "⚠ verify-pushed-commit-builds: could not create a temp worktree for ${sha} — skipping (fail-open)."
rm -rf "$tmpwt" 2>/dev/null
return 0
fi
if ! ( cd "$tmpwt" && eval "$BUILD_CMD" ) >/dev/null 2>&1; then
rc=1
fi
git -C "$REPO_ROOT" worktree remove --force "$tmpwt" >/dev/null 2>&1 || true
rm -rf "$tmpwt" 2>/dev/null
git -C "$REPO_ROOT" worktree prune >/dev/null 2>&1 || true
return "$rc"
}
# MAX_COMMITS caps the isolated builds so a huge range can't wedge a push for minutes;
# beyond it we validate the tip + the most-recent MAX_COMMITS and warn (the tip is what
# main becomes; deep history is rarely first-introduced in one push).
MAX_COMMITS="${AGENTOPS_COMMIT_BUILD_MAX:-12}"
# Collect the commits to validate. Two modes:
# explicit args -> exactly those shas (tests / manual), no range expansion or skip.
# git stdin -> for each `<lref> <lsha> <rref> <rsha>` push line, the pushed RANGE
# `rsha..lsha` (every NEW commit), not just the tip — a tip that
# builds can still push a build-broken intermediate (refuted 2026-06-22).
# New branches / unknown rsha fall back to the tip only.
shas=()
explicit=0
clean=0
worktree_matches_head && clean=1
if [ "$#" -gt 0 ]; then
explicit=1
shas=("$@")
elif [ ! -t 0 ]; then
while read -r _lref lsha _rref rsha; do
[ -z "${lsha:-}" ] || [ "$lsha" = "$ZERO" ] && continue
# Clean tree: the gate's worktree build already validated the TIP (lsha == HEAD),
# but NEVER the intermediates — so only the tip is droppable when clean.
range_commits=""
if [ -n "${rsha:-}" ] && [ "$rsha" != "$ZERO" ] && git -C "$REPO_ROOT" cat-file -e "${rsha}^{commit}" 2>/dev/null; then
range_commits="$(git -C "$REPO_ROOT" rev-list --reverse "${rsha}..${lsha}" 2>/dev/null)"
else
range_commits="$lsha"
fi
for c in $range_commits; do
# On a clean tree, the tip is covered by the worktree build — skip just it.
{ [ "$clean" -eq 1 ] && [ "$c" = "$lsha" ]; } && continue
shas+=("$c")
done
done
fi
[ "${#shas[@]}" -eq 0 ] && exit 0
# Explicit-arg mode honors the clean-tree skip too (so the hook's clean fast-path holds),
# unless a sha was given that is not HEAD.
if [ "$explicit" -eq 1 ] && [ "$clean" -eq 1 ]; then
head_sha="$(git -C "$REPO_ROOT" rev-parse HEAD 2>/dev/null)"
filtered=()
for c in "${shas[@]}"; do [ "$c" = "$head_sha" ] || filtered+=("$c"); done
shas=("${filtered[@]}")
[ "${#shas[@]}" -eq 0 ] && exit 0
fi
if [ "${#shas[@]}" -gt "$MAX_COMMITS" ]; then
echo >&2 "⚠ verify-pushed-commit-builds: ${#shas[@]} pushed commits — validating only the most recent ${MAX_COMMITS}."
shas=("${shas[@]: -$MAX_COMMITS}")
fi
for sha in "${shas[@]}"; do
if ! build_commit_isolated "$sha"; then
echo >&2 "✗ pre-push (age-yy24): a COMMIT being pushed (${sha}) does NOT build, though the working tree does."
echo >&2 " Partial-commit class — 'git show ${sha} --stat' likely omits files the code needs. Push refused."
echo >&2 " (override only with cause: AGENTOPS_PREPUSH_SKIP_COMMIT_BUILD=1)"
exit 1
fi
done
exit 0