Files
SeedPass/scripts/ci_prepush.sh
T
thePR0M3TH3ANandClaude Opus 5 864229bad1 Add local CI: a preflight that refuses to lie, and a gate under a minute
Today cost roughly sixteen push-and-wait cycles discovering serially what one
local run could have surfaced at once. This is the local half of that, built
around one rule: a local gate is worth having only if it reaches the same
verdict CI would.

THE PREFLIGHT IS THE POINT

scripts/ci_preflight.py checks that this machine agrees with the lockfiles,
and refuses everything downstream when it does not. That is not theoretical
hygiene. Run right now it says:

    black 26.1.0 != locked 24.10.0
    pytest 9.0.2 != locked 8.4.1
    coverage 7.10.1 != locked 7.10.2

which is exactly today's failure: `poetry run black --check .` passes here and
fails on GitHub, and an earlier commit titled "satisfy the black gate" had
already reformatted five files with the WRONG black, making the gate worse
while looking like a fix. A green local gate that disagrees with CI is not a
weak check, it is a harmful one, because people stop reading the real result.

It is stdlib-only and run by the system interpreter, so it works when the
environment it is checking is broken -- which is when it is needed. And it
only ever CHECKS. scripts/dev_sync.sh repairs. A gate that silently fixes
your environment hides the drift it exists to surface.

TOOLS COME FROM THE REPOSITORY, NEVER FROM PATH

Every invocation is `poetry run ...` or `pnpm -C js ...`. A bare tool name is
precisely how a local gate ends up disagreeing with CI, so the preflight
inspects those environments rather than PATH.

THE BUDGET IS THE DESIGN

Measured here: preflight 2.6s, lock check 0.6s, black 2.5s, typecheck 7.3s,
core vitest 24.0s, determinism subset 5.4s — about 44s. What that excludes
was measured too: the cli suite alone is 176.3s, three times the whole
budget, and full pytest is ~350s. Those go in ci_parity.sh, which is what you
run before calling a branch ready. A hook that takes minutes becomes
`git push --no-verify` inside a week, at which point it is a control everyone
believes in and nobody runs.

The cli suite is slow for a good reason — real daemons, real sockets, real
subprocesses instead of mocks — so the answer is to run it less often, not to
make it lie faster.

THE SMOKE TEST IS NOW NODE, NOT BASH

js/scripts/smoke-cli.mjs replaces an inline shell test that used mktemp,
export and `test -n`, and therefore could only run on Unix — leaving the one
artifact users actually execute never smoke-tested on Windows. Every Windows
failure today was a Unix assumption leaking. It also checks more than the
original: that the bundle runs with no node_modules in sight, that the secret
honours the requested length, and that revealing twice yields the same value,
because determinism is the product rather than an incidental property.

The lint step is written but commented out: `pnpm -C js lint` still exits
non-zero on 35 known assertions, and shipping a red check into the push gate
is how gates get routed around. It goes in when it goes green.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
2026-08-20 13:21:38 -04:00

79 lines
2.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# The gate that runs before every push. Target: well under a minute.
#
# WHAT IS IN HERE AND WHY THAT LIST
#
# The budget is the design constraint, not an afterthought. A hook that takes
# minutes becomes `git push --no-verify` within a week, at which point it is a
# ceremonial control that everyone believes in and nobody runs. So this holds
# only checks that are cheap AND catch things CI would otherwise catch late.
#
# Measured on this machine, 2026-08-20:
#
# preflight 2.6s
# poetry check --lock 0.6s
# black --check 2.5s
# oxlint 0.9s (not enabled yet, see below)
# tsc --noEmit (workspace) 7.3s
# pytest determinism subset 5.4s
# vitest core (node) 24.0s
# -----
# ~44s
#
# What is deliberately NOT here:
#
# vitest cli 176.3s <- alone, three times the budget
# pytest (full) ~350s
# cross-impl, fuzz, packaging smoke
#
# Those live in ci_parity.sh, which is what you run before calling a branch
# ready rather than before every push. The cli suite is slow for a good
# reason -- it spawns real daemons, real sockets and real subprocesses rather
# than mocking them -- and that is worth keeping, not worth waiting on twenty
# times a day.
#
# EVERY TOOL IS INVOKED THROUGH THE REPOSITORY ENVIRONMENT. `poetry run ...`
# and `pnpm -C js ...`, never a bare `black` or `vitest` off PATH. That rule is
# the whole reason preflight runs first: a bare tool name is how a local gate
# ends up passing while CI fails.
set -euo pipefail
cd "$(dirname "$0")/.."
step() { printf "\n\033[1m==> %s\033[0m\n" "$1"; }
step "preflight: does this machine agree with the lockfiles?"
python3 scripts/ci_preflight.py
step "python: lockfile consistency"
poetry check --lock
step "python: formatting (the pinned black, not whatever is on PATH)"
poetry run black --check .
# NOT YET. `pnpm -C js lint` currently exits non-zero on 35 known chained type
# assertions -- one design problem (VaultIndex does not model what a vault
# index contains) counted 35 times. Putting a red check in the push gate is
# how a gate becomes something people route around, so it goes in when it goes
# green, not before. Track it with: pnpm -C js lint
#
# step "javascript: lint"
# pnpm -C js lint
step "javascript: typecheck"
pnpm -C js typecheck
step "javascript: core tests"
pnpm -C js --filter @seedpass/core test:node
step "python: deterministic artifact regression"
poetry run pytest -q src/tests/test_deterministic_artifact_regression.py
if command -v actionlint >/dev/null 2>&1; then
step "workflows: actionlint"
actionlint
else
printf "\n\033[2m(actionlint not installed — skipping workflow lint)\033[0m\n"
fi
printf "\n\033[32mci:prepush passed.\033[0m Run ./scripts/ci_parity.sh before calling a branch ready.\n"