Pulse/scripts/dev-prepush.sh
courtmanr@gmail.com 8aeffc456d Parallelize Build and Test and add a local pre-push check
The Build and Test workflow ran everything in one serial job — frontend
lint/tests/build, the full go test -race sweep, and benchmarks — for a
37-55 minute wall clock on every push. The job now splits into parallel
jobs: frontend (lint, unit tests, type-check, bundle build and size gate),
backend tests sharded three ways with internal/api on its own shard since
it alone takes ~10m under -race, script smoke tests with the backend
build, and benchmarks with the existing baseline cache flow. Backend jobs
satisfy the frontend embed with a stub index.html, which unit tests do
not assert against; the real bundle is still built and size-gated by the
frontend job and by release builds. Expected wall clock is roughly the
slowest single job, 10-15 minutes.

A new changes job skips the build/test jobs when a push touches only
docs/ and Markdown, mirroring the path filters Core E2E already has. The
gitleaks secret scan deliberately stays unconditional since this workflow
is the only per-push scan and docs can leak secrets too.

scripts/dev-prepush.sh runs the checks that most often turn main red,
scoped to the outgoing commits: the canonical completion guard in CI
mode, the registry snapshot tests when registry.json changed, the
mutation registry audits, build plus tests for touched Go packages, and
the frontend type-check. It exists because CI verdicts arrive well after
the push; today's unclassified force-fail route would have failed it
locally in about a minute.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 14:32:01 +01:00

99 lines
4.1 KiB
Bash
Executable file

#!/usr/bin/env bash
#
# Fast local validation before pushing to main. CI takes 10+ minutes to
# deliver a verdict; this catches the common failure classes in a few:
#
# 1. canonical completion guard (contract/verification coupling, per commit)
# 2. guard/registry snapshot tests when the subsystem registry changed
# 3. mutation registry audits (fail closed on unclassified API routes)
# 4. compilation plus tests for the Go packages the outgoing commits touch
# 5. frontend type-check when frontend-modern changed
#
# Usage: scripts/dev-prepush.sh [base-ref]
# base-ref defaults to origin/main. Run `git fetch origin` first for an
# accurate range. Tests here run without -race for speed; CI still runs
# the full -race suite across all packages.
set -uo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT_DIR"
BASE="${1:-origin/main}"
FAILURES=0
step() { printf '\n=== %s ===\n' "$1"; }
fail() { printf 'FAIL: %s\n' "$1"; FAILURES=$((FAILURES + 1)); }
if ! git rev-parse --verify --quiet "$BASE" >/dev/null; then
echo "Base ref $BASE not found; run git fetch origin first." >&2
exit 2
fi
if ! git diff --quiet || ! git diff --cached --quiet; then
echo "Note: working tree has uncommitted changes; only committed work is checked."
fi
AHEAD=$(git rev-list --count "$BASE"..HEAD)
if [ "$AHEAD" -eq 0 ]; then
echo "No commits ahead of $BASE; nothing to check."
exit 0
fi
CHANGED=$(git diff --name-only "$BASE"...HEAD)
CHANGED_GO=$(printf '%s\n' "$CHANGED" | grep -E '\.go$' || true)
step "Canonical completion guard (HEAD commit, CI mode)"
if [ "$AHEAD" -gt 1 ]; then
echo "Note: $AHEAD commits ahead; the guard is checked for HEAD only here, CI checks each commit."
fi
if ! git diff --name-only HEAD~1 HEAD | \
python3 scripts/release_control/canonical_completion_guard.py --files-from-stdin --diff-base HEAD~1; then
fail "canonical completion guard"
fi
if printf '%s\n' "$CHANGED" | grep -q 'docs/release-control/v6/internal/subsystems/registry.json'; then
step "Registry snapshot tests (registry.json changed)"
python3 scripts/release_control/canonical_completion_guard_test.py || fail "guard snapshot test"
python3 scripts/release_control/subsystem_lookup_test.py || fail "subsystem lookup snapshot test (slow, ~3m)"
fi
if [ -n "$CHANGED_GO" ]; then
# internal/api embeds frontend-modern/dist; a stub keeps compilation working
# in worktrees that never built the frontend. CI does the same for test jobs.
if [ ! -f internal/api/frontend-modern/dist/index.html ]; then
mkdir -p internal/api/frontend-modern/dist
printf '<!doctype html><title>local embed stub</title>\n' > internal/api/frontend-modern/dist/index.html
echo "Note: created frontend embed stub (dist was absent in this checkout)."
fi
step "Go build"
go build ./... || fail "go build"
step "Mutation registry audits"
go test ./internal/mutationregistry ./internal/ai/tools -count=1 \
-run 'Test(EveryRegisteredMutationHasDisposition|InfrastructureAPIRoutesResolveToRegistry|TransportCommandCatalogsResolveToRegistry|PatrolJobRegistrationResolvesToRegistry|RuntimeCandidateAuditNegativeFixtures|ActionRouteMethodAuthorityIsExactAndLookalikesFailClosed|NonAdmittingTransportMessagesCannotCarryDispatchAuthority|UnknownTransportLookalikeFailsClosed|RegisteredModelMutationSchemasResolveToClosedRegistry|RetiredMutationAliasesCannotShadowExtensions)' \
|| fail "mutation registry audits"
step "Go tests for touched packages (no -race locally)"
PKGS=$(printf '%s\n' "$CHANGED_GO" | xargs -n1 dirname | sort -u | sed 's|^|./|')
echo "$PKGS"
# shellcheck disable=SC2086
go test -count=1 -timeout 15m $PKGS || fail "go tests for touched packages"
fi
if printf '%s\n' "$CHANGED" | grep -q '^frontend-modern/'; then
step "Frontend type-check"
if [ -d frontend-modern/node_modules ]; then
(cd frontend-modern && npm run type-check) || fail "frontend type-check"
else
echo "Skipped: frontend-modern/node_modules missing (run npm ci there first)."
fi
fi
step "Result"
if [ "$FAILURES" -gt 0 ]; then
echo "$FAILURES check(s) failed; fix before pushing."
exit 1
fi
echo "All pre-push checks passed."