From ad0b58c572a9764350e9a0cfb5927412438f8904 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 19 Jul 2026 13:59:27 -0700 Subject: [PATCH 1/2] ci: route the engine build through Turborepo, persist .turbo/cache Phase 1 of the Turborepo plan: wires turbo into ci.yml itself rather than leaving it local-only. Scoped narrowly to the single highest- value redundancy from the original CI audit -- @loopover/engine's build, currently repeated independently in validate-code and each of the 6 validate-tests shards (up to 7x per run). Both "Build engine package" steps (validate-code, validate-tests) now run `turbo run build --filter=@loopover/engine` instead of a bare npm workspace call, and both package.json orchestrators that build engine internally (build:miner, ui:build) are routed the same way -- so every call site benefits from the same cache uniformly, not just the standalone ci.yml steps. .turbo/cache is persisted via actions/cache, mirroring the existing .tsbuildinfo pattern (run_id-suffixed save key + prefix restore-keys fallback) rather than node_modules' hit-or-miss pattern, since turbo's cache grows additively rather than being wholesale-replaced per lockfile hash. Correctness doesn't depend on this restore working -- turbo content-hashes @loopover/engine's own inputs independently of whatever gets restored -- it's purely an accelerator, with the real win being cross-run reuse (same-run reuse across the 6 parallel shards is a real but non-guaranteed bonus, since they race to start). Verified end-to-end in a genuinely plain (non-worktree) clone, not just locally: confirmed turbo's cache lands at exactly `.turbo/cache` relative to repo root (the same path this PR persists) when there's no worktree involved, and confirmed a real cache hit on a second build in that same clean clone. Deliberately scoped to just the engine build for this PR -- the other build/lint/typecheck steps (ui-kit, mcp, miner, extensions) stay as plain npm calls for now, per the plan's "one cluster at a time" guidance, since each has its own conditional-gating nuances that deserve the same individual verification this one got. --- .github/workflows/ci.yml | 44 +++++++++++++++++++++-- package.json | 4 +-- test/unit/ci-engine-miner-filters.test.ts | 5 ++- 3 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9b509b9887..32bccf4476 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -362,9 +362,31 @@ jobs: # engine) and after Typecheck (broke a real, non-vi.mock `import type` from the package specifier with # TS2307 in CI while passing locally against a stale leftover dist/ -- first hit by PR #5082). Now ahead # of both. + # + # Turborepo (turbo.json) owns the actual cache correctness here (content-hashes @loopover/engine's own + # inputs independently of whatever this restore serves) -- the .turbo/cache restore below is purely an + # accelerator, same accumulating-cache shape as "Restore TypeScript incremental build cache" below (a + # run_id-suffixed save key + prefix restore-keys fallback, not the hit-or-miss node_modules pattern), + # since turbo's cache directory grows additively rather than being wholesale-replaced per lockfile hash. + # This is also what lets each validate-tests shard (which independently needs this same package built) + # skip rebuilding it from scratch when a warm cache from this job or an earlier run is available. + - name: Restore Turborepo cache + if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }} + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo/cache + key: turbo-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} + restore-keys: | + turbo-${{ hashFiles('package-lock.json') }}- - name: Build engine package if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }} - run: npm run build --workspace @loopover/engine + run: turbo run build --filter=@loopover/engine + - name: Save Turborepo cache + if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true') }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo/cache + key: turbo-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} # .tsbuildinfo mutates every run (tsc's own incremental state), unlike node_modules above which is # immutable per lockfile -- so this needs the run_id-suffixed-key + restore-keys-prefix pattern (always # creates a new cache entry to save into, restore falls back to the most recent matching prefix) rather @@ -639,9 +661,25 @@ jobs: apps/loopover-ui/node_modules key: ${{ steps.node-modules-cache.outputs.cache-primary-key }} # Any backend test run needs the engine package's dist/ built first -- see the identical step's - # comment in validate-code (#ci-engine-build-order) for why. + # comment in validate-code (#ci-engine-build-order) for why. Same accumulating .turbo/cache + # restore/save pattern as validate-code's identical pair, sharing the same key lineage -- whichever + # job (this shard or another, in this run or an earlier one) saved a warm cache first, the rest can + # reuse it. Correctness is turbo's own content-hashing, not this restore -- see validate-code's comment. + - name: Restore Turborepo cache + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo/cache + key: turbo-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} + restore-keys: | + turbo-${{ hashFiles('package-lock.json') }}- - name: Build engine package - run: npm run build --workspace @loopover/engine + run: turbo run build --filter=@loopover/engine + - name: Save Turborepo cache + if: ${{ !cancelled() }} + uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: .turbo/cache + key: turbo-${{ hashFiles('package-lock.json') }}-${{ github.run_id }} - name: Prepare test reports dir run: mkdir -p reports/junit - name: Test with coverage (shard ${{ matrix.shard }}/6) diff --git a/package.json b/package.json index cc1929021c..8b42ce7a2b 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "db:migrate:remote": "wrangler d1 migrations apply loopover --remote", "drizzle:generate": "drizzle-kit generate", "build:mcp": "npm --workspace @loopover/mcp run build", - "build:miner": "npm --workspace @loopover/engine run build && npm --workspace @loopover/miner run build", + "build:miner": "turbo run build --filter=@loopover/engine && npm --workspace @loopover/miner run build", "test:mcp-pack": "node scripts/check-mcp-package.mjs", "test:miner-pack": "node scripts/check-miner-package.mjs", "test:miner-deployment-docs-audit": "node scripts/check-miner-deployment-docs.mjs", @@ -54,7 +54,7 @@ "miner-extension:lint": "npm --workspace @loopover/miner-extension run lint", "miner-extension:typecheck": "npm --workspace @loopover/miner-extension run typecheck", "ui:kit:build": "npm run build --workspace @loopover/ui-kit", - "ui:build": "npm run ui:kit:build && npm run build --workspace @loopover/engine && npm run ui:openapi && npm run extension:build && npm run miner-extension:build && npm --workspace @loopover/ui run build && npm --workspace @loopover/ui-miner run build", + "ui:build": "npm run ui:kit:build && turbo run build --filter=@loopover/engine && npm run ui:openapi && npm run extension:build && npm run miner-extension:build && npm --workspace @loopover/ui run build && npm --workspace @loopover/ui-miner run build", "ui:preview": "npm run ui:build && wrangler dev --config apps/loopover-ui/dist/server/wrangler.json --ip 127.0.0.1 --port 4173 --local", "ui:lint": "npm run ui:kit:build && npm --workspace @loopover/ui run lint && npm --workspace @loopover/ui-miner run lint", "ui:typecheck": "npm run ui:kit:build && npm --workspace @loopover/ui run typecheck && npm --workspace @loopover/ui-miner run typecheck", diff --git a/test/unit/ci-engine-miner-filters.test.ts b/test/unit/ci-engine-miner-filters.test.ts index a18e9584bc..01375810ba 100644 --- a/test/unit/ci-engine-miner-filters.test.ts +++ b/test/unit/ci-engine-miner-filters.test.ts @@ -15,7 +15,10 @@ describe("CI engine/miner path filters", () => { expect(ci).toContain("name: Build engine package"); expect(ci).toContain("name: Build miner CLI"); expect(ci).toContain("name: Miner package check"); - expect(ci).toContain("npm run build --workspace @loopover/engine"); + // Routed through Turborepo (turbo.json) rather than a bare npm workspace call, so its build cache is + // shared across validate-code and every validate-tests shard instead of rebuilding independently in + // each -- see the "Restore/Save Turborepo cache" steps alongside this one. + expect(ci).toContain("turbo run build --filter=@loopover/engine"); expect(ci).toContain("npm run build:miner"); expect(ci).toContain("npm run test:miner-pack"); }); From fbc341015fdedfeca8c306b6d4c7db8773b6fa76 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Sun, 19 Jul 2026 14:03:25 -0700 Subject: [PATCH 2/2] fix(ci): invoke turbo via npx in bare workflow steps A bare `run:` step in a GitHub Actions workflow doesn't get node_modules/.bin on PATH the way an `npm run` script does -- unlike package.json's build:miner/ui:build (which correctly resolve turbo since npm injects that PATH for script children), ci.yml's two standalone "Build engine package" steps called `turbo` directly and failed with "command not found" (exit 127) on every job. Verified locally by reproducing the exact failure (`which turbo` fails on ambient PATH) and confirming `npx turbo` resolves it correctly, matching what actually ran successfully in every prior local test this session -- all of which happened to go through an npm-script or npx invocation, never this exact bare form. --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32bccf4476..0b69fcd398 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -380,7 +380,7 @@ jobs: turbo-${{ hashFiles('package-lock.json') }}- - name: Build engine package if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true' }} - run: turbo run build --filter=@loopover/engine + run: npx turbo run build --filter=@loopover/engine - name: Save Turborepo cache if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.engine == 'true' || needs.changes.outputs.ui == 'true') }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -673,7 +673,7 @@ jobs: restore-keys: | turbo-${{ hashFiles('package-lock.json') }}- - name: Build engine package - run: turbo run build --filter=@loopover/engine + run: npx turbo run build --filter=@loopover/engine - name: Save Turborepo cache if: ${{ !cancelled() }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0