Skip to content

Commit 16b3d44

Browse files
committed
ci: run every local-gate drift check in CI, and guard the two lists against drifting again
`npm run test:ci` (the documented local gate) and ci.yml (what actually blocks a merge) are two independently hand-maintained lists, and they had drifted: 22 of the 42 drift checks reachable from test:ci ran in no workflow at all. On a repo whose gate auto-merges on green CI, such a check is a convention rather than a gate, and worse, its presence reads as coverage. Three of the missing ones make that concrete: checkers-wired:check is the #9860 meta-guard that detects unwired checkers and was itself unwired; db:migrations:immutable:check guards the invariant whose violation breaks boot for every upgraded Orb; test-wiring and typecheck-coverage exist to stop coverage silently narrowing. Wired as ONE ungated block rather than 19 individually gated steps. Per-check path gating is where the fidelity bugs actually live -- the sibling PR had to add an entire `contract` filter because nothing here watched packages/loopover-contract/** -- and 19 hand-chosen gates would be exactly the hand-maintained list #9860 rejects. These are sub-second file-reading scripts, so running them unconditionally costs less than choosing 19 gates correctly. Three checks are deliberately NOT in that block. They need git history that validate-code's checkout does not have, and measuring them there showed they degrade in two different and equally bad ways: db:migrations:immutable:check exits 1 with "no orb-v* tags visible", so it would fail every PR, while release-commit-parsing:check returns [] and passes, and releasable-commit-types:check prints "cannot resolve; skipping" and exits 0. The last two would have been permanently green while verifying nothing -- the very failure this change exists to remove, reproduced at the CI layer and then read as coverage. They run in a new drift-checks-history job with fetch-depth: 0 and tags, which asserts up front that the history is actually present rather than trusting it. The aggregator gains a dependency on that job, since a job nothing depends on cannot fail a PR. check-ci-drift-checks-wired.ts is the guard that stops this reopening. It is the sibling of check-checkers-wired.ts on the other axis: that one asks whether a checker runs anywhere (test:ci counts), this one asks whether CI runs it, which is the question that decides whether a merge is gated. Both sides are computed, never listed, so the roster cannot drift the way ci.yml drifted from test:ci in the first place. Includes the contract api-schemas wiring from the sibling branch, because this guard cannot pass without it. Closes #10269
1 parent 7b6e2e2 commit 16b3d44

5 files changed

