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." }