|
43 | 43 | mcp: ${{ steps.filter.outputs.mcp }} |
44 | 44 | mcpCliHarness: ${{ steps.filter.outputs.mcpCliHarness }} |
45 | 45 | engine: ${{ steps.filter.outputs.engine }} |
| 46 | + contract: ${{ steps.filter.outputs.contract }} |
46 | 47 | discoveryIndex: ${{ steps.filter.outputs.discoveryIndex }} |
47 | 48 | miner: ${{ steps.filter.outputs.miner }} |
48 | 49 | minerTestHarness: ${{ steps.filter.outputs.minerTestHarness }} |
@@ -188,6 +189,10 @@ jobs: |
188 | 189 | engine: |
189 | 190 | - 'packages/loopover-engine/**' |
190 | 191 | - 'package-lock.json' |
| 192 | + contract: |
| 193 | + - 'packages/loopover-contract/**' |
| 194 | + - 'scripts/gen-contract-api-schemas.ts' |
| 195 | + - 'package-lock.json' |
191 | 196 | miner: |
192 | 197 | - 'packages/loopover-miner/**' |
193 | 198 | - 'scripts/check-miner-package.ts' |
@@ -478,6 +483,55 @@ jobs: |
478 | 483 | env: |
479 | 484 | NODE_OPTIONS: "" |
480 | 485 | 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: mcp:tool-reference:check resolves @loopover/engine through node_modules to |
| 503 | + # its dist/, so it cannot run before "Build engine package" -- it has its own step below, carrying |
| 504 | + # that build's condition rather than a gate chosen from its own inputs. It is the ONLY check in this |
| 505 | + # set with a build prerequisite; verified by deleting packages/loopover-engine/dist and re-running |
| 506 | + # all of them, which is also how CI caught it here rather than anyone predicting it. |
| 507 | + # |
| 508 | + # NOT here either: db:migrations:immutable:check, release-commit-parsing:check and |
| 509 | + # releasable-commit-types:check all need git history this job does not have (its checkout is shallow |
| 510 | + # by design). They run in `drift-checks-history` below -- see that job for why putting them here |
| 511 | + # would have been worse than leaving them unwired. |
| 512 | + - name: Drift checks (unconditional) |
| 513 | + run: | |
| 514 | + set -euo pipefail |
| 515 | + npm run turbo-inputs:check |
| 516 | + npm run workspace-dep-ranges:check |
| 517 | + npm run control-plane:contract:check |
| 518 | + npm run control-plane:openapi:check |
| 519 | + npm run coverage-boltons:check |
| 520 | + npm run ui-derived-types:check |
| 521 | + npm run server-manifest:check |
| 522 | + npm run dead-exports:check |
| 523 | + npm run publishable-deps:check |
| 524 | + npm run fixture-clock-races:check |
| 525 | + npm run typecheck-coverage:check |
| 526 | + npm run test-wiring:check |
| 527 | + npm run checkers-wired:check |
| 528 | + npm run focus-manifest-fields:check |
| 529 | + npm run maintainer-associations:check |
| 530 | + npm run release-linked-versions:check |
| 531 | + npm run mcp:client-config:check |
| 532 | + npm run record-mapper-fields:check |
| 533 | + npm run dispatch-provenance:check |
| 534 | + npm run ci-drift-checks-wired:check |
481 | 535 | # Runs ahead of Typecheck AND "Test with coverage" (#ci-engine-build-order): src/mcp/find-opportunities.ts |
482 | 536 | # (root backend, since #2281/#3985) imports packages/loopover-miner/lib/opportunity-fanout.js -- |
483 | 537 | # a .js-suffixed specifier esbuild/Vite resolve straight to the real (gitignored, uncompiled) |
@@ -545,6 +599,16 @@ jobs: |
545 | 599 | - name: Build engine package |
546 | 600 | if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.miner == 'true' }} |
547 | 601 | run: npx turbo run build --filter=@loopover/engine |
| 602 | + # The one drift check with a build prerequisite (#10269): it resolves a bare `@loopover/engine` |
| 603 | + # specifier through node_modules to dist/, so it must follow the build above and carries that build's |
| 604 | + # condition VERBATIM. The gate is therefore derived ("run iff the thing I need was built"), not |
| 605 | + # hand-picked from this check's own inputs -- which is the distinction that keeps it out of the |
| 606 | + # hand-maintained-gate class the unconditional block above exists to avoid. If the two conditions ever |
| 607 | + # diverge this check silently stops running, so they must move together, exactly as the Typecheck step |
| 608 | + # and this build already must. |
| 609 | + - name: MCP tool-reference drift check |
| 610 | + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.miner == 'true' }} |
| 611 | + run: npm run mcp:tool-reference:check |
548 | 612 | # Mirrors "MCP package check"/"Miner package check" below: the published npm tarball is a |
549 | 613 | # different surface than the workspace build above (files field, forbidden paths/content, |
550 | 614 | # stale README wording) and needs its own dry-run validation (#8591). |
@@ -822,6 +886,19 @@ jobs: |
822 | 886 | - name: OpenAPI drift check |
823 | 887 | if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.uiContract == 'true' }} |
824 | 888 | run: npm run ui:openapi:check |
| 889 | + # Same generated-artifact-drift class as the OpenAPI check above, and it was enforced ONLY by the |
| 890 | + # local `npm run test:ci` aggregate -- exactly the local-only gap called out on the selfhost/miner |
| 891 | + # env-reference steps in validate-code, which each went stale with zero CI signal until someone |
| 892 | + # happened to run the full local script. It has now bitten this file twice (#10237): once when the |
| 893 | + # discovery-route schemas were added without teaching the generator, leaving `main` red for every PR |
| 894 | + # that ran the local gate, and again when #10160 added `linkedIssueMaintainerExempt` without |
| 895 | + # regenerating. Neither surfaced in CI. |
| 896 | + # Its inputs are src/openapi/schemas.ts (`backend`), apps/loopover-ui/public/openapi.json (`ui`), and |
| 897 | + # the contract package's own modules, which it scans to emit imports -- hence the third filter, added |
| 898 | + # with this step because nothing here previously watched packages/loopover-contract/** at all. |
| 899 | + - name: Contract api-schemas drift check |
| 900 | + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.contract == 'true' }} |
| 901 | + run: npm run contract:api-schemas:check |
825 | 902 | # Checks apps/loopover-ui/src' known-latest MCP version string against the published package, so |
826 | 903 | # its dependency is `ui` (the file it scans) + `mcp` (the package it checks against) -- NOT the |
827 | 904 | # OpenAPI contract, which this script never reads. |
@@ -956,6 +1033,58 @@ jobs: |
956 | 1033 | # never serializes with the fast drift/typecheck/build checks -- that split remains). It ran 2023-style |
957 | 1034 | # as a 3-shard matrix for wall-clock; unsharded again 2026-07-24 (see the job's own header comment) to |
958 | 1035 | # trade PR latency for 2 fewer runners per PR and the simpler, flake-free single-report pipeline. |
| 1036 | + # The three drift checks that need real git history (#10269). They are a SEPARATE job purely because |
| 1037 | + # validate-code's checkout is shallow on purpose ("this job no longer uploads to Codecov ... so it has no |
| 1038 | + # reason to fetch full history anymore"), and each of these three degrades differently and silently there: |
| 1039 | + # |
| 1040 | + # - db:migrations:immutable:check exits 1 with "no orb-v* tags visible", so it would fail EVERY PR. |
| 1041 | + # - release-commit-parsing:check returns [] and PASSES when it cannot resolve the base. |
| 1042 | + # - releasable-commit-types:check prints "cannot resolve ...; skipping" and exits 0. |
| 1043 | + # |
| 1044 | + # The last two are why this is a job and not a step in validate-code: wired there they would be |
| 1045 | + # permanently green while verifying nothing, which is the "guards nothing while looking like a guard" |
| 1046 | + # failure (#9860) that #10269 exists to fix. Reproducing that at the CI layer while claiming to fix it |
| 1047 | + # would be worse than leaving them unwired, because the green would then be read as coverage. |
| 1048 | + drift-checks-history: |
| 1049 | + name: drift-checks-history |
| 1050 | + needs: changes |
| 1051 | + if: ${{ github.event_name == 'push' || github.event.pull_request.draft != true }} |
| 1052 | + runs-on: ubuntu-latest |
| 1053 | + timeout-minutes: 10 |
| 1054 | + steps: |
| 1055 | + # fetch-depth: 0 + tags is the entire point of this job -- the immutability check compares migration |
| 1056 | + # blobs against every orb-v* tag, and both commit checks diff against origin/<base>. |
| 1057 | + - name: Checkout |
| 1058 | + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 |
| 1059 | + with: |
| 1060 | + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} |
| 1061 | + fetch-depth: 0 |
| 1062 | + fetch-tags: true |
| 1063 | + - name: Setup workspace |
| 1064 | + uses: ./.github/actions/setup-workspace |
| 1065 | + # A PR checkout leaves no ref for the base branch, and both commit checks diff against |
| 1066 | + # `origin/<base>`. Fetch it explicitly rather than assuming fetch-depth: 0 created the remote ref. |
| 1067 | + - name: Fetch base ref |
| 1068 | + if: ${{ github.event_name == 'pull_request' }} |
| 1069 | + run: git fetch --no-tags --quiet origin "+refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }}" |
| 1070 | + # Fails loudly if the history this job exists to provide is somehow absent, rather than letting the |
| 1071 | + # two skip-on-error checks below report a green that means nothing. |
| 1072 | + - name: Assert history is present |
| 1073 | + run: | |
| 1074 | + set -euo pipefail |
| 1075 | + tags="$(git tag -l 'orb-v*' | wc -l | tr -d ' ')" |
| 1076 | + if [ "$tags" -eq 0 ]; then |
| 1077 | + echo "::error::No orb-v* tags after fetch-tags -- the history checks below would be vacuous." |
| 1078 | + exit 1 |
| 1079 | + fi |
| 1080 | + echo "orb-v* tags visible: $tags" |
| 1081 | + - name: Released-migration immutability check |
| 1082 | + run: npm run db:migrations:immutable:check |
| 1083 | + - name: Release commit-parsing check |
| 1084 | + run: npm run release-commit-parsing:check |
| 1085 | + - name: Releasable commit-types check |
| 1086 | + run: npm run releasable-commit-types:check |
| 1087 | + |
959 | 1088 | validate-tests: |
960 | 1089 | name: validate-tests |
961 | 1090 | needs: changes |
@@ -1283,7 +1412,7 @@ jobs: |
1283 | 1412 | # Path-filtered jobs report "skipped", which is treated as success. |
1284 | 1413 | validate: |
1285 | 1414 | name: validate |
1286 | | - needs: [changes, validate-code, validate-tests] |
| 1415 | + needs: [changes, validate-code, validate-tests, drift-checks-history] |
1287 | 1416 | if: ${{ always() }} |
1288 | 1417 | # Pure result-aggregation (reads needs.*.result, echoes pass/fail) -- no build/test work, so it never |
1289 | 1418 | # needed the self-hosted pool's cached toolchain (#2507). |
|
0 commit comments