Lines changed: 376 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
mcp: ${{ steps.filter.outputs.mcp }}
4444
mcpCliHarness: ${{ steps.filter.outputs.mcpCliHarness }}
4545
engine: ${{ steps.filter.outputs.engine }}
46+
contract: ${{ steps.filter.outputs.contract }}
4647
discoveryIndex: ${{ steps.filter.outputs.discoveryIndex }}
4748
miner: ${{ steps.filter.outputs.miner }}
4849
minerTestHarness: ${{ steps.filter.outputs.minerTestHarness }}
@@ -188,6 +189,10 @@ jobs:
188189
engine:
189190
- 'packages/loopover-engine/**'
190191
- 'package-lock.json'
192+
contract:
193+
- 'packages/loopover-contract/**'
194+
- 'scripts/gen-contract-api-schemas.ts'
195+
- 'package-lock.json'
191196
miner:
192197
- 'packages/loopover-miner/**'
193198
- 'scripts/check-miner-package.ts'
@@ -478,6 +483,48 @@ jobs:
478483
env:
479484
NODE_OPTIONS: ""
480485
run: node --experimental-strip-types scripts/validate-observability-configs.ts
486+
# ── DRIFT CHECKS THAT CI PREVIOUSLY DID NOT RUN (#10269) ────────────────────────────────────────
487+
# `npm run test:ci` (the local gate) and this file are two independently hand-maintained lists, and
488+
# they had drifted: 22 of the 42 checks reachable from `test:ci` ran in NO workflow at all. They were
489+
# therefore enforced only when a human happened to run the full local aggregate -- which on a repo
490+
# whose gate auto-merges on green CI makes them a convention, not a gate. #10237 is the same class
491+
# having already fired twice on one artifact, found both times by a human and never by CI.
492+
#
493+
# Deliberately ONE ungated block rather than 19 individually-gated steps. Per-check path gating is
494+
# where the fidelity bugs actually live -- #10268 had to add a whole `contract` filter because
495+
# nothing here watched packages/loopover-contract/** -- and 19 hand-chosen gates would be exactly the
496+
# hand-maintained list #9860 exists to reject. These are all sub-second file-reading scripts, so
497+
# running them unconditionally costs far less than choosing 19 gates correctly.
498+
#
499+
# Ordering: this sits after "Build contract package" because the two control-plane generators import
500+
# @loopover/contract/control-plane. Everything else here reads source, package.json or workflow files.
501+
#
502+
# NOT here, on purpose: db:migrations:immutable:check, release-commit-parsing:check and
503+
# releasable-commit-types:check all need git history this job does not have (its checkout is shallow
504+
# by design). They run in `drift-checks-history` below -- see that job for why putting them here
505+
# would have been worse than leaving them unwired.
506+
- name: Drift checks (unconditional)
507+
run: |
508+
set -euo pipefail
509+
npm run turbo-inputs:check
510+
npm run workspace-dep-ranges:check
511+
npm run control-plane:contract:check
512+
npm run control-plane:openapi:check
513+
npm run coverage-boltons:check
514+
npm run ui-derived-types:check
515+
npm run server-manifest:check
516+
npm run dead-exports:check
517+
npm run publishable-deps:check
518+
npm run fixture-clock-races:check
519+
npm run typecheck-coverage:check
520+
npm run test-wiring:check
521+
npm run checkers-wired:check
522+
npm run focus-manifest-fields:check
523+
npm run maintainer-associations:check
524+
npm run release-linked-versions:check
525+
npm run mcp:tool-reference:check
526+
npm run mcp:client-config:check
527+
npm run ci-drift-checks-wired:check
481528
# Runs ahead of Typecheck AND "Test with coverage" (#ci-engine-build-order): src/mcp/find-opportunities.ts
482529
# (root backend, since #2281/#3985) imports packages/loopover-miner/lib/opportunity-fanout.js --
483530
# a .js-suffixed specifier esbuild/Vite resolve straight to the real (gitignored, uncompiled)
@@ -822,6 +869,19 @@ jobs:
822869
- name: OpenAPI drift check
823870
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.uiContract == 'true' }}
824871
run: npm run ui:openapi:check
872+
# Same generated-artifact-drift class as the OpenAPI check above, and it was enforced ONLY by the
873+
# local `npm run test:ci` aggregate -- exactly the local-only gap called out on the selfhost/miner
874+
# env-reference steps in validate-code, which each went stale with zero CI signal until someone
875+
# happened to run the full local script. It has now bitten this file twice (#10237): once when the
876+
# discovery-route schemas were added without teaching the generator, leaving `main` red for every PR
877+
# that ran the local gate, and again when #10160 added `linkedIssueMaintainerExempt` without
878+
# regenerating. Neither surfaced in CI.
879+
# Its inputs are src/openapi/schemas.ts (`backend`), apps/loopover-ui/public/openapi.json (`ui`), and
880+
# the contract package's own modules, which it scans to emit imports -- hence the third filter, added
881+
# with this step because nothing here previously watched packages/loopover-contract/** at all.
882+
- name: Contract api-schemas drift check
883+
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.contract == 'true' }}
884+
run: npm run contract:api-schemas:check
825885
# Checks apps/loopover-ui/src' known-latest MCP version string against the published package, so
826886
# its dependency is `ui` (the file it scans) + `mcp` (the package it checks against) -- NOT the
827887
# OpenAPI contract, which this script never reads.
@@ -956,6 +1016,58 @@ jobs:
9561016
# never serializes with the fast drift/typecheck/build checks -- that split remains). It ran 2023-style
9571017
# as a 3-shard matrix for wall-clock; unsharded again 2026-07-24 (see the job's own header comment) to
9581018
# trade PR latency for 2 fewer runners per PR and the simpler, flake-free single-report pipeline.
1019+
# The three drift checks that need real git history (#10269). They are a SEPARATE job purely because
1020+
# validate-code's checkout is shallow on purpose ("this job no longer uploads to Codecov ... so it has no
1021+
# reason to fetch full history anymore"), and each of these three degrades differently and silently there:
1022+
#
1023+
# - db:migrations:immutable:check exits 1 with "no orb-v* tags visible", so it would fail EVERY PR.
1024+
# - release-commit-parsing:check returns [] and PASSES when it cannot resolve the base.
1025+
# - releasable-commit-types:check prints "cannot resolve ...; skipping" and exits 0.
1026+
#
1027+
# The last two are why this is a job and not a step in validate-code: wired there they would be
1028+
# permanently green while verifying nothing, which is the "guards nothing while looking like a guard"
1029+
# failure (#9860) that #10269 exists to fix. Reproducing that at the CI layer while claiming to fix it
1030+
# would be worse than leaving them unwired, because the green would then be read as coverage.
1031+
drift-checks-history:
1032+
name: drift-checks-history
1033+
needs: changes
1034+
if: ${{ github.event_name == 'push' || github.event.pull_request.draft != true }}
1035+
runs-on: ubuntu-latest
1036+
timeout-minutes: 10
1037+
steps:
1038+
# fetch-depth: 0 + tags is the entire point of this job -- the immutability check compares migration
1039+
# blobs against every orb-v* tag, and both commit checks diff against origin/<base>.
1040+
- name: Checkout
1041+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1042+
with:
1043+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
1044+
fetch-depth: 0
1045+
fetch-tags: true
1046+
- name: Setup workspace
1047+
uses: ./.github/actions/setup-workspace
1048+
# A PR checkout leaves no ref for the base branch, and both commit checks diff against
1049+
# `origin/<base>`. Fetch it explicitly rather than assuming fetch-depth: 0 created the remote ref.
1050+
- name: Fetch base ref
1051+
if: ${{ github.event_name == 'pull_request' }}
1052+
run: git fetch --no-tags --quiet origin "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}"
1053+
# Fails loudly if the history this job exists to provide is somehow absent, rather than letting the
1054+
# two skip-on-error checks below report a green that means nothing.
1055+
- name: Assert history is present
1056+
run: |
1057+
set -euo pipefail
1058+
tags="$(git tag -l 'orb-v*' | wc -l | tr -d ' ')"
1059+
if [ "$tags" -eq 0 ]; then
1060+
echo "::error::No orb-v* tags after fetch-tags -- the history checks below would be vacuous."
1061+
exit 1
1062+
fi
1063+
echo "orb-v* tags visible: $tags"
1064+
- name: Released-migration immutability check
1065+
run: npm run db:migrations:immutable:check
1066+
- name: Release commit-parsing check
1067+
run: npm run release-commit-parsing:check
1068+
- name: Releasable commit-types check
1069+
run: npm run releasable-commit-types:check
1070+
9591071
validate-tests:
9601072
name: validate-tests
9611073
needs: changes
@@ -1283,7 +1395,7 @@ jobs:
12831395
# Path-filtered jobs report "skipped", which is treated as success.
12841396
validate:
12851397
name: validate
1286-
needs: [changes, validate-code, validate-tests]
1398+
needs: [changes, validate-code, validate-tests, drift-checks-history]
12871399
if: ${{ always() }}
12881400
# Pure result-aggregation (reads needs.*.result, echoes pass/fail) -- no build/test work, so it never
12891401
# needed the self-hosted pool's cached toolchain (#2507).

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"release-commit-parsing:check": "tsx scripts/check-release-commit-parsing.ts",
9898
"releasable-commit-types:check": "tsx scripts/check-releasable-commit-types.ts",
9999
"checkers-wired:check": "node --experimental-strip-types scripts/check-checkers-wired.ts",
100+
"ci-drift-checks-wired:check": "node --experimental-strip-types scripts/check-ci-drift-checks-wired.ts",
100101
"maintainer-associations:check": "node --experimental-strip-types scripts/check-maintainer-association-copies.ts",
101102
"regate-sort-key:check": "node --experimental-strip-types scripts/check-regate-sort-key.ts",
102103
"command-redelivery-guards:check": "node --experimental-strip-types scripts/check-command-redelivery-guards.ts",
@@ -149,7 +150,7 @@
149150
"test:smoke:browser:install": "playwright install chromium",
150151
"test:smoke:browser": "node --experimental-strip-types scripts/smoke-ui-browser.ts",
151152
"pretest:ci": "npm run check-node-version",
152-
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:migrations:immutable:check && npm run turbo-inputs:check && npm run workspace-dep-ranges:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:contract-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run control-plane:contract:check && npm run control-plane:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run server-manifest:check && npm run dead-source-files:check && npm run dead-exports:check && npm run publishable-deps:check && npm run fixture-clock-races:check && npm run typecheck-coverage:check && npm run test-wiring:check && npm run checkers-wired:check && npm run focus-manifest-fields:check && npm run release-commit-parsing:check && npm run releasable-commit-types:check && npm run regate-sort-key:check && npm run maintainer-associations:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run release-linked-versions:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run mcp:client-config:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
153+
"test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:migrations:immutable:check && npm run turbo-inputs:check && npm run workspace-dep-ranges:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:contract-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run control-plane:contract:check && npm run control-plane:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run server-manifest:check && npm run dead-source-files:check && npm run dead-exports:check && npm run publishable-deps:check && npm run fixture-clock-races:check && npm run typecheck-coverage:check && npm run test-wiring:check && npm run checkers-wired:check && npm run ci-drift-checks-wired:check && npm run focus-manifest-fields:check && npm run release-commit-parsing:check && npm run releasable-commit-types:check && npm run regate-sort-key:check && npm run maintainer-associations:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run release-linked-versions:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run mcp:client-config:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build",
153154
"test:release": "npm run test:ci && npm run changelog:check",
154155
"test:release:mcp": "npm run test:ci",
155156
"test:watch": "vitest",

0 commit comments

Comments
 (0)