Skip to content

ci: feed merged unit+integration coverage into the fallow structural quality gate - #909

Merged
chrisdburr merged 2 commits into
stagingfrom
ci/fallow-integration-coverage
Aug 25, 2026
Merged

chrisdburr merged 2 commits into
stagingfrom
ci/fallow-integration-coverage

Conversation

@chrisdburr

Copy link
Copy Markdown
Collaborator

What

Root-cause fix for the Structural Quality gate's coverage blind spot. The gate previously audited with unit-test coverage only, so service/route functions covered exclusively by the integration suite (the repo's testing-trophy convention) scored 0% and failed on inflated CRAP scores — most recently five false positives on PR #908.

Now: the existing integration-test run in build.yaml also records coverage (~0-2.6% duration overhead), unit + integration coverage are merged (fail-closed if either input is missing), and the fallow audit runs on the merged file in a structural-quality job that keeps the exact required-check name "Structural Quality (blocking)". The standalone fallow.yml workflow is retired to a no-op stub (deletion is hook-blocked; its workflow_dispatch trigger exists only because a trigger-less workflow is schema-invalid, and its job name cannot satisfy the required check).

Verification highlight: with real merged coverage, the PR #908 false positives disappear while a genuinely-uncovered function in the same file correctly stays flagged — the gate discriminates real gaps from estimation artefacts.

Notes

  • Negative coverage counts: vitest's v8 provider under the multi-fork pool can emit impossible negative hit counts, which fallow's parser hard-errors on. A sanitize step clamps them to 0 before merging (logged as a warning annotation). Clamping can only lower measured coverage, i.e. make CRAP scores more conservative — it cannot hide risk.
  • Accepted risk — run cancellation: if a whole workflow run is cancelled before the structural-quality job starts, GitHub force-skips it and a skipped required check passes branch protection. This is a documented Actions limitation with no in-workflow fix; the compensating control is process — re-run a cancelled run to completion before merging. (The old standalone job reported "cancelled" and blocked; this is a known, accepted trade-off of moving the audit behind the test job, recorded in the workflow header.)
  • .fallowrc ignore patterns for the new coverage directories are a defensive addition: neither implementer nor QA could reproduce fallow's discovery actually scanning generated report assets in the current version.
  • Follow-up required before trusting the new gate on source PRs: the identity-mode health baseline must be regenerated with the merged-coverage recipe (documented in the build.yaml header) — landing as its own commit on staging straight after this merges. A baseline saved under the old unit-only recipe will not pair with merged-coverage scores (audit --gate new-only misattributes unchanged high-CRAP functions as introduced when --coverage is supplied fallow-rs/fallow#2347 class).

Review chain

  • QA replay in Node-20/22 containers matching CI: full coverage flow end to end, zero negative counts in the merged file post-sanitize, both negative controls held (missing input refuses loudly; corrupted file fails distinguishably from findings), sanitize scope confined, docs-only PR path verified green-without-audit.
  • Code review: approve — audit invocation semantics byte-identical to the retired workflow bar the coverage input; artifact flow run-scoped and fail-loud; fix round was comment-only (machine-verified).
  • actionlint clean on both workflows at the final commit.

Unblocks #908.

…quality gate

The fallow audit previously ran in its own workflow (fallow.yml) against a
unit-only coverage pass it generated itself, so functions covered only by
the integration suite (the testing-trophy convention for services/routes)
scored 0% and produced false CRAP-inflation findings (PR #908: 5 findings
on functions with 11 dedicated integration tests).

The integration test step in build.yaml's `test` job now records its own
coverage (instrumenting the existing run, not a second one) into
coverage-integration/, separate from the unit ratchet's coverage/. Both are
merged (nyc merge) after sanitizing negative hit counts that vitest's
v8-coverage provider can emit under the integration project's multi-fork
pool — an existing artefact, discovered while testing this change, that
otherwise hard-errors fallow's coverage parser. The merged file is uploaded
as an artifact and consumed by a new `structural-quality` job (required
check name "Structural Quality (blocking)", preserved) that runs the same
fallow invocation fallow.yml used to, verbatim in semantics, including the
PR-comment posting and fail-closed exit-code handling.

fallow.yml is retired (stripped to a no-op with no trigger, kept for the
retirement note in its history — .github/ deletions are blocked here).
.fallowrc.json now ignores the new coverage-* directories, which fallow's
whole-tree discovery was otherwise scanning as source files.

The structural-quality job branches explicitly on needs.test.result rather
than relying on either skip-propagation reading or an implicit success()
check (see its header comment for the both-readings analysis), so a failed
or cancelled test job fails the gate instead of silently reporting green
via skip, while a docs-only PR (test genuinely skipped) reports a clean
success with nothing to audit.

Baseline refresh recipe for .fallow-baseline.health.json under the new
merged-coverage input is documented in build.yaml's header comment;
Chris/cid runs and commits it separately.
…nt-only)

