From e9d49d10bc801e2390208ac8a9cc0ca7521bb718 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN Date: Thu, 20 Aug 2026 13:26:50 -0400 Subject: [PATCH] Stop 13 jobs firing on a README edit, and delete an SBOM check that never ran MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two GitHub-side changes, both about not wasting runs. PATH FILTERS. tests.yml (nine jobs across three OSes and three Pythons) and python-ci.yml (four) both triggered on every push and every pull request with no path restriction, so editing a markdown file spent thirteen jobs. They now trigger on the Python implementation and the files that pin its environment. ts-parity keeps triggering on src/** as well as js/**, deliberately: Python is the reference implementation the port is validated against, so a change there can break parity even when no TypeScript moved. THE SBOM STEP IS GONE. 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 could not have worked on any run. It then swallowed the failure with `|| echo "non-fatal"` and uploaded with `if-no-files-found: warn`, two independent layers stopping it from going red, and its log said "No files were found with the provided path" every time. Removed rather than pinned. Pinning the version of a tool that cannot read this repository's lockfile fixes the wrong problem, and a security check that cannot fail is the same anti-pattern as the pwsh exit-code bug fixed earlier today — worse than absent, because it is trusted. `pnpm audit --audit-level moderate` stays and does fail on findings. A real bill of materials wants a pnpm-native generator, tracked in TODO.md. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN --- .github/workflows/python-ci.yml | 24 ++++++++++++ .github/workflows/tests.yml | 24 ++++++++++++ .github/workflows/ts-parity.yml | 26 ++++++------- TODO.md | 16 ++++++++ js/pnpm-workspace.yaml | 1 + scripts/ci_preflight.py | 69 +++++++++++++++++++++++---------- 6 files changed, 127 insertions(+), 33 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index e88ba6a3..9e5bb149 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -3,8 +3,32 @@ name: CI on: push: branches: [ "**" ] + paths: + # The Python implementation and everything that pins its environment. + # It is the reference implementation the TypeScript port is validated + # against, so it still has to be tested — but a docs edit or a change + # under js/ has no business firing 4 jobs. + - "src/**" + - "scripts/**" + - "pyproject.toml" + - "poetry.lock" + - "requirements.lock" + - "conftest.py" + - ".github/workflows/python-ci.yml" pull_request: branches: [ "**" ] + paths: + # The Python implementation and everything that pins its environment. + # It is the reference implementation the TypeScript port is validated + # against, so it still has to be tested — but a docs edit or a change + # under js/ has no business firing 4 jobs. + - "src/**" + - "scripts/**" + - "pyproject.toml" + - "poetry.lock" + - "requirements.lock" + - "conftest.py" + - ".github/workflows/python-ci.yml" schedule: - cron: '0 3 * * *' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index d7c8470e..b6172ed4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,8 +3,32 @@ name: Tests on: push: branches: ["**"] + paths: + # The Python implementation and everything that pins its environment. + # It is the reference implementation the TypeScript port is validated + # against, so it still has to be tested — but a docs edit or a change + # under js/ has no business firing 9 jobs. + - "src/**" + - "scripts/**" + - "pyproject.toml" + - "poetry.lock" + - "requirements.lock" + - "conftest.py" + - ".github/workflows/tests.yml" pull_request: branches: ["**"] + paths: + # The Python implementation and everything that pins its environment. + # It is the reference implementation the TypeScript port is validated + # against, so it still has to be tested — but a docs edit or a change + # under js/ has no business firing 9 jobs. + - "src/**" + - "scripts/**" + - "pyproject.toml" + - "poetry.lock" + - "requirements.lock" + - "conftest.py" + - ".github/workflows/tests.yml" jobs: test: diff --git a/.github/workflows/ts-parity.yml b/.github/workflows/ts-parity.yml index 1436affd..f682d191 100644 --- a/.github/workflows/ts-parity.yml +++ b/.github/workflows/ts-parity.yml @@ -94,19 +94,19 @@ jobs: # 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 + # 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. diff --git a/TODO.md b/TODO.md index a349282a..4fb1db5f 100644 --- a/TODO.md +++ b/TODO.md @@ -342,6 +342,22 @@ everything still open, in the order it should be tackled. smoke step assert something that can fail. Do not leave it as a dependency nobody declared and tests nobody runs. (Found 2026-08-20 while fixing the first CI run of this branch.) + - [ ] **The JavaScript SBOM needs a pnpm-native generator.** The old + step invoked `@cyclonedx/cyclonedx-npm --package-lock-only`, which + is the npm tool and reads `package-lock.json` — a file this pnpm + workspace does not have. It then swallowed the failure twice over + (`|| echo "non-fatal"` and `if-no-files-found: warn`), so it + reported success on every run while its own log said "No files + were found with the provided path". Removed 2026-08-20 rather than + pinned, because pinning a tool that cannot read our lockfile fixes + the wrong problem. + + `pnpm audit --audit-level moderate` remains and does fail on + findings, so supply-chain checking is not absent — only the bill + of materials is. The shipped bundle has six production + dependencies, so a generator over `pnpm list --json` is likely + simpler and more auditable than another third-party tool, and + fits the zero-dependency posture of the bundle itself. - [ ] **A nested export path answers 500 instead of a refusal.** `POST /api/v1/entry/:id/document/export` with `{"path": "exports/nested/secret.txt"}` fails with an opaque diff --git a/js/pnpm-workspace.yaml b/js/pnpm-workspace.yaml index 4dd398c1..d0927c4c 100644 --- a/js/pnpm-workspace.yaml +++ b/js/pnpm-workspace.yaml @@ -3,3 +3,4 @@ packages: - apps/* allowBuilds: esbuild: true + libxmljs2: set this to true or false diff --git a/scripts/ci_preflight.py b/scripts/ci_preflight.py index a42bba86..641beec8 100755 --- a/scripts/ci_preflight.py +++ b/scripts/ci_preflight.py @@ -55,7 +55,9 @@ failures: list[str] = [] def ok(label: str, detail: str = "") -> None: - print(f" {GREEN}ok{RESET} {label}" + (f" {DIM}{detail}{RESET}" if detail else "")) + print( + f" {GREEN}ok{RESET} {label}" + (f" {DIM}{detail}{RESET}" if detail else "") + ) def bad(label: str, detail: str, fix: str) -> None: @@ -101,14 +103,22 @@ def check_python_env() -> None: locked = parse_lock_versions(REPO / "poetry.lock") code, out = run( - ["poetry", "run", "python", "-c", - "import json;from importlib.metadata import distributions;" - "print(json.dumps({d.metadata['Name'].lower(): d.version " - "for d in distributions() if d.metadata['Name']}))"] + [ + "poetry", + "run", + "python", + "-c", + "import json;from importlib.metadata import distributions;" + "print(json.dumps({d.metadata['Name'].lower(): d.version " + "for d in distributions() if d.metadata['Name']}))", + ] ) if code != 0: - bad("cannot inspect the poetry environment", out.splitlines()[-1] if out else "", - "./scripts/dev_sync.sh") + bad( + "cannot inspect the poetry environment", + out.splitlines()[-1] if out else "", + "./scripts/dev_sync.sh", + ) return installed = json.loads(out.splitlines()[-1]) @@ -122,11 +132,16 @@ def check_python_env() -> None: elif have != want: drift.append(f"{tool} {have} != locked {want}") if drift: - bad("installed tools disagree with poetry.lock", "; ".join(drift), - "./scripts/dev_sync.sh") + bad( + "installed tools disagree with poetry.lock", + "; ".join(drift), + "./scripts/dev_sync.sh", + ) else: - ok("black, pytest and coverage match poetry.lock", - " ".join(f"{t}={locked.get(t,'?')}" for t in VERDICT_TOOLS)) + ok( + "black, pytest and coverage match poetry.lock", + " ".join(f"{t}={locked.get(t,'?')}" for t in VERDICT_TOOLS), + ) def check_js_env() -> None: @@ -136,19 +151,28 @@ def check_js_env() -> None: want_node = str(pkg.get("engines", {}).get("node", "")).lstrip("^>=~ ") have_node = sys.version and run(["node", "--version"])[1].lstrip("v") if not want_node: - bad("js/package.json declares no engines.node", "nothing to check against", - 'add "engines": {"node": ">=22"} to js/package.json') + bad( + "js/package.json declares no engines.node", + "nothing to check against", + 'add "engines": {"node": ">=22"} to js/package.json', + ) elif have_node.split(".")[0] != want_node.split(".")[0]: - bad("node major differs from engines.node", f"have {have_node}, want {want_node}", - f"install Node {want_node.split('.')[0]}") + bad( + "node major differs from engines.node", + f"have {have_node}, want {want_node}", + f"install Node {want_node.split('.')[0]}", + ) else: ok("node matches engines.node", f"v{have_node}") want_pnpm = str(pkg.get("packageManager", "")).removeprefix("pnpm@") have_pnpm = run(["pnpm", "--version"])[1] if want_pnpm and have_pnpm != want_pnpm: - bad("pnpm differs from packageManager", f"have {have_pnpm}, want {want_pnpm}", - "corepack enable && corepack prepare --activate") + bad( + "pnpm differs from packageManager", + f"have {have_pnpm}, want {want_pnpm}", + "corepack enable && corepack prepare --activate", + ) else: ok("pnpm matches packageManager", have_pnpm) @@ -157,8 +181,11 @@ def check_js_env() -> None: if code == 0: ok("node_modules satisfies pnpm-lock.yaml") else: - bad("pnpm-lock.yaml not satisfied", out.splitlines()[-1] if out else "", - "./scripts/dev_sync.sh") + bad( + "pnpm-lock.yaml not satisfied", + out.splitlines()[-1] if out else "", + "./scripts/dev_sync.sh", + ) def main() -> int: @@ -168,7 +195,9 @@ def main() -> int: print() if failures: - print(f"{RED}preflight failed{RESET} — a local gate run now could disagree with CI.\n") + print( + f"{RED}preflight failed{RESET} — a local gate run now could disagree with CI.\n" + ) for f in failures: print(f" - {f}") print("\nRepair with: ./scripts/dev_sync.sh")