Skip to content

ci(build): catch stale committed miner/mcp compiled output before merge - #7690

Merged
JSONbored merged 2 commits into
mainfrom
claude/js-ts-migration-analysis-f409fd
Jul 21, 2026
Merged

ci(build): catch stale committed miner/mcp compiled output before merge#7690
JSONbored merged 2 commits into
mainfrom
claude/js-ts-migration-analysis-f409fd

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • The #7290/#7291 TypeScript migrations for packages/loopover-miner and packages/loopover-mcp are complete — every .js/.d.ts in both packages is now compiler-owned, not hand-maintained. But both packages compile in place and commit the emitted output (they ship as installable CLIs, so a runnable .js has to exist without a build step for consumers), and nothing verified that the committed output actually matched a fresh build: root-level tests import the emitted .js by its literal specifier, and the job that builds these packages runs as a separate GitHub Actions job from the one that tests them — so a contributor who edited .ts without rebuilding got tests silently exercising old behavior with zero CI signal. This is the same drift class the Selfhost/Miner env-reference checks were previously added to close.
  • Adds scripts/check-build-drift.mjs (git-status-based, so a compiled file that was never committed at all is caught too, not just a stale one) and wires it into ci.yml right after each package's real build step, plus into package.json's local test:ci for pre-push parity.
  • Fixes three comments left stale by the completed migrations (packages/loopover-mcp/tsconfig.json, .github/workflows/publish-miner.yml, and one in ci.yml), and adds loopover-mcp's missing migration-complete guard test (it had none; loopover-miner already had one).
  • Also fixes a real bug found while validating the new check against #7668's freshly-merged full-execution mode: defaultPrepareExecutionWorkspace built its scratch directory straight from mkdtempSync, but coding-task-spec.ts's writeAcceptanceCriteriaFile separately realpaths the working directory before writing into it (a deliberate containment-check canonicalization). On macOS, os.tmpdir() resolves under a symlink (/var/folders/.../private/var/folders/...), so the two disagreed on which string names the same directory, and acceptanceCriteriaPath.startsWith(workingDirectory) failed even though the file genuinely was inside the working directory. Fixed by resolving once at the source; no-op on Linux CI runners.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused: both commits are backend/CI tooling in the same area (the second was discovered while validating the first against a same-day merge, not an unrelated change).
  • This follows CONTRIBUTING.md and does not touch site/, CNAME, or **/lovable/**.
  • Linked issue — this is maintainer-authored (repo owner), not a contributor PR; the linked-open-issue hard rule is contributor-scoped.

Validation

  • git diff --check — clean.
  • npm run actionlint — clean (validates the ci.yml/publish-miner.yml edits).
  • npm run typecheck — clean.
  • npm run build:mcp / npm run build:miner (via the new :check variants) — clean, both pre- and post-commit.
  • npm audit --audit-level=moderate — 0 vulnerabilities.
  • New/changed behavior has tests for new branches and fallback paths: check-build-drift-script.test.ts covers the clean/dirty/untracked-file/unknown-package-name cases (including a real scratch-git-repo mechanics test, not just injected fakes); the realpath fix has a direct regression test (workspace.path is already realpath-canonical) plus the pre-existing integration test it was breaking now passes (60/60 in miner-cross-repo-evaluation.test.ts).
  • Manually verified the new check actually catches drift: perturbed packages/loopover-miner/lib/cross-repo-evaluation.ts (the real file from #7668) without rebuilding, confirmed build:miner:check failed with a clear message, then reverted cleanly before making the real change.

If any required check was skipped, explain why:

  • npm run test:ci (the full local gate, which includes test:coverage, test:workers, ui:*) ran fully green once on this branch before the last two commits (a bug fix + a merge from main that picked up #7668) landed. Since then, validation was targeted: the affected test files, typecheck, actionlint, npm audit, and the new drift-checks themselves (pre- and post-commit) — all clean. Neither commit touches UI, workers, or anything else that full run covered. Real CI will run the authoritative version of the full suite against this exact pushed commit.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, trust scores, or private data anywhere.
  • Public GitHub text stays sanitized and low-noise.
  • Auth/cookie/CORS/GitHub App/Cloudflare/session changes — n/a, none of those touched.
  • API/OpenAPI/MCP behavior — n/a, no API surface changed.
  • UI changes — n/a, no UI files touched.
  • Public docs/changelogs — no changelog edit; the one skill-doc line updated is internal contributor tooling, not a public changelog.

Notes

  • The ci.yml/publish-miner.yml comment fixes and the new mcp-typescript-migration-complete.test.ts are small, low-risk hygiene bundled in because they're the same "stale migration bookkeeping" class this PR is otherwise about, and were found in the course of tracing exactly why packages/loopover-miner/lib/cross-repo-evaluation.js/.d.ts still show up in contributor diffs like #7668.

packages/loopover-miner and packages/loopover-mcp compile real TypeScript
in place and commit the emitted .js/.d.ts (both ship as installable CLIs,
so a runnable .js has to exist without a build step). Nothing enforced
that the committed output actually matched a fresh build: root-level
tests import the emitted .js by its literal specifier, and the job that
builds these packages runs as a separate GitHub Actions job from the one
that tests them, so a contributor who edited .ts without rebuilding got
tests silently running old behavior with zero CI signal -- the same
drift class the Selfhost/Miner env-reference checks were previously
added to close.

Adds scripts/check-build-drift.mjs (git-status-based, so a compiled file
that was never committed at all is caught too, not just a stale one),
wires it into ci.yml right after each package's real build step, and
into package.json's local test:ci for pre-push parity. Also fixes three
comments left stale by the #7290/#7291 TypeScript migrations completing,
and adds loopover-mcp's missing migration-complete guard test (mirroring
loopover-miner's existing one).
…path

defaultPrepareExecutionWorkspace built its scratch directory straight
from mkdtempSync, but coding-task-spec.ts's writeAcceptanceCriteriaFile
separately realpath's the working directory before writing into it (a
deliberate containment-check canonicalization, not something to
remove). On macOS, os.tmpdir() resolves under a symlink
(/var/folders/... -> /private/var/folders/...), so the two disagreed on
which string names the same directory: a plain
acceptanceCriteriaPath.startsWith(workingDirectory) check failed even
though the file genuinely was inside the working directory.

Resolving once at the source, right after mkdtempSync, keeps every
downstream path in the same canonical form with no other changes
needed, and is a no-op on Linux CI runners where /tmp isn't itself a
symlink.

Discovered while validating the build-drift check against #7668's own
newly-merged full-execution mode.
@JSONbored JSONbored self-assigned this Jul 21, 2026
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui d8998e7 Commit Preview URL

Branch Preview URL
Jul 21 2026, 08:30 AM

@JSONbored
JSONbored merged commit 6ac7109 into main Jul 21, 2026
6 checks passed
@JSONbored
JSONbored deleted the claude/js-ts-migration-analysis-f409fd branch July 21, 2026 08:30
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@codecov

codecov Bot commented Jul 21, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.55%. Comparing base (e2a0e7b) to head (d8998e7).
⚠️ Report is 2 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #7690   +/-   ##
=======================================
  Coverage   88.55%   88.55%           
=======================================
  Files         725      725           
  Lines       76188    76188           
  Branches    22679    22679           
=======================================
  Hits        67466    67466           
  Misses       7680     7680           
  Partials     1042     1042           
Flag Coverage Δ
rees 88.56% <ø> (ø)
shard-1 31.07% <0.00%> (-2.20%) ⬇️
shard-2 35.11% <0.00%> (-1.11%) ⬇️
shard-3 31.13% <100.00%> (-4.88%) ⬇️
shard-4 40.76% <0.00%> (-0.06%) ⬇️
shard-5 38.00% <0.00%> (+8.59%) ⬆️
shard-6 31.11% <0.00%> (-0.93%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...ckages/loopover-miner/lib/cross-repo-evaluation.ts 100.00% <100.00%> (ø)

JSONbored added a commit that referenced this pull request Jul 21, 2026
The #7690 drift-check made staleness loud, but contributors still had
to generate and commit compiled output for every .ts change -- churn
that made no sense once the #7290/#7291 migrations finished making
these two packages real TypeScript. Both were the only packages in the
repo using in-place tsc emit; everything else already builds to a
gitignored dist/.

Turns out nothing forced that: Vite/esbuild and Wrangler's own
production bundler already resolve a .js-suffixed relative import
specifier to its sibling .ts by default when no literal .js exists on
disk -- the same behavior packages/loopover-engine/src/** has always
silently relied on (it has never had a single committed .js). Verified
against the real production bundler, not just tests: `wrangler deploy
--dry-run` bundles cleanly with zero compiled output present anywhere,
confirming src/mcp/find-opportunities.ts's direct import of
packages/loopover-miner/lib/opportunity-fanout.ts resolves fine at
deploy time too.

That leaves exactly one place still needing a real file: the two
CLI-harness test suites that spawn the binary as an actual OS
subprocess (not a Vite-mediated import). Those now spawn the .ts
directly through Node's own --experimental-strip-types instead of
requiring a prior `tsc` build -- explicit rather than relying on its
default-on state, and faster than routing through tsx (measured).

So: .gitignore both packages' bin/lib compiled output, git rm --cached
the ~250 files that were tracked, and remove everything #7690 added
that's now pointless (check-build-drift.mjs, its test, the
build:{mcp,miner}:check scripts, the two migration-complete guard
tests -- there's nothing left to drift-check once nothing is
committed). build:mcp/build:miner still exist and still run in CI --
they're what validates the actual publishable artifact still compiles
and packs, same as before.

One real bug found along the way: vitest.config.ts's coverage.include
still needs BOTH the .js and .ts glob per file, not just .ts.
@vitest/coverage-v8 tracks an executed module under the id Vite
resolved it FROM (the requested .js specifier) rather than the .ts
file actually read off disk -- confirmed by removing the .js entry
once and watching genuinely-tested files (5 passing tests, real
function calls) report a flat 0% because coverage.include no longer
matched their reported id.
JSONbored added a commit that referenced this pull request Jul 21, 2026
The #7690 drift-check made staleness loud, but contributors still had
to generate and commit compiled output for every .ts change -- churn
that made no sense once the #7290/#7291 migrations finished making
these two packages real TypeScript. Both were the only packages in the
repo using in-place tsc emit; everything else already builds to a
gitignored dist/.

Turns out nothing forced that: Vite/esbuild and Wrangler's own
production bundler already resolve a .js-suffixed relative import
specifier to its sibling .ts by default when no literal .js exists on
disk -- the same behavior packages/loopover-engine/src/** has always
silently relied on (it has never had a single committed .js). Verified
against the real production bundler, not just tests: `wrangler deploy
--dry-run` bundles cleanly with zero compiled output present anywhere,
confirming src/mcp/find-opportunities.ts's direct import of
packages/loopover-miner/lib/opportunity-fanout.ts resolves fine at
deploy time too.

That leaves exactly one place still needing a real file: the two
CLI-harness test suites that spawn the binary as an actual OS
subprocess (not a Vite-mediated import). Those now spawn the .ts
directly through Node's own --experimental-strip-types instead of
requiring a prior `tsc` build -- explicit rather than relying on its
default-on state, and faster than routing through tsx (measured).

So: .gitignore both packages' bin/lib compiled output, git rm --cached
the ~250 files that were tracked, and remove everything #7690 added
that's now pointless (check-build-drift.mjs, its test, the
build:{mcp,miner}:check scripts, the two migration-complete guard
tests -- there's nothing left to drift-check once nothing is
committed). build:mcp/build:miner still exist and still run in CI --
they're what validates the actual publishable artifact still compiles
and packs, same as before.

One real bug found along the way: vitest.config.ts's coverage.include
still needs BOTH the .js and .ts glob per file, not just .ts.
@vitest/coverage-v8 tracks an executed module under the id Vite
resolved it FROM (the requested .js specifier) rather than the .ts
file actually read off disk -- confirmed by removing the .js entry
once and watching genuinely-tested files (5 passing tests, real
function calls) report a flat 0% because coverage.include no longer
matched their reported id.
JSONbored added a commit that referenced this pull request Jul 21, 2026
The #7690 drift-check made staleness loud, but contributors still had
to generate and commit compiled output for every .ts change -- churn
that made no sense once the #7290/#7291 migrations finished making
these two packages real TypeScript. Both were the only packages in the
repo using in-place tsc emit; everything else already builds to a
gitignored dist/.

Turns out nothing forced that: Vite/esbuild and Wrangler's own
production bundler already resolve a .js-suffixed relative import
specifier to its sibling .ts by default when no literal .js exists on
disk -- the same behavior packages/loopover-engine/src/** has always
silently relied on (it has never had a single committed .js). Verified
against the real production bundler, not just tests: `wrangler deploy
--dry-run` bundles cleanly with zero compiled output present anywhere,
confirming src/mcp/find-opportunities.ts's direct import of
packages/loopover-miner/lib/opportunity-fanout.ts resolves fine at
deploy time too.

That leaves exactly one place still needing a real file: the two
CLI-harness test suites that spawn the binary as an actual OS
subprocess (not a Vite-mediated import). Those now spawn the .ts
directly through Node's own --experimental-strip-types instead of
requiring a prior `tsc` build -- explicit rather than relying on its
default-on state, and faster than routing through tsx (measured).

So: .gitignore both packages' bin/lib compiled output, git rm --cached
the ~250 files that were tracked, and remove everything #7690 added
that's now pointless (check-build-drift.mjs, its test, the
build:{mcp,miner}:check scripts, the two migration-complete guard
tests -- there's nothing left to drift-check once nothing is
committed). build:mcp/build:miner still exist and still run in CI --
they're what validates the actual publishable artifact still compiles
and packs, same as before.

One real bug found along the way: vitest.config.ts's coverage.include
still needs BOTH the .js and .ts glob per file, not just .ts.
@vitest/coverage-v8 tracks an executed module under the id Vite
resolved it FROM (the requested .js specifier) rather than the .ts
file actually read off disk -- confirmed by removing the .js entry
once and watching genuinely-tested files (5 passing tests, real
function calls) report a flat 0% because coverage.include no longer
matched their reported id.
JSONbored added a commit that referenced this pull request Jul 21, 2026
The #7690 drift-check made staleness loud, but contributors still had
to generate and commit compiled output for every .ts change -- churn
that made no sense once the #7290/#7291 migrations finished making
these two packages real TypeScript. Both were the only packages in the
repo using in-place tsc emit; everything else already builds to a
gitignored dist/.

Turns out nothing forced that: Vite/esbuild and Wrangler's own
production bundler already resolve a .js-suffixed relative import
specifier to its sibling .ts by default when no literal .js exists on
disk -- the same behavior packages/loopover-engine/src/** has always
silently relied on (it has never had a single committed .js). Verified
against the real production bundler, not just tests: `wrangler deploy
--dry-run` bundles cleanly with zero compiled output present anywhere,
confirming src/mcp/find-opportunities.ts's direct import of
packages/loopover-miner/lib/opportunity-fanout.ts resolves fine at
deploy time too.

That leaves exactly one place still needing a real file: the two
CLI-harness test suites that spawn the binary as an actual OS
subprocess (not a Vite-mediated import). Those now spawn the .ts
directly through Node's own --experimental-strip-types instead of
requiring a prior `tsc` build -- explicit rather than relying on its
default-on state, and faster than routing through tsx (measured).

So: .gitignore both packages' bin/lib compiled output, git rm --cached
the ~250 files that were tracked, and remove everything #7690 added
that's now pointless (check-build-drift.mjs, its test, the
build:{mcp,miner}:check scripts, the two migration-complete guard
tests -- there's nothing left to drift-check once nothing is
committed). build:mcp/build:miner still exist and still run in CI --
they're what validates the actual publishable artifact still compiles
and packs, same as before.

One real bug found along the way: vitest.config.ts's coverage.include
still needs BOTH the .js and .ts glob per file, not just .ts.
@vitest/coverage-v8 tracks an executed module under the id Vite
resolved it FROM (the requested .js specifier) rather than the .ts
file actually read off disk -- confirmed by removing the .js entry
once and watching genuinely-tested files (5 passing tests, real
function calls) report a flat 0% because coverage.include no longer
matched their reported id.
JSONbored added a commit that referenced this pull request Jul 21, 2026
The #7690 drift-check made staleness loud, but contributors still had
to generate and commit compiled output for every .ts change -- churn
that made no sense once the #7290/#7291 migrations finished making
these two packages real TypeScript. Both were the only packages in the
repo using in-place tsc emit; everything else already builds to a
gitignored dist/.

Turns out nothing forced that: Vite/esbuild and Wrangler's own
production bundler already resolve a .js-suffixed relative import
specifier to its sibling .ts by default when no literal .js exists on
disk -- the same behavior packages/loopover-engine/src/** has always
silently relied on (it has never had a single committed .js). Verified
against the real production bundler, not just tests: `wrangler deploy
--dry-run` bundles cleanly with zero compiled output present anywhere,
confirming src/mcp/find-opportunities.ts's direct import of
packages/loopover-miner/lib/opportunity-fanout.ts resolves fine at
deploy time too.

That leaves exactly one place still needing a real file: the two
CLI-harness test suites that spawn the binary as an actual OS
subprocess (not a Vite-mediated import). Those now spawn the .ts
directly through Node's own --experimental-strip-types instead of
requiring a prior `tsc` build -- explicit rather than relying on its
default-on state, and faster than routing through tsx (measured).

So: .gitignore both packages' bin/lib compiled output, git rm --cached
the ~250 files that were tracked, and remove everything #7690 added
that's now pointless (check-build-drift.mjs, its test, the
build:{mcp,miner}:check scripts, the two migration-complete guard
tests -- there's nothing left to drift-check once nothing is
committed). build:mcp/build:miner still exist and still run in CI --
they're what validates the actual publishable artifact still compiles
and packs, same as before.

One real bug found along the way: vitest.config.ts's coverage.include
still needs BOTH the .js and .ts glob per file, not just .ts.
@vitest/coverage-v8 tracks an executed module under the id Vite
resolved it FROM (the requested .js specifier) rather than the .ts
file actually read off disk -- confirmed by removing the .js entry
once and watching genuinely-tested files (5 passing tests, real
function calls) report a flat 0% because coverage.include no longer
matched their reported id.
JSONbored added a commit that referenced this pull request Jul 21, 2026
* build(mcp,miner): stop committing compiled .js/.d.ts entirely

The #7690 drift-check made staleness loud, but contributors still had
to generate and commit compiled output for every .ts change -- churn
that made no sense once the #7290/#7291 migrations finished making
these two packages real TypeScript. Both were the only packages in the
repo using in-place tsc emit; everything else already builds to a
gitignored dist/.

Turns out nothing forced that: Vite/esbuild and Wrangler's own
production bundler already resolve a .js-suffixed relative import
specifier to its sibling .ts by default when no literal .js exists on
disk -- the same behavior packages/loopover-engine/src/** has always
silently relied on (it has never had a single committed .js). Verified
against the real production bundler, not just tests: `wrangler deploy
--dry-run` bundles cleanly with zero compiled output present anywhere,
confirming src/mcp/find-opportunities.ts's direct import of
packages/loopover-miner/lib/opportunity-fanout.ts resolves fine at
deploy time too.

That leaves exactly one place still needing a real file: the two
CLI-harness test suites that spawn the binary as an actual OS
subprocess (not a Vite-mediated import). Those now spawn the .ts
directly through Node's own --experimental-strip-types instead of
requiring a prior `tsc` build -- explicit rather than relying on its
default-on state, and faster than routing through tsx (measured).

So: .gitignore both packages' bin/lib compiled output, git rm --cached
the ~250 files that were tracked, and remove everything #7690 added
that's now pointless (check-build-drift.mjs, its test, the
build:{mcp,miner}:check scripts, the two migration-complete guard
tests -- there's nothing left to drift-check once nothing is
committed). build:mcp/build:miner still exist and still run in CI --
they're what validates the actual publishable artifact still compiles
and packs, same as before.

One real bug found along the way: vitest.config.ts's coverage.include
still needs BOTH the .js and .ts glob per file, not just .ts.
@vitest/coverage-v8 tracks an executed module under the id Vite
resolved it FROM (the requested .js specifier) rather than the .ts
file actually read off disk -- confirmed by removing the .js entry
once and watching genuinely-tested files (5 passing tests, real
function calls) report a flat 0% because coverage.include no longer
matched their reported id.

* build(scripts): convert every remaining .mjs/.d.mts pair to real .ts

Same motivation as the prior commit, extended to scripts/: a hand-maintained
.d.mts alongside a .mjs is exactly the duplicated-declaration problem the
TypeScript migration was supposed to eliminate, and it can silently drift
from the real implementation with no compiler ever checking it. Converts
all 39 remaining scripts/*.mjs files that had one -- the mcp-release/
orb-release family, every docs/settings/schema drift checker, and the rest
of the standalone generators -- to real .ts, inferring each function's
types from its actual behavior rather than trusting the old declaration,
per the pattern proven out on the miner/mcp packages. Nothing in scripts/
was ever part of Codecov's coverage surface, so this adds no coverage
obligation; it's a straight type-safety and drift-elimination win.

Real drift the old .d.mts files had already accumulated, found while
converting:
- check-schema-drift.ts read a table's name via SQLiteTable.Symbol.Name,
  an @internal drizzle-orm symbol never in its public type exports (which
  is exactly why the old hand-written declaration typed it without
  complaint) -- switched to the public getTableName().
- ci-duration-report.ts's WorkflowRun type was missing the `event` field
  the code actually filters on.
- orb-release-core.ts's IMAGE_RELEVANT_PREFIXES still named two sibling
  scripts by their old .mjs filenames, now renamed here too -- a commit
  touching either file under its real name would have silently stopped
  counting as image-relevant.

Every consumer updated to match: .js-suffixed import specifiers (Vite/
esbuild/Wrangler already resolve these to the sibling .ts, same as the
prior commit), test imports, and every real invocation site. A script
whose own file stays .mjs but now imports something converted here (e.g.
check-mcp-package.mjs importing forbidden-content.ts) needs tsx instead
of plain node, since only tsx (not node --experimental-strip-types)
resolves a same-directory .ts import transitively; a script with zero
local imports uses --experimental-strip-types directly, cheaper than
spawning tsx. Covers every affected npm script, the three release-watch
GitHub workflows (which previously needed no npm install at all --
added ./.github/actions/setup-workspace to each), the Dockerfile, and
deploy-selfhost-prebuilt.sh.

That last category caught two live regressions already sitting on this
branch from the prior commit, beyond the one this commit's own
check-miner-deployment-docs.ts conversion fixes (that one's what's been
failing this PR's own CI): packages/loopover-miner/scripts/
generate-env-reference.mjs (npm run miner:env-reference, part of
test:ci) and the Dockerfile/deploy-selfhost-prebuilt.sh's
validate-selfhost-sourcemap invocations were both silently broken the
same way -- caught by grepping for every remaining literal .mjs
reference to a converted filename repo-wide, not by any test, since the
one existing test for the miner env-reference generator imports it
through Vite (which already tolerates the mismatch) rather than
spawning it as the real subprocess the npm script actually runs.

* fix(scripts): make miner/selfhost env-reference source scanning build-independent

npm run miner:env-reference:check was reporting the committed docs stale in
CI (validate-code's "Miner env-reference drift check" step, which runs
before "Build miner CLI" in the same job) but clean locally, because
gen-selfhost-env-reference.ts's directory walk scans both .js and .ts
extensions and let whichever sorts first win the firstReference attribution
for a given env var. That was harmless while packages/loopover-miner's
compiled .js was always committed (#7290/#7291) -- every environment saw
the same file set. Once it became gitignored, build-on-demand output
(#7705), the walk's result started depending on whether a build happened to
run before it: a dev machine with a recent `npm run build:miner` sees both
lib/foo.js and lib/foo.ts and picks .js (alphabetically first); a fresh CI
checkout before its own build step sees only lib/foo.ts.

Skip a .js/.mjs/.cjs file when a same-basename .ts/.tsx sibling exists in
the same directory listing, in both gen-selfhost-env-reference.ts's walker
and generate-env-reference.mjs's own duplicate of it (used for the
generated doc's Default column) -- .ts is always the real, always-present
source; its compiled sibling is redundant now, not a second independent
reference. Verified deterministic by generating with the compiled output
physically removed and again with it present: identical output both times,
regenerated docs to match.

* fix(ci): build MCP/miner CLI binaries in validate-tests before running their harness tests

The compiled packages/loopover-{mcp,miner} bin/lib output stopped being
committed in this branch's own first commit (build(mcp,miner): stop
committing compiled .js/.d.ts entirely) -- validate-code's job already
builds both via its "Build MCP"/"Build miner CLI" steps, but
validate-tests only ever built @loopover/engine, since the compiled
binaries used to just be present from checkout for free. Its own
SKIP_MCP_CLI_HARNESS/SKIP_MINER_TEST_HARNESS gates can both be false
(true whenever a PR touches the relevant paths, which this branch does
by construction), spawning the real loopover-mcp.js/loopover-miner.js
CLI as a subprocess -- a child-process spawn has no bundler to resolve
a .js specifier to sibling .ts, unlike every other consumer of these
packages, so every mcp-cli-*/miner-* test failed with MODULE_NOT_FOUND
across all 3 shards. Mirrors validate-code's own two build steps,
unconditional like this job's pre-existing "Build engine package" --
turbo's own content-hash caching keeps an unrelated PR's cost near
zero.

