Files
SeedPass/scripts/ci_parity.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

52 lines
1.9 KiB
Bash
Executable File

#!/usr/bin/env bash
# The gate that runs before a branch is called ready. Minutes, not seconds.
#
# ci_prepush.sh answers "is this push going to embarrass me". This answers
# "do the two implementations still agree, and does the thing we ship still
# work" -- which is the question SeedPass actually cares about, because the
# TypeScript port only has value if it derives byte-identical secrets to the
# Python it replaces.
#
# Everything expensive lives here on purpose. The cli suite spawns real
# daemons and real subprocesses; the fuzzer runs both implementations over
# generated cases; the packaging check installs the tarball the way a user
# would. None of that belongs on the path of every `git push`.
#
# Fixed seeds, deliberately. GitHub additionally runs an exploratory seed
# derived from the run id so the explored space grows over time; locally you
# want the same cases every run, so a failure is reproducible rather than a
# rumour.
set -euo pipefail
cd "$(dirname "$0")/.."
step() { printf "\n\033[1m==> %s\033[0m\n" "$1"; }
step "everything in ci:prepush"
./scripts/ci_prepush.sh
step "javascript: full test suite (cli included, node + jsdom)"
pnpm -C js test
step "javascript: build and smoke the shipped bundle"
pnpm -C js build
node js/scripts/smoke-cli.mjs
step "python: full test suite"
poetry run pytest -q
step "parity: python <-> typescript cross-implementation"
poetry run python scripts/cross_impl_check.py
step "parity: fixtures still come from the committed generator"
poetry run python scripts/generate_ts_port_fixtures.py
git diff --exit-code -- \
js/packages/test-vectors/fixtures \
':(exclude)js/packages/test-vectors/fixtures/manifest.json'
step "parity: differential fuzz, fixed seeds"
for seed in 1 2 3; do
poetry run python scripts/differential_fuzz.py --cases 150 --seed "$seed"
done
printf "\n\033[32mci:parity passed.\033[0m This branch is ready for review.\n"