Files
SeedPass/.github/workflows/ts-parity.yml
T
thePR0M3TH3ANandClaude Opus 5 4edf50e5de Attest the bundle on three platforms; give agents two gates with meanings
Adds ts-platform.yml and finishes the local-CI split.

THE DIVISION OF LABOUR

Properties of the CODE — cross-implementation agreement, fixture drift,
differential fuzzing — are asked once, on Linux, because asking them three
times measures nothing extra. ts-platform asks the questions where the
platform IS the variable: does the bundle build, lint, test, run and install
on a machine that is not the one we develop on.

That is not theoretical. Every CI failure today that survived local checking
was a platform difference: MSYS2 shadowing `python` on PATH, pwsh discarding
a non-final exit code, portalocker needing pywin32, shlex eating the
backslashes out of Windows paths in the TUI. None are visible from Linux, and
all of them would have reached users' machines rather than ours. With
TypeScript becoming the only implementation, the bundle working on Windows
stops being a nice-to-have.

The workflow sets `shell: bash` for every step, including Windows, for the
reason found today: under pwsh only the last command in a multi-line block
decides whether a step failed.

ts-parity's inline bash smoke test is replaced by the Node one, so a single
smoke test now runs locally, in parity, and on all three platforms — and it
checks that revealing an entry twice gives the same secret, rather than that
some output appeared.

REPRODUCIBLE BUILDS, AS AN EXPERIMENT

A second job compares the bundle's SHA-256 across the three platforms,
marked continue-on-error. Two builds on one machine are already
byte-identical, so the cross-platform question is real rather than a fantasy.
If the three agree that is an invariant worth promoting to required; if they
do not, the interesting part is why, and neither answer is worth blocking a
merge over before we know which.

TWO GATES WITH MEANINGS

AGENTS.md now says an agent may not report a task complete or push until
ci_prepush passes, and may not call a branch merge-ready until ci_parity
passes — plus the two rules that make those gates trustworthy: invoke tools
through the repository rather than off PATH, and never add a check that
cannot fail. Both rules are written with the evidence from today attached,
because a rule with its reason is one people keep.

Verified by running ci_parity end to end before this commit: 689 JavaScript
tests, 1132 Python tests, cross-implementation 41/41, fixtures clean, three
fuzz seeds with no divergences across 1050 compared cases each.

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

191 lines
7.2 KiB
YAML

