Skip to content

fix(ci): stop mise run ci racing bootstrap, and itself, on a fresh tree - #38

Merged
yashau merged 1 commit into
mainfrom
claude/ci-bootstrap-ordering
Aug 20, 2026
Merged

fix(ci): stop mise run ci racing bootstrap, and itself, on a fresh tree#38
yashau merged 1 commit into
mainfrom
claude/ci-bootstrap-ordering

Conversation

@yashau

@yashau yashau commented Aug 20, 2026

Copy link
Copy Markdown
Owner

Two ways mise run ci was unreliable. Both are tasks in one run reaching the same files at the same time, so they are fixed the same way — with wait_for, which this repo already uses for exactly this.

1. It raced bootstrap on a fresh tree

depends runs in parallel, so listing bootstrap first in [tasks.ci] did not make it finish first. openapi:check and build:mcp need the pnpm workspace installed and did not declare it, so on a fresh clone or worktree they started before bootstrap:js and failed with:

Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@scalar/hono-api-reference'
  imported from packages/app/src/lib/server/http/app.ts

The gate then aborted the whole DAG seconds in — bootstrap never even appeared in the output, and lint, typecheck, test:js and e2e never ran. So the first mise run ci in any new worktree failed on something unrelated to the change under test, and named a missing npm package rather than a missing bootstrap step.

That also quietly undercut the guarantee AGENTS.md makes about this command being a superset of ci.yml: the real workflow works because it installs dependencies as an explicit earlier step.

Four tasks now declare what they need: openapi, openapi:check, test:mcp, build:mcp.

openapi carries a comment because it is the counterintuitive one — the script imports nothing from node_modules itself, it reaches them by loading the Hono router.

Deliberately left alone: lint:loc, test:scripts, test:action and version:check also shell out to node, but their scripts import only Node builtins and relative paths, so they do not need the workspace. version:check passing during the failing run corroborates that.

2. It raced itself

lint:docs and lint:svelte each re-run a check that typecheck also reaches through pnpm -r. That overlap is intentional and both tasks' comments explain why — this PR keeps it. But it has to be sequenced, not merely tolerated, because both halves write generated state:

  • astro check syncs its content layer through a temporary file renamed over packages/docs/node_modules/.astro/data-store.json. Two of them racing that rename leaves the loser reporting an ENOENT from MutableDataStore for a .tmp path the other process already moved — which reads as a broken docs build rather than as two copies of one check. Observed intermittently, and it passes when run on its own, which is what makes it look like a docs problem.
[UnknownFilesystemError]
  Caused by: ENOENT: no such file or directory, rename
    'packages\docs\node_modules\.astro\data-store.json.tmp'
    -> 'packages\docs\node_modules\.astro\data-store.json'
  • lint:svelte is ordered for the same structural reason: its first step is svelte-kit sync, which generates into .svelte-kit/. Two copies of one generator writing one directory is a race whether or not it has surfaced. To be explicit: this one has not been observed failing — it is fixed because it is the same shape, not because it broke.

Both now wait_for = ["bootstrap:js", "typecheck"].

Verification

Wiped every node_modules in the tree — root, all five packages/*, e2e, action — and ran the gate from that state, which is the exact condition that used to fail within seconds.

  • openapi:check now waits for bootstrap:js and passes.
  • Sequencing is visible in the log: [typecheck] Finished in 31.14s at line 2341, then [lint:svelte] $ … and [lint:docs] $ … at 2342-2343. No overlapping astro check.
  • Full gate green, all 32 leaves: test:rust 663, miri 127, test:js 1152, e2e 101, plus the whole lint/typecheck/audit/deny set.

Note on wait_for semantics: it orders tasks within a run that schedules both, so it does not turn mise run openapi:check on a never-bootstrapped tree into a self-installing command — mise run bootstrap is still the documented setup step. That is the same contract every existing wait_for in this file has, and it keeps each leaf individually runnable as AGENTS.md promises.

🤖 Generated with Claude Code

…tree

Two ways the pre-flight gate was unreliable. Both come from tasks in the same
run reaching the same files at the same time.

`depends` runs in parallel, so listing `bootstrap` first in `ci` did not make it
finish first. `openapi:check` and `build:mcp` need the pnpm workspace installed
and did not say so, so on a fresh clone or worktree they started before
`bootstrap:js` and failed naming a missing npm package rather than a missing
bootstrap -- with the whole gate aborting seconds in, before lint, typecheck,
test:js or e2e had run. The repo already had the mechanism for this: `wait_for`,
which `fmt:js`, `typecheck`, `test:js` and `e2e` all use. The four tasks that
need the workspace and lacked it now declare it.

`openapi` is the counterintuitive one and carries a comment: the script imports
nothing from node_modules itself, it reaches them by loading the Hono router.
`lint:loc`, `test:scripts`, `test:action` and `version:check` also shell out to
node and are deliberately left alone -- they import only builtins and relative
paths, and `version:check` passed in the failing run that proves it.

Separately, `lint:docs` and `lint:svelte` each deliberately re-run a check that
`typecheck` also reaches through `pnpm -r`, and their comments say why. That
overlap has to be sequenced rather than merely tolerated. `astro check` syncs
its content layer through a temporary file renamed over
`packages/docs/node_modules/.astro/data-store.json`, and two of them racing that
rename leaves the loser reporting an ENOENT from `MutableDataStore` for a path
the other process already moved -- which reads as a broken docs build rather
than as two copies of one check. Observed intermittently, and now ordered after
`typecheck`. `lint:svelte` is ordered for the same structural reason: its first
step is `svelte-kit sync`, generating into `.svelte-kit/`, and two copies of one
generator writing one directory is a race whether or not it has surfaced.

Verified by wiping every node_modules and running the gate from that state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@yashau
yashau merged commit 1ba3fa9 into main Aug 20, 2026
19 checks passed
@yashau
yashau deleted the claude/ci-bootstrap-ordering branch August 20, 2026 23:44
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