Skip to content

Commit 5f9e1e3

Browse files
authored
fix(docs): resolve verify-docs harness spawn enoent root cause (#2178) (#2210)
The "Node 22 test-runner spawn ENOENT" the verify-docs harness reported was not a Node regression. The real chain: 1. `wheels new` exits 0 even when it cannot locate vendor/wheels/ (it prints a red error, cleans up the partial scaffold, then returns 0). 2. `createFixture` saw exit 0 and returned the expected fixture path. 3. The next spawn used that path as `cwd`. Node's posix_spawn can't chdir there, so it reports `spawn PROGRAM ENOENT` — pointing at the program instead of the missing cwd, which read as a program-lookup bug. Changes: - `createFixture` now stats the returned path. If `wheels new` exited 0 but didn't create the directory, throw immediately with the captured stdout/stderr and a pointer to `WHEELS_FRAMEWORK_PATH`. - Workflow sets `WHEELS_FRAMEWORK_PATH` to `${{ github.workspace }}/vendor/wheels` so `wheels new` can find the framework in CI. Drops the Node 20 pin and the Node 22 restore step — both were treating the symptom. - VALIDATION.md documents the `WHEELS_FRAMEWORK_PATH` requirement for local harness runs. - Updates stale comments in exec.mjs that speculated about a Node 22 posix_spawn quirk. The `wheels new` exit-0-on-error behavior is a separate upstream bug (tracked elsewhere); catching the missing fixture dir makes the harness robust to it in the meantime.
1 parent 43d1411 commit 5f9e1e3

4 files changed

Lines changed: 61 additions & 37 deletions

File tree

.github/workflows/docs-verify.yml

Lines changed: 13 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -70,43 +70,27 @@ jobs:
7070
# the harness spawns `wheels`.
7171
run: wheels --version >/dev/null
7272

73-
- name: Set up Node.js (harness tests)
74-
# Pin Node 20 for the harness unit tests only. Node 22's test-runner
75-
# workers on Linuxbrew return spawn ENOENT on EVERY absolute path —
76-
# including /bin/bash itself — even when statSync/accessSync confirm
77-
# the file is executable and spawnable from the main process. Node 20
78-
# is unaffected. Tracked as issue #2178 (workaround pending upstream
79-
# Node fix). The main content gate (Verify v4 docs) continues on
80-
# Node 22 below.
81-
uses: actions/setup-node@v4
82-
with:
83-
node-version: 20
84-
cache: pnpm
85-
cache-dependency-path: web/pnpm-lock.yaml
86-
8773
- name: Run harness unit tests
8874
working-directory: web/sites/guides
89-
# Runs on Node 20 to avoid the Node 22 spawn ENOENT bug (see the
90-
# "Set up Node.js (harness tests)" step above). Ref #2178.
75+
# Runs on Node 22 (no longer pinned to Node 20). The "spawn ENOENT"
76+
# that looked like a Node 22 posix_spawn regression was actually
77+
# caused by `wheels new` exiting 0 after a framework-not-found
78+
# error, leaving the fixture cwd missing; Node's spawn then reports
79+
# ENOENT against the program instead of the cwd. Setting
80+
# WHEELS_FRAMEWORK_PATH at the checked-out repo's vendor/wheels
81+
# lets `wheels new` succeed, which makes fixture cwds real. See
82+
# #2178 for the full root-cause write-up.
9183
#
92-
# Still soft-fail for now: with Node 20 the spawns work, which
93-
# surfaces 7 pre-existing tutorial-driver failures (blog-tutorial
94-
# fixture's lucee.json is no longer emitted by `wheels new`).
95-
# Tracked separately — once fixtures are fixed, drop
96-
# continue-on-error so regressions fail CI as originally intended.
84+
# Still soft-fail: surfaces the remaining tutorial-driver fixture
85+
# issues (e.g., blog-tutorial's lucee.json emission). Drop
86+
# continue-on-error once those are fixed.
9787
continue-on-error: true
88+
env:
89+
WHEELS_FRAMEWORK_PATH: ${{ github.workspace }}/vendor/wheels
9890
run: |
9991
export LUCLI_HOME="$HOME/.wheels"
10092
pnpm test:docs-harness
10193
102-
- name: Set up Node.js (main)
103-
# Restore Node 22 for the content gate and the guides build below.
104-
uses: actions/setup-node@v4
105-
with:
106-
node-version: 22
107-
cache: pnpm
108-
cache-dependency-path: web/pnpm-lock.yaml
109-
11094
- name: Verify v4 docs
11195
working-directory: web/sites/guides
11296
# Soft-fail: at 290 tagged blocks / 4-way concurrency, LuCLI's

web/sites/guides/scripts/verify-docs/VALIDATION.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,21 @@ Blocks that cannot or should not compile:
110110
```cfm title="illustrative — do not type"
111111
someAPI.callThat.doesntExistYet();
112112
```
113+
114+
## Running the harness locally
115+
116+
The drivers spawn `wheels new` into a temp directory per test. `wheels new`
117+
needs a `vendor/wheels/` source tree, and a temp directory has none in its
118+
ancestry. Point the CLI at this repo's checkout before running either the
119+
harness unit tests or the full content gate:
120+
121+
```sh
122+
export WHEELS_FRAMEWORK_PATH="$(git rev-parse --show-toplevel)/vendor/wheels"
123+
pnpm --filter @wheels/guides test:docs-harness # harness unit tests
124+
pnpm --filter @wheels/guides verify:docs # full content gate
125+
```
126+
127+
Without `WHEELS_FRAMEWORK_PATH`, `wheels new` prints a framework-not-found
128+
error and exits 0 anyway; the harness detects the missing fixture directory
129+
and throws with a pointer back to this doc rather than surfacing a
130+
misleading `spawn … ENOENT` on the next child process.

web/sites/guides/scripts/verify-docs/lib/exec.mjs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ export function runExec(program, args = [], opts = {}) {
4848
if (timeout !== undefined) spawnOpts.timeout = timeout;
4949

5050
// Substitute the absolute `wheels` path resolved at module load.
51-
// Workers inherit default env from the parent; the resolved path
52-
// sidesteps PATH lookup fragility inside test-runner workers. On
53-
// Linuxbrew + node --test workers, spawn ALSO ENOENTs on absolute
54-
// paths including /bin/bash (Node 22 test-runner posix_spawn quirk
55-
// we couldn't pin down). The main verify-docs run works — only the
56-
// harness unit tests hit this; those are soft-failed in CI.
51+
// Belt-and-braces: also protects against shells where PATH doesn't
52+
// include the homebrew bin dir (e.g., a test runner spawned from a
53+
// stripped env). The separate "spawn PROGRAM ENOENT" failures we
54+
// used to see under node --test were not a Node bug — they came
55+
// from spawning with a cwd that no longer existed because
56+
// `wheels new` exited 0 despite a framework-not-found error. See
57+
// fixtures.mjs `createFixture` for the guard.
5758
const resolvedProgram = program === 'wheels' ? RESOLVED_WHEELS : program;
5859

5960
return new Promise((resolve) => {

web/sites/guides/scripts/verify-docs/lib/fixtures.mjs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdtemp, rm } from 'node:fs/promises';
1+
import { mkdtemp, rm, stat } from 'node:fs/promises';
22
import { tmpdir } from 'node:os';
33
import { join } from 'node:path';
44
import { runExec } from './exec.mjs';
@@ -39,7 +39,28 @@ export async function createFixture(name = 'fixture') {
3939
['new', name, '--no-open-browser'],
4040
{ cwd: parent },
4141
);
42-
if (result.code === 0) return join(parent, name);
42+
const expected = join(parent, name);
43+
if (result.code === 0) {
44+
// `wheels new` can exit 0 even when framework lookup fails (it prints
45+
// an error, cleans up the partial scaffold, and still returns 0). If
46+
// we don't catch that here, downstream spawns run with cwd=<missing>
47+
// and Node surfaces misleading "spawn PROGRAM ENOENT" errors that
48+
// point at the executable instead of the cwd. See #2178.
49+
try {
50+
await stat(expected);
51+
} catch {
52+
await rm(parent, { recursive: true, force: true });
53+
throw new Error(
54+
`wheels new reported success (exit 0) but did not create ${expected}.\n` +
55+
`Likely cause: the wheels CLI could not locate the framework source. ` +
56+
`Set WHEELS_FRAMEWORK_PATH to a vendor/wheels/ directory (e.g., the ` +
57+
`wheels repo checkout) before running the harness.\n` +
58+
`--- wheels stderr ---\n${result.stderr || ''}\n` +
59+
`--- wheels stdout ---\n${result.stdout || ''}`,
60+
);
61+
}
62+
return expected;
63+
}
4364
await rm(parent, { recursive: true, force: true });
4465
const combined = `${result.stderr || ''}${result.stdout || ''}`;
4566
const transient = TRANSIENT_PATTERNS.some((p) => p.test(combined));

0 commit comments

Comments
 (0)