name: TypeScript Parity
on:
push:
branches: ["**"]
paths:
- "js/**"
- "src/**"
- "scripts/generate_ts_port_fixtures.py"
- "scripts/cross_impl_check.py"
- "scripts/differential_fuzz.py"
- ".github/workflows/ts-parity.yml"
pull_request:
branches: ["**"]
paths:
- "js/**"
- "src/**"
- "scripts/generate_ts_port_fixtures.py"
- "scripts/cross_impl_check.py"
- "scripts/differential_fuzz.py"
- ".github/workflows/ts-parity.yml"
jobs:
parity:
runs-on: ubuntu-latest
defaults:
run:
working-directory: js
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Enable pnpm via corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm -r typecheck
- name: Parity tests (Node + jsdom)
run: pnpm -r test
- name: Build the distributable CLI
run: pnpm --filter @seedpass/cli build
- name: Smoke-test the bundle standalone
# js/scripts/smoke-cli.mjs, not an inline shell script. The version
# that used to live here used mktemp/export/`test -n`, so it could
# only run on Unix -- which meant the artifact users actually execute
# was never smoke-tested on Windows. Same file now runs everywhere,
# and checks determinism rather than just "some output appeared".
run: node scripts/smoke-cli.mjs
- name: Verify the packed npm tarball installs and runs
# `files`/`exports` mistakes only surface at install time: a package
# that omits its entry point still builds and tests fine here.
run: |
set -eu
pnpm --filter @seedpass/cli pack --pack-destination "$RUNNER_TEMP"
TARBALL="$(ls "$RUNNER_TEMP"/seedpass-cli-*.tgz)"
mkdir -p "$RUNNER_TEMP/packtest" && cd "$RUNNER_TEMP/packtest"
npm init -y > /dev/null
npm install "$TARBALL" > /dev/null
./node_modules/.bin/seedpass-js --version
- name: Upload CLI bundle
uses: actions/upload-artifact@v4
with:
name: seedpass-js-cli
path: |
js/packages/cli/dist/seedpass-js.mjs
js/packages/cli/dist/seedpass-js.mjs.sha256
javascript-supply-chain:
runs-on: ubuntu-latest
defaults:
run:
working-directory: js
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Enable pnpm via corepack
run: corepack enable
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit dependencies
# The Python side has had a dependency audit for a while; the
# JavaScript tree had none.
run: pnpm audit --audit-level moderate
# An SBOM step used to sit here and it never worked. It invoked
# `@cyclonedx/cyclonedx-npm@latest --package-lock-only`, which is the
# NPM tool and reads package-lock.json — a file this pnpm workspace does
# not have and never will. It then swallowed the failure with
# `|| echo "non-fatal"` and uploaded with `if-no-files-found: warn`, so
# two separate layers stopped it from ever going red. Its logs said
# "No files were found with the provided path" on every run.
#
# Removed rather than pinned: pinning the version of a tool that cannot
# read this repository's lockfile fixes the wrong problem. Generating a
# real SBOM needs a pnpm-native generator, which is tracked in TODO.md.
# `pnpm audit` above is the supply-chain check that actually runs and
# actually fails.
# Fixtures prove the two implementations compute the same values; this job
# proves they can read each other's on-disk profiles and backups.
cross-implementation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Enable pnpm via corepack
run: corepack enable
- name: Install JS dependencies
run: pnpm install --frozen-lockfile
working-directory: js
- name: Install Poetry
run: pipx install poetry
- name: Install Python dependencies
run: poetry install --no-interaction --no-ansi
- name: Cross-implementation compatibility
run: poetry run python scripts/cross_impl_check.py
# Differential fuzzing: random inputs to BOTH implementations, compared as
# canonical JSON. Fixtures prove agreement on inputs a human chose; this
# proves it on inputs nobody chose, which is where the bugs that survive to
# production live. It found a real silent-divergence defect in canonical
# number encoding the first time it ran at scale.
differential-fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Enable pnpm via corepack
run: corepack enable
- name: Install JS dependencies
run: pnpm install --frozen-lockfile
working-directory: js
- name: Install Poetry
run: pipx install poetry
- name: Install Python dependencies
run: poetry install --no-interaction --no-ansi
# Fixed seeds so a regression is deterministic and bisectable: the same
# commit always gets the same cases.
- name: Differential fuzz (fixed seeds)
run: |
for seed in 1 2 3; do
poetry run python scripts/differential_fuzz.py --cases 150 --seed "$seed"
done
# Plus one seed nobody has run before, so the explored input space grows
# over time instead of freezing at whatever was chosen today. The script
# prints its seed, so a failure here is still reproducible exactly.
- name: Differential fuzz (exploratory seed)
run: |
poetry run python scripts/differential_fuzz.py \
--cases 150 --seed "${{ github.run_id }}"
# The fixtures are committed, so the parity job never regenerates them.
# This job proves the committed fixtures actually come from the committed
# generator: it regenerates from Python and fails on any drift.
fixture-drift:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install Poetry
run: pipx install poetry
- name: Install dependencies
run: poetry install --no-interaction --no-ansi
- name: Regenerate fixtures
run: poetry run python scripts/generate_ts_port_fixtures.py
- name: Fail on drift
# manifest.json records the generating commit, which legitimately
# differs from the checked-out commit — everything else must match.
run: >
git diff --exit-code --
js/packages/test-vectors/fixtures
':(exclude)js/packages/test-vectors/fixtures/manifest.json'