From 4edf50e5dec71fba0d827604c6c9c096d882a6b2 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 20 Aug 2026 13:53:39 -0400 Subject: [PATCH] Attest the bundle on three platforms; give agents two gates with meanings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN --- .github/workflows/ts-parity.yml | 21 +-- .github/workflows/ts-platform.yml | 134 ++++++++++++++++++ AGENTS.md | 46 ++++++ .../test-vectors/fixtures/manifest.json | 2 +- 4 files changed, 187 insertions(+), 16 deletions(-) create mode 100644 .github/workflows/ts-platform.yml diff --git a/.github/workflows/ts-parity.yml b/.github/workflows/ts-parity.yml index f682d191..540e9c7f 100644 --- a/.github/workflows/ts-parity.yml +++ b/.github/workflows/ts-parity.yml @@ -42,21 +42,12 @@ jobs: - name: Build the distributable CLI run: pnpm --filter @seedpass/cli build - name: Smoke-test the bundle standalone - # Run from a directory with no node_modules to prove the bundle - # carries everything it needs. - run: | - set -eu - BIN="$PWD/packages/cli/dist/seedpass-js.mjs" - APP_DIR="$(mktemp -d)" - cd "$(mktemp -d)" - node "$BIN" --version - node "$BIN" capabilities > /dev/null - export SEEDPASS_APP_DIR="$APP_DIR" - export SEEDPASS_MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - export SEEDPASS_PASSWORD="ci-smoke-password" - node "$BIN" fingerprint add --name ci > /dev/null - node "$BIN" entry add password ci-site --length 16 > /dev/null - test -n "$(node "$BIN" entry reveal ci-site)" + # 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. diff --git a/.github/workflows/ts-platform.yml b/.github/workflows/ts-platform.yml new file mode 100644 index 00000000..1a7f189b --- /dev/null +++ b/.github/workflows/ts-platform.yml @@ -0,0 +1,134 @@ +name: TypeScript Platform + +# Three operating systems, running the checks that can only be answered by an +# actual machine of that kind. +# +# The division of labour with ts-parity.yml is deliberate. Everything that is +# a property of the CODE -- cross-implementation agreement, fixture drift, +# differential fuzzing -- is asked once, on Linux, because asking it three +# times measures nothing extra. What goes here is the set of questions where +# the platform IS the variable: does the bundle build, run and install on a +# machine that is not the one we develop on. +# +# That distinction is not theoretical. On 2026-08-20 every CI failure that +# survived local checking was a platform difference -- MSYS2 shadowing +# `python` on PATH, pwsh discarding a non-final exit code, portalocker +# needing pywin32, and shlex eating the backslashes out of Windows paths in +# the TUI. None of those are visible from Linux, and all of them reached +# users' machines rather than ours. + +on: + push: + branches: ["**"] + paths: + - "js/**" + - ".github/workflows/ts-platform.yml" + pull_request: + branches: ["**"] + paths: + - "js/**" + - ".github/workflows/ts-platform.yml" + +permissions: + contents: read + +jobs: + platform: + strategy: + # Not fail-fast: when something breaks on one platform the useful + # question is immediately "and the others?", which a cancelled sibling + # cannot answer. + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: js + # bash everywhere, including Windows. Under pwsh only the LAST command + # in a multi-line block decides whether a step failed, which is how + # the Tests workflow reported success while its own log read + # "2 failed, 1114 passed". + shell: bash + 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: Lint + run: pnpm lint + - name: Tests + run: pnpm -r test + - name: Build the distributable CLI + run: pnpm build + - name: Smoke-test the bundle standalone + # The same Node script that runs locally and in ts-parity. It executes + # the bundle from a directory with no node_modules, so a dependency + # that failed to bundle surfaces here and not in a user's install, + # and it reveals the same entry twice to check the secret is + # deterministic -- which for this product is the whole point. + run: pnpm smoke + - name: Verify the packed npm tarball installs and runs + run: | + 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: Record the bundle hash + run: | + shasum -a 256 packages/cli/dist/seedpass-js.mjs | awk '{print $1}' \ + > "$RUNNER_TEMP/bundle.sha256" + echo "${{ matrix.os }}: $(cat "$RUNNER_TEMP/bundle.sha256")" + - uses: actions/upload-artifact@v4 + with: + name: bundle-sha-${{ matrix.os }} + path: ${{ runner.temp }}/bundle.sha256 + if-no-files-found: error + + # EXPERIMENT, not yet a requirement. + # + # build.mjs already emits a SHA-256 over exactly the file users execute, and + # two builds on one machine are byte-identical -- so same-machine + # determinism holds and the cross-platform question is a real one rather + # than a fantasy. If esbuild produces identical bytes on all three, that is + # a reproducibility invariant worth promoting to required. If it does not, + # the interesting part is WHY: path separators baked into a banner, line + # endings, a platform-specific esbuild binary. Either answer is worth + # having, and neither is worth blocking a merge over until we know which. + reproducible: + needs: platform + if: always() + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/download-artifact@v4 + with: + pattern: bundle-sha-* + path: hashes + - name: Compare the three bundle hashes + run: | + echo "Bundle SHA-256 per platform:" + found=0 + for f in hashes/bundle-sha-*/bundle.sha256; do + [ -f "$f" ] || continue + found=$((found + 1)) + printf ' %-28s %s\n' "$(basename "$(dirname "$f")")" "$(cat "$f")" + done + if [ "$found" -lt 2 ]; then + echo "::warning::fewer than two platforms produced a bundle hash" + exit 0 + fi + if [ "$(cat hashes/bundle-sha-*/bundle.sha256 | sort -u | wc -l)" -eq 1 ]; then + echo "::notice::the bundle is byte-identical across platforms" + else + echo "::warning::the bundle differs by platform — worth understanding before this becomes a required check" + fi diff --git a/AGENTS.md b/AGENTS.md index b961a4ff..7f6bb2b9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -182,6 +182,52 @@ SeedPass maintains a comprehensive test infrastructure. Agents should use the ce bash scripts/run_ci_tests.sh ``` +### Local CI: the two gates that matter + +TypeScript is the implementation going forward; Python is the reference it is +validated against. Both are covered by two gates, and they have meanings, not +just names: + +```bash +./scripts/ci_prepush.sh # ~45s. Run before every push. +./scripts/ci_parity.sh # minutes. Run before calling a branch ready. +``` + +**An agent may not report a task complete or push until `ci_prepush.sh` +passes, and may not call a branch merge-ready until `ci_parity.sh` passes.** + +Both begin with `scripts/ci_preflight.py`, which verifies this machine agrees +with the lockfiles and refuses to continue when it does not. That is not +ceremony. On 2026-08-20 the local environment held black 26.1.0 while +`poetry.lock` pinned 24.10.0, so `poetry run black --check .` passed locally +and failed on GitHub — and an earlier commit titled "satisfy the black gate" +had already reformatted five files with the wrong black, making CI worse +while looking like a fix. A local gate that disagrees with CI is worse than +no gate, because people stop reading the real result. + +If preflight fails, repair with: + +```bash +./scripts/dev_sync.sh # the only script that mutates your environment +``` + +Two rules follow from this and apply to everything you write here: + +- **Invoke tools through the repository, never off PATH.** `poetry run black`, + `pnpm -C js lint` — never a bare `black` or `oxlint`. A bare name is exactly + how a local run and a CI run end up disagreeing about the same source. +- **Never add a check that cannot fail.** Two shipped examples were found and + removed on 2026-08-20: a test step whose failures pwsh discarded on Windows, + and an SBOM step that swallowed its own error and uploaded nothing. A green + check nobody can turn red is worse than an absent one, because it is + trusted. + +Enable the pre-push hook once per clone: + +```bash +git config core.hooksPath .githooks +``` + Currently the test folder is located in `src/tests/`. New tests should be placed there so `pytest` can discover them automatically. ## Style Guidelines diff --git a/js/packages/test-vectors/fixtures/manifest.json b/js/packages/test-vectors/fixtures/manifest.json index 4ec7a46d..ce53fcb4 100644 --- a/js/packages/test-vectors/fixtures/manifest.json +++ b/js/packages/test-vectors/fixtures/manifest.json @@ -32,6 +32,6 @@ ], "fixture_version": 1, "generator": "scripts/generate_ts_port_fixtures.py", - "python_commit": "64b27d7bf932120392e2b4ff0f46f32de59f26c9", + "python_commit": "72b3e52f500114c57ac360dcb82952a49631a8f0", "redaction_policy": "All fixtures derive from public BIP-39 test mnemonics (abandon.../legal.../zoo...). Secret-shaped outputs are generated test material, never from a live profile." }