Skip to content

ci: close three blind spots found investigating why CI missed sable-l10k - #221

Merged
Rome-1 merged 1 commit into
mainfrom
fix/sable-bm5k-ci-blind-spots
Sep 1, 2026
Merged

ci: close three blind spots found investigating why CI missed sable-l10k#221
Rome-1 merged 1 commit into
mainfrom
fix/sable-bm5k-ci-blind-spots

Conversation

@Rome-1

@Rome-1 Rome-1 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Closes sable-bm5k. The assignment was to explain why 8 of 17 checks skipped on PR #220. The skip pattern is real, but it is not why the bug reached a customer — and testing that rather than assuming it is what turned up the rest.

The sharp question: would any existing check have caught it, if it had run?

No. I checked out main at 0996492 — the buggy tree — and ran the entire suite against it:

Tests  7 failed | 2065 passed | 9 skipped (2081)

All seven failures are in three files (cross-runtime-parity, skill-review-deep, skill-scanner) that fail here for environmental reasons and have nothing to do with polling. tests/scan-remote.test.ts and test_scan_remote.py both cover the poll loop — with the HTTP layer mocked, and neither ever injected a non-2xx mid-poll. They pass against the bug.

And nothing executed github-action/action.yml at all, which is the file the customer's error string came from:

  • test-action.yml drives the root action.yml — a different action that scans locally. Its four jobs never touch the composite action.
  • In test-github-action.yml, test-threshold-evaluation and test-pr-comment-tip run hand-copied reimplementations of the action's bash (both scripts' own headers admit the duplication is manual), and test-action-yml-defaults greps the YAML as text.

So the workflow that fires on github-action/** changes did run, and still executed none of the code. The gap was coverage, and #220 closed it. The skip pattern would not have mattered.

What does matter — three blind spots found on the way

1. publish-python had no needs:. publish-node has needed the test jobs since it was written; the Python half published to PyPI in parallel with the tests, ungated. A red suite blocked the npm release and shipped the PyPI one anyway. In a dual-implementation product where CI enforces that the two versions match, that diverges them at the registry — the one place users can't see it. Now gated on [test-node, test-package].

2. backend-api rendered identically whether it tested the backend or nothing. Its only real step is gated on RAFTER_API_KEY, which has never been set on this repo (gh secret list: CLAWHUB_TOKEN, NPM_TOKEN, PYPI_TOKEN). So backend-api ✓ has always meant "checked out and built". It now says so in the log and the step summary. Compounding it, tests/backend-api.test.ts self-skips on the same condition, so even when the step runs without a key it asserts nothing.

3. test-node and test-python were skipped on internal PRs into main. On #220 — which changed both clients — neither ran. They now run on every PR.

The premise behind the original gate ("our own work is reviewed and tested locally before it is pushed") is weaker than it looks: this repo has test files that fail locally for environmental reasons, so "green on my machine" isn't a signal anyone can act on. I only got a trustworthy green on #220 by dispatching the workflow manually.

Cost is ~4 minutes of wall clocktest-node 234s and test-python 100s, in parallel. The expensive part, the 6-way cross-platform grid with 3 macOS runners, stays gated. This reverses part of #219 narrowly and deliberately; if you'd rather keep the old behavior, revert the run_core split and leave the other two fixes.

Established, not changed here

main has no branch protection at all. The only ruleset (Prod Protection) targets refs/heads/prod and contains no required-status-checks rule. So no check is required anywhere, and a red PR can merge into main. That also makes the "does a skipping job satisfy a required check" question moot today — nothing is required. Worth knowing before adding required checks: a job skipped by an if: gate reports as satisfied, so requiring these checks would not have closed this hole either.

That's a policy call rather than a workflow fix, so it's flagged, not touched.

Verification

This PR is itself the test for change 3: it is an internal PR into main, so under the old gate test-node and test-python would both skip. They should now run, while cross-platform stays skipped.

The assignment was to explain why 8 of 17 checks skipped on PR #220. The skip
pattern is real, but it is not why the bug reached a customer. Testing that
claim rather than assuming it is what turned up the rest.

WOULD ANY EXISTING CHECK HAVE CAUGHT IT, IF IT HAD RUN? No. Checked out main
at 0996492 (the buggy tree) and ran the entire suite against it: 2065 passed,
and the only failures were three files that fail for environmental reasons
here and are unrelated to polling. tests/scan-remote.test.ts and
test_scan_remote.py both cover the poll loop — with the HTTP layer mocked, and
neither ever injected a non-2xx mid-poll. They pass against the bug.

And nothing executed github-action/action.yml at all, which is the file the
customer's error came from. test-action.yml drives the ROOT action.yml, a
different action that scans locally. Of the jobs in test-github-action.yml,
two run hand-copied reimplementations of the action's bash (their own headers
admit the duplication) and one greps the YAML as text. So the workflow that
fires on github-action/** changes ran, and still executed none of the code.

The gap was COVERAGE, and #220 closed it. The skip pattern would not have
mattered. Three things found on the way there do:

1. publish-python had no `needs:`. publish-node has needed the test jobs since
   it was written; the Python half published to PyPI in parallel with the
   tests, ungated. A red suite blocked the npm release and shipped the PyPI
   one anyway — in a dual-implementation product where the two versions must
   match, that diverges them at the registry, the one place users cannot see
   it. Now gated. (publish.yaml also runs no pytest anywhere; filed separately.)

2. backend-api rendered identically whether it tested the backend or nothing.
   Its only real step is gated on RAFTER_API_KEY, which has never been set on
   this repo, so "backend-api ✓" has always meant "checked out and built".
   It now says so, loudly, in the log and the step summary.

3. test-node and test-python were skipped on internal PRs into main. On #220 —
   which changed both clients — neither ran. They now run on every PR. The
   premise that our own work is tested locally first is also weaker than it
   looks: this repo has test files that fail locally for environmental
   reasons, so "green on my machine" is not a signal anyone can act on. Cost
   is ~4 minutes of wall clock (234s and 100s, in parallel). The expensive
   part, the 6-way cross-platform grid with 3 macOS runners, stays gated —
   this reverses part of #219 narrowly and deliberately, not wholesale.

Also established, not changed here: main has no branch protection at all. The
only ruleset targets refs/heads/prod and contains no required-status-checks
rule, so no check is required anywhere and a red PR can merge into main. That
is a policy call, not a workflow fix.
@Rome-1

Rome-1 commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

Flagging the one part of this that reverses a deliberate decision, so it gets argued rather than slipped through.

#219 gated the suite off for internal PRs into main, on the stated premise that "our own work (Rome-1's PRs, or any branch living in the Raftersecurity repo) is reviewed and tested locally before it is pushed." Change 3 here partially reverses that. The counter-evidence:

The premise cannot hold in this repo, because a local run is not a trustworthy signal. Three test files — cross-runtime-parity, skill-review-deep, skill-scanner — fail locally for environmental reasons (a stale rafter_cli in site-packages, so the version-parity check compares 0.9.0 against the tree's 0.10.0). Anyone testing locally sees red and has to decide which red to ignore. That is not a gate, it is a judgment call made under time pressure by the person least able to be objective about it.

Concretely: on #220 I ran everything locally and could not tell from that run whether I had broken anything. I only got a green I trusted by dispatching test-comprehensive.yml by hand against the branch — which is the thing the gate exists to avoid doing.

And on #220 specifically the gate meant that on a PR changing both the Node and the Python client, neither test-node nor test-python ran. That PR was a P0 customer fix touching github-action/action.yml, node/src/** and python/rafter_cli/**.

What I have not done is un-skip wholesale. #219's cost argument is sound and I took it seriously: the expensive part is the cross-platform grid — 6 jobs, 3 of them macOS at 10× minutes — and it stays gated exactly as #219 left it. The two unit-test jobs are 234s and 100s, running in parallel, so this costs about four minutes of wall clock on an internal PR.

If the cost is still judged too high, reverting is a small, clean change: drop the run_core output from the gate job and put test-node and test-python back on needs.gate.outputs.run. The other two fixes in this PR — the publish-python gating and the backend-api loud-skip — are independent and stand on their own.

@Rome-1
Rome-1 merged commit dbb4f60 into main Sep 1, 2026
9 checks passed
@Rome-1
Rome-1 deleted the fix/sable-bm5k-ci-blind-spots branch September 1, 2026 02:50
Rome-1 pushed a commit that referenced this pull request Sep 1, 2026
#221 added [test-node, test-package] to stop a red Node suite shipping the
PyPI package. #222 adds test-python so Python tests run on the release path
at all. Both are wanted; the union is [test-node, test-python, test-package].
Rome-1 added a commit that referenced this pull request Sep 1, 2026
python/tests/ executed in exactly one workflow in this repo —
test-comprehensive.yml — and that workflow is not on the release path.
publish.yaml had no Python test job at all, and validate-release.yml's
test-build ran `python -m build` without pytest, so the pre-release gate
asserted the Python package compiles and never that it works. A
Python-only regression reached PyPI green, and PyPI is not somewhere you
can quietly unship from.

- publish.yaml: add test-python, mirroring test-comprehensive.yml's job.
  It runs in parallel with test-node, so it costs no wall clock the
  release was not already spending.
- publish.yaml: gate BOTH publish jobs on it. The two registries publish
  from one push and version parity is enforced elsewhere, so a suite that
  goes red after npm has already published leaves the two runtimes at
  different versions on the two indexes. The divergence is the failure
  mode whichever half breaks.
- publish.yaml: give test-node a Python toolchain. The node release path
  looked safe because publish-node has a `needs:` clause, but the job it
  needed was quietly weaker than the same job in test-comprehensive.yml:
  tests/cross-runtime-parity.test.ts gates its whole describe block on
  `python3 -c "import typer"` and describe.skip is silent, so all 40
  parity assertions were skipped and the suite reported green. Verified
  by stubbing python3: "40 skipped", exit 0. Those tests are what enforce
  the dual-implementation contract — the ones a release least wants to
  skip.
- validate-release.yml: run pytest in test-build. ~80s.

Note for PR #221, which touches the same job: it adds
`needs: [test-node, test-package]` to publish-python to stop a red NODE
suite shipping the PyPI package. That is a different hole. Keep both
sets of needs if the two land together.

Co-authored-by: mayor <hello@rafter.so>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants