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 # 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)" - 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 - name: Generate a CycloneDX SBOM run: | npx --yes @cyclonedx/cyclonedx-npm@latest \ --output-format JSON \ --output-file "$RUNNER_TEMP/seedpass-js-sbom.json" \ --package-lock-only || echo "SBOM generation failed (non-fatal)" - name: Upload SBOM uses: actions/upload-artifact@v4 if: always() with: name: seedpass-js-sbom path: ${{ runner.temp }}/seedpass-js-sbom.json if-no-files-found: warn # 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'