feat(gate): add --two-pass mode and default local scope to head

Two-pass mode runs pass 1 (--fast --scope head --fail-fast, blocking)
then pass 2 (--scope upstream --accumulate, advisory WARN not FAIL).
Local pushes now default to --scope head (~3s) instead of upstream;
CI keeps upstream. Override with PRE_PUSH_GO_SCOPE=upstream.

Closes soc-7c3v
This commit is contained in:
Boden Fuller
2026-05-04 17:56:43 -04:00
parent 60d7463e2c
commit 5dbe1e6d0b
2 changed files with 225 additions and 3 deletions
+70 -3
View File
@@ -107,8 +107,15 @@ NC='\033[0m'
errors=0
skipped=0
SCOPE="${PRE_PUSH_GO_SCOPE:-upstream}"
SCOPE_EXPLICIT=false
if [[ -n "${PRE_PUSH_GO_SCOPE:-}" ]]; then
SCOPE="$PRE_PUSH_GO_SCOPE"
SCOPE_EXPLICIT=true
else
SCOPE="upstream"
fi
FAST_MODE=false
TWO_PASS=false
FAIL_FAST_SETTING="${PRE_PUSH_FAIL_FAST:-auto}"
FAIL_FAST_EFFECTIVE=false
FAIL_FAST_PENDING=false
@@ -148,6 +155,15 @@ cleanup_hash_snapshot() {
}
blocked_exit() {
echo ""
if truthy "${_PRE_PUSH_ADVISORY:-0}"; then
if [[ "$FAST_MODE" == "true" ]]; then
echo -e "${YELLOW}pre-push gate (fast, advisory): $errors issues found ($skipped skipped)${NC}"
else
echo -e "${YELLOW}pre-push gate (advisory): $errors issues found${NC}"
fi
cleanup_hash_snapshot
exit 0
fi
if [[ "$FAST_MODE" == "true" ]]; then
echo -e "${RED}pre-push gate (fast): BLOCKED ($errors failures, $skipped skipped)${NC}"
else
@@ -157,7 +173,11 @@ blocked_exit() {
exit 1
}
fail() {
echo -e "${RED}FAIL${NC} $1"
if truthy "${_PRE_PUSH_ADVISORY:-0}"; then
echo -e "${YELLOW}WARN${NC} $1 (advisory)"
else
echo -e "${RED}FAIL${NC} $1"
fi
errors=$((errors + 1))
if [[ "${FAIL_FAST_EFFECTIVE:-false}" == "true" ]]; then
FAIL_FAST_PENDING=true
@@ -181,15 +201,19 @@ run_hash_snapshot() {
usage() {
cat <<'EOF'
Usage: scripts/pre-push-gate.sh [--fast] [--scope auto|upstream|staged|worktree|head]
scripts/pre-push-gate.sh --two-pass
Options:
--fast Only run checks relevant to changed files
--scope How to determine changed files (default: upstream)
--scope How to determine changed files (default: head local, upstream CI)
--fail-fast Stop after first blocking failure
--accumulate Continue after failures and report all blocking failures
--two-pass Pass 1: --fast --scope head --fail-fast (blocking)
Pass 2: --scope upstream --accumulate (advisory, WARN not FAIL)
Environment:
PRE_PUSH_FAIL_FAST=0|1|auto default auto: enabled for local --fast, off in CI
PRE_PUSH_TWO_PASS=1 enable two-pass mode via env
PRE_PUSH_RUN_EVAL=1 run eval canaries even when eval files did not change
PRE_PUSH_STRICT_EVAL=1 make local fast eval canaries blocking
PRE_PUSH_AGENT_HEALTH=1 run local fast AgentOps health/ratchet checks
@@ -198,6 +222,10 @@ Environment:
EOF
}
if truthy "${PRE_PUSH_TWO_PASS:-0}"; then
TWO_PASS=true
fi
while [[ $# -gt 0 ]]; do
case "$1" in
--fast)
@@ -206,6 +234,7 @@ while [[ $# -gt 0 ]]; do
;;
--scope)
SCOPE="${2:-}"
SCOPE_EXPLICIT=true
shift 2
;;
--fail-fast)
@@ -216,6 +245,10 @@ while [[ $# -gt 0 ]]; do
FAIL_FAST_SETTING=0
shift
;;
--two-pass)
TWO_PASS=true
shift
;;
-h|--help)
usage
exit 0
@@ -228,6 +261,10 @@ while [[ $# -gt 0 ]]; do
esac
done
if [[ "$SCOPE_EXPLICIT" != "true" ]] && ! is_ci_env; then
SCOPE="head"
fi
case "$SCOPE" in
auto|upstream|staged|worktree|head) ;;
*)
@@ -245,6 +282,28 @@ elif truthy "$FAIL_FAST_SETTING"; then
FAIL_FAST_EFFECTIVE=true
fi
# --- Two-pass mode: re-invoke as pass 1 (blocking) + pass 2 (advisory) ---
if [[ "$TWO_PASS" == "true" ]]; then
SELF="$SCRIPT_DIR/pre-push-gate.sh"
echo "=== Two-pass mode ==="
echo ""
echo "--- Pass 1: HEAD commit (blocking) ---"
set +e
PRE_PUSH_TWO_PASS=0 "$SELF" --fast --scope head --fail-fast
pass1_rc=$?
set -e
if [[ $pass1_rc -ne 0 ]]; then
echo ""
echo -e "${RED}--- Pass 1: FAILED (blocking) ---${NC}"
exit 1
fi
echo -e "${GREEN}--- Pass 1: PASSED ---${NC}"
echo ""
echo "--- Pass 2: upstream range (advisory) ---"
_PRE_PUSH_ADVISORY=1 PRE_PUSH_TWO_PASS=0 "$SELF" --scope upstream --accumulate || true
exit 0
fi
collect_all_changed() {
case "$SCOPE" in
upstream)
@@ -1516,6 +1575,14 @@ fi
maybe_fail_fast
echo ""
if [[ $errors -gt 0 ]]; then
if truthy "${_PRE_PUSH_ADVISORY:-0}"; then
if [[ "$FAST_MODE" == "true" ]]; then
echo -e "${YELLOW}pre-push gate (fast, advisory): $errors issues found ($skipped skipped)${NC}"
else
echo -e "${YELLOW}pre-push gate (advisory): $errors issues found${NC}"
fi
exit 0
fi
if [[ "$FAST_MODE" == "true" ]]; then
echo -e "${RED}pre-push gate (fast): BLOCKED ($errors failures, $skipped skipped)${NC}"
else
+155
View File
@@ -1168,3 +1168,158 @@ GIT
run grep -q 'check-test-home-isolation\.sh' "$SCRIPT"
[ "$status" -eq 0 ]
}
# ─────────────────────────────────────────────────────────────────────────
# soc-7c3v: two-pass mode tests
# ─────────────────────────────────────────────────────────────────────────
@test "pre-push-gate.sh --two-pass re-invokes with pass 1 (head) and pass 2 (upstream)" {
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"diff --name-only"* ]]; then echo ""; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
if [[ "$*" == *"show --name-only"* ]]; then echo ""; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env -u CI -u GITHUB_ACTIONS bash "$GATE" --two-pass
[ "$status" -eq 0 ]
[[ "$output" == *"Two-pass mode"* ]]
[[ "$output" == *"Pass 1"* ]]
[[ "$output" == *"Pass 2"* ]]
}
@test "pre-push-gate.sh --two-pass exits 0 when pass 2 has advisory failures" {
# Pass 1 (head scope) sees no changes → passes.
# Pass 2 (upstream scope) sees a go build failure → advisory, still exit 0.
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"show --name-only"* ]]; then echo ""; exit 0; fi
if [[ "$*" == *"diff --name-only"* && "$*" == *"upstream"* ]]; then echo "cli/main.go"; exit 0; fi
if [[ "$*" == *"diff --name-only"* ]]; then echo ""; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cat > "$MOCK_BIN/go" <<'GO'
#!/usr/bin/env bash
exit 1
GO
chmod +x "$MOCK_BIN/go"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env -u CI -u GITHUB_ACTIONS bash "$GATE" --two-pass
[ "$status" -eq 0 ]
[[ "$output" == *"Pass 1: PASSED"* ]]
[[ "$output" == *"advisory"* ]]
}
@test "pre-push-gate.sh --two-pass exits 1 when pass 1 fails" {
# Pass 1 (head scope) sees a go build failure → blocking, exit 1.
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"show --name-only"* ]]; then echo "cli/main.go"; exit 0; fi
if [[ "$*" == *"diff --name-only"* ]]; then echo "cli/main.go"; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cat > "$MOCK_BIN/go" <<'GO'
#!/usr/bin/env bash
exit 1
GO
chmod +x "$MOCK_BIN/go"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env -u CI -u GITHUB_ACTIONS bash "$GATE" --two-pass
[ "$status" -eq 1 ]
[[ "$output" == *"Pass 1: FAILED (blocking)"* ]]
}
@test "pre-push-gate.sh PRE_PUSH_TWO_PASS=1 enables two-pass via env" {
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"diff --name-only"* ]]; then echo ""; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
if [[ "$*" == *"show --name-only"* ]]; then echo ""; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env -u CI -u GITHUB_ACTIONS PRE_PUSH_TWO_PASS=1 bash "$GATE"
[ "$status" -eq 0 ]
[[ "$output" == *"Two-pass mode"* ]]
}
@test "pre-push-gate.sh defaults to --scope head locally (not CI)" {
run grep -q 'SCOPE_EXPLICIT' "$SCRIPT"
[ "$status" -eq 0 ]
run grep -q 'SCOPE="head"' "$SCRIPT"
[ "$status" -eq 0 ]
}
@test "pre-push-gate.sh keeps --scope upstream in CI" {
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"diff --name-only"* ]]; then echo ""; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env CI=true bash "$GATE" --fast
[ "$status" -eq 0 ]
}
@test "pre-push-gate.sh PRE_PUSH_GO_SCOPE overrides local default" {
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"diff --name-only"* ]]; then echo ""; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env -u CI -u GITHUB_ACTIONS PRE_PUSH_GO_SCOPE=upstream bash "$GATE" --fast
[ "$status" -eq 0 ]
}
@test "pre-push-gate.sh advisory mode prints WARN not FAIL" {
cat > "$MOCK_BIN/git" <<'GIT'
#!/usr/bin/env bash
if [[ "$*" == *"diff --name-only"* ]]; then echo "cli/main.go"; fi
if [[ "$*" == *"rev-parse"* ]]; then echo "/tmp"; fi
exit 0
GIT
chmod +x "$MOCK_BIN/git"
cat > "$MOCK_BIN/go" <<'GO'
#!/usr/bin/env bash
exit 1
GO
chmod +x "$MOCK_BIN/go"
cd "$FAKE_REPO"
export PATH="$MOCK_BIN:$PATH"
run env -u CI -u GITHUB_ACTIONS _PRE_PUSH_ADVISORY=1 bash "$GATE" --scope upstream
[ "$status" -eq 0 ]
[[ "$output" == *"WARN"* ]]
[[ "$output" == *"advisory"* ]]
}