From 7bb5a9a96f39931366b6e99b7953bcb9429ab04b Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Fri, 31 Jul 2026 08:28:20 -0700 Subject: [PATCH] ci: run the contract api-schemas drift check in CI, not only in the local gate `contract:api-schemas:check` was reachable only from the `npm run test:ci` aggregate, so nothing in CI ever ran it. That is the same local-only gap the selfhost and miner env-reference steps already document on themselves, and it has now let this artifact drift twice with zero CI signal: once when the discovery-route schemas were added without teaching the generator, which left `main` red for every PR that ran the full local gate, and again when #10160 added `linkedIssueMaintainerExempt` without regenerating. The step is gated on the generator's three real inputs. Two were already covered -- src/openapi/schemas.ts by `backend`, apps/loopover-ui/public/ openapi.json by `ui` -- but the generator also scans the contract package's own modules to emit its imports, and no filter here watched packages/loopover-contract/** at all, so a contract-only PR would have skipped the check it most needs. Hence the new `contract` filter, shaped like its `engine` / `mcp` / `miner` siblings. This also completes the issue's "regenerating must produce a file that compiles" guard without a new test, and the pairing is worth stating: the drift check proves committed == generated, and `typecheck:packages` compiles packages/loopover-contract/src/**, which includes the generated file. Together they now prove in CI what previously only held if someone ran the local gate. The other two deliverables are already resolved on main: #10225 taught the generator to emit the discovery-route schemas, so regenerating is now a no-op rather than destructive, and the `linkedIssueMaintainerExempt` regeneration landed with it. Closes #10237 --- .github/workflows/ci.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8eaf61799..049b88954 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,7 @@ jobs: mcp: ${{ steps.filter.outputs.mcp }} mcpCliHarness: ${{ steps.filter.outputs.mcpCliHarness }} engine: ${{ steps.filter.outputs.engine }} + contract: ${{ steps.filter.outputs.contract }} discoveryIndex: ${{ steps.filter.outputs.discoveryIndex }} miner: ${{ steps.filter.outputs.miner }} minerTestHarness: ${{ steps.filter.outputs.minerTestHarness }} @@ -188,6 +189,10 @@ jobs: engine: - 'packages/loopover-engine/**' - 'package-lock.json' + contract: + - 'packages/loopover-contract/**' + - 'scripts/gen-contract-api-schemas.ts' + - 'package-lock.json' miner: - 'packages/loopover-miner/**' - 'scripts/check-miner-package.ts' @@ -822,6 +827,19 @@ jobs: - name: OpenAPI drift check if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.uiContract == 'true' }} run: npm run ui:openapi:check + # Same generated-artifact-drift class as the OpenAPI check above, and it was enforced ONLY by the + # local `npm run test:ci` aggregate -- exactly the local-only gap called out on the selfhost/miner + # env-reference steps in validate-code, which each went stale with zero CI signal until someone + # happened to run the full local script. It has now bitten this file twice (#10237): once when the + # discovery-route schemas were added without teaching the generator, leaving `main` red for every PR + # that ran the local gate, and again when #10160 added `linkedIssueMaintainerExempt` without + # regenerating. Neither surfaced in CI. + # Its inputs are src/openapi/schemas.ts (`backend`), apps/loopover-ui/public/openapi.json (`ui`), and + # the contract package's own modules, which it scans to emit imports -- hence the third filter, added + # with this step because nothing here previously watched packages/loopover-contract/** at all. + - name: Contract api-schemas drift check + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.contract == 'true' }} + run: npm run contract:api-schemas:check # Checks apps/loopover-ui/src' known-latest MCP version string against the published package, so # its dependency is `ui` (the file it scans) + `mcp` (the package it checks against) -- NOT the # OpenAPI contract, which this script never reads.