Nanaki PASS-WITH-NOTES / vincent APPROVE-WITH-NOTES on 0acb949. Four
must-fixes, all comment-tier:

1. The structural-quality header comment claimed cancellation was handled
   fail-closed. It isn't: GitHub force-skips a not-yet-started job on
   whole-run cancellation before its `if:` is evaluated, so `!cancelled()`
   never runs and a skipped required check passes branch protection —
   a regression from the old standalone fallow.yml job, which reported its
   own 'cancelled' conclusion and blocked merge. Rewrote both the
   structural-quality comment and the fallow.yml retirement note to state
   this accurately instead of claiming fail-closed.

2. Added an explicit accepted-risk line next to the corrected comment:
   documented GitHub Actions limitation, no in-workflow fix; compensating
   control is process (a cancelled run must be re-run to completion before
   merge); a stronger technical fix is out of scope, separate issue if it
   bites in practice.

3. fallow.yml's `on: workflow_dispatch: {}` contradicted its own "no
   trigger, never runs" claim. Verified with actionlint that both an empty
   `on: {}` and a missing `on:` key are hard errors ("on section should not
   be empty" / "on section is missing") — a genuinely trigger-less workflow
   isn't valid, so workflow_dispatch stays. Corrected the comment to say it
   exists only to keep the file schema-valid, and that a manual run is
   harmless (different job name, doesn't satisfy or collide with the
   required check).

4. Could not reproduce the .fallowrc.json causal claim under nanaki's
   method either (774 total_files with or without the three new
   ignorePatterns entries, tested directly against this worktree's own
   coverage-integration/ HTML report tree). The earlier "discovered 1130
   files" observation was from a stray duplicate scratch directory, not
   from the ignorePatterns state. JSON has no comment syntax so there is no
   inline claim to soften in .fallowrc.json itself; the correction belongs
   in the PR description (commit 0acb949's message stands per Chris/cid's
   call) — restating the patterns as a defensive addition, not an
   observed-problem fix.

No step logic, `if:` conditions, flags, or job structure changed — diffed
non-comment/non-blank lines against 0acb949 to confirm.
@github-actions

Copy link
Copy Markdown

Fallow audit report

No GitHub PR/MR findings.

Generated by fallow.

@chrisdburr
chrisdburr merged commit ada8457 into staging Aug 25, 2026
6 checks passed
@chrisdburr
chrisdburr deleted the ci/fallow-integration-coverage branch August 25, 2026 09:04
chrisdburr added a commit that referenced this pull request Aug 25, 2026
…verage

First baseline saved under the merged-coverage recipe that PR #909's gate
now audits with (identity mode, real unit+integration coverage, negative
counts sanitized per the build.yaml merge step). The previous baseline was
saved with unit-only coverage, which inflated CRAP scores on integration-
tested functions; under the merged recipe those findings cease to exist,
hence the large shrink. A baseline saved under the old recipe would not
pair with merged-coverage head scores (fallow-rs/fallow#2347 class).
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.

1 participant