* fix(build): declare @loopover/mcp#build's real turbo outputs

outputs: [] was correct back when bin/lib's compiled .js was always
committed -- present via git regardless of whether turbo actually ran
tsc or replayed a cached log entry, so a cache hit was harmless. Once
compiled output stopped being committed, that stopped being true: a
cache hit now means turbo trusts a previously-cached log and skips
running tsc again, but with no outputs declared it never snapshotted
or restores the actual files either, so the compiled output can end up
silently missing on a runner that only ever saw a "hit."

Confirmed live in this PR's own CI: validate-tests' 3 parallel shards
share one turbo-tests- GitHub Actions cache key/prefix. On the run
that added the "Build MCP" step, shard 1's @loopover/mcp:build showed
a genuine cache miss (real tsc execution) while shard 2 showed a hit
for the identical content hash and never ran tsc at all -- leaving
packages/loopover-mcp/bin/loopover-mcp.js missing on that shard's
runner despite the step reporting success, and every mcp-cli-* test
that reads or spawns it failing with ENOENT/MODULE_NOT_FOUND.

Declaring the real outputs (bin/**/*.js, lib/**/*.js) doesn't disable
caching -- unchanged source still skips a real tsc run -- it makes a
hit correct: turbo now restores the actual files from its snapshot
instead of only the log. @loopover/miner#build:tsc doesn't share this
risk (cache: false forces real execution every time, by design).

* fix(ci): force-build MCP/miner CLI binaries in validate-tests + fail fast if missing

Two more layers on top of the last two fixes (adding the build steps,
then declaring turbo's real outputs), after live CI still showed a
sporadic single-shard failure: validate-tests' 3 shards race on one
shared turbo-tests- GitHub Actions cache key, so even with correct
outputs declared, a shard's cache "hit" can still be for content a
sibling shard built moments earlier on a different runner -- valid in
principle, but one more moving part than a job with hundreds of
downstream tests depending on the result should lean on.

Also traced (and ruled out as a separate bug) why this looked, for one
run, like a genuine test regression rather than a build issue:
test/unit/support/mcp-cli-harness.ts spawns the CLI's .ts entry
directly via `node --experimental-strip-types` instead of requiring a
prior build, but --experimental-strip-types only strips types from the
directly-executed file -- it does NOT make plain Node resolve a
`.js`-suffixed relative import to a sibling .ts the way Vite/esbuild
do. Confirmed live: `node --experimental-strip-types
packages/loopover-mcp/bin/loopover-mcp.ts doctor` with no compiled
lib/ present fails ERR_MODULE_NOT_FOUND on lib/local-branch.js, the
CLI's own first local import. So every mcp-cli-*/miner-mcp-* test --
including ones already using the "fixed" harness -- still needs
bin/lib's compiled .js physically on disk; the harness's own .ts-entry
change never removed that dependency, it only changed how the entry
file itself gets its types stripped.

--force on both Build MCP/Build miner CLI steps makes this job's
correctness independent of turbo cache state entirely -- always
executes for real, never trusts a hit from any source. The packages
are small (a couple seconds each, confirmed locally), a fixed cost
worth paying to eliminate the whole cache-hit-but-file-missing failure
class rather than keep re-verifying it shard by shard. A new
verification step right after both builds fails fast with a clear
message if a binary is somehow still missing, instead of dozens of
confusing per-test MODULE_NOT_FOUND/ENOENT failures far downstream
that don't obviously point back here.

Verified locally end to end: deleted all compiled mcp/miner output and
.tsbuildinfo, ran both --force build commands from that clean state,
confirmed both binaries exist, then ran the full mcp-cli-*/mcp-*/
miner-mcp-*/miner-cli-* test suite (120 files, 974 tests) against that
real build -- all green, including mcp-cli-doctor.test.ts's "runs
doctor against a local health/session fixture", the one test that had
looked like a genuine assertion regression in CI.

* fix(test): stop asserting client_path always passes in mcp-cli-doctor.test.ts

Genuinely pre-existing, unrelated to any of the build/caching work on
this branch (confirmed: zero commits since this branch's base touch
this file). Only surfaced now because earlier CI failures on this
branch were catastrophic enough (every mcp-cli-*/miner-* test failing
on a missing binary) to mask this one underneath.

doctor's "local_repo_readiness" group includes a "client_path" check
that scans process.env.PATH for a "loopover-mcp" executable via a
plain findExecutable() -- in practice this resolves through npm's own
workspace bin-link at node_modules/.bin/loopover-mcp. npm only creates
that symlink if the bin target (bin/loopover-mcp.js) already exists at
`npm ci` time. Since build(mcp,miner): stop committing compiled .js/
.d.ts entirely stopped committing that file, every CI job's npm ci now
runs before any build step ever does, so the symlink is never created
there -- regardless of a later build succeeding. The check itself
already treats this gracefully (status "warn" with remediation
guidance, never "fail"), but the test hardcoded status: "pass" for the
whole group, which only happened to hold locally because of a stray
global @loopover/mcp install left on this machine's own PATH from
earlier, unrelated testing -- not something any CI runner, or a fresh
contributor checkout, would ever have.

Verified by literally reproducing the CI condition: moved my own
machine's global loopover-mcp symlink aside, confirmed `which
loopover-mcp` finds nothing (matching a clean CI PATH), ran this exact
test file against that state -- all 19 tests pass, including this one.
Restored the symlink afterward.
@github-actions github-actions Bot mentioned this pull request Jul 21, 2026
12 tasks
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