Skip to content

Commit a4976e2

Browse files
authored
fix: rebuild stale source launcher output (#2071)
1 parent f356e33 commit a4976e2

4 files changed

Lines changed: 70 additions & 21 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1309,7 +1309,7 @@ Callers can gate success on post-agent verification through `verify_steps`: an a
13091309

13101310
Each component contract declares `slug`, `path` or `source`, optional explicit `pluginFile`, optional `activate`, optional `loadAs`, and optional `readiness_probe` metadata.
13111311

1312-
The CLI binary can come from ability input, the `wp_codebox_bin` option, or the `wp_codebox_bin` filter. Source-checkout snapshots that do not include generated `dist/` files should point at `bin/wp-codebox-source.mjs`; it builds WP Codebox when needed before delegating to the compiled CLI.
1312+
The CLI binary can come from ability input, the `wp_codebox_bin` option, or the `wp_codebox_bin` filter. Source-checkout snapshots should point at `bin/wp-codebox-source.mjs`; it runs the incremental source build before every invocation so ignored `dist/` output cannot predate the checked-out core, Playground, or CLI source, then delegates to the compiled CLI.
13131313

13141314
Caller-owned runtime components can provide workspace, file, GitHub, or other tools to the sandboxed agent. WP Codebox owns the parent-site ability surface, sandbox lifecycle, and artifact capture boundary.
13151315

bin/wp-codebox-source.mjs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,18 +35,21 @@ function run(command, args, options = {}) {
3535
}
3636

3737
if (result.status !== 0) {
38+
if (options.failureContext) {
39+
console.error(`WP Codebox source entrypoint failed to ${options.failureContext} (exit ${result.status ?? 1}).`)
40+
}
3841
process.exit(result.status ?? 1)
3942
}
4043
}
4144

42-
if (!existsSync(distEntrypoint)) {
45+
const distIsAbsent = !existsSync(distEntrypoint)
46+
if (distIsAbsent) {
4347
console.error("WP Codebox CLI dist entrypoint is absent; bootstrapping the source checkout before running the CLI.")
48+
}
4449

45-
if (!existsSync(nodeModules)) {
46-
run("npm", [existsSync(packageLock) ? "ci" : "install"])
47-
}
48-
49-
run("npm", ["run", "build"])
50+
if (!existsSync(nodeModules)) {
51+
run("npm", [existsSync(packageLock) ? "ci" : "install"], { failureContext: "install source checkout dependencies" })
5052
}
5153

54+
run("npm", ["run", "build"], { failureContext: "build the source checkout" })
5255
run(process.execPath, [distEntrypoint, ...process.argv.slice(2)], { cwd: callerCwd, delegate: true })

packages/cli/README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,11 @@ entrypoint instead of an ignored build artifact:
3232
node /path/to/wp-codebox/bin/wp-codebox-source.mjs commands --json
3333
```
3434

35-
The source entrypoint runs the compiled CLI when `packages/cli/dist/index.js`
36-
exists. When `dist/` is absent, it installs dependencies if needed, runs
37-
`npm run build`, then delegates to the compiled CLI. Parent WordPress runners
38-
also fail early with a diagnostic if `wp_codebox_bin` points at a missing `.js`
39-
or `.mjs` file.
35+
The source entrypoint installs dependencies when needed, runs the incremental
36+
`npm run build` across the core, Playground, and CLI packages, then delegates to
37+
the compiled CLI. This prevents ignored `dist/` output from an older checkout
38+
from being executed as current source. Parent WordPress runners also fail early
39+
with a diagnostic if `wp_codebox_bin` points at a missing `.js` or `.mjs` file.
4040

4141
## Smoke
4242

scripts/source-checkout-entrypoint-smoke.ts

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import assert from "node:assert/strict"
22
import { execFile } from "node:child_process"
3-
import { copyFile, mkdir, mkdtemp, realpath, rm, writeFile } from "node:fs/promises"
3+
import { copyFile, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises"
44
import { tmpdir } from "node:os"
55
import { join } from "node:path"
66
import { promisify } from "node:util"
@@ -14,6 +14,11 @@ try {
1414
await mkdir(join(fixture, "bin"), { recursive: true })
1515
await mkdir(join(fixture, "node_modules"), { recursive: true })
1616
await mkdir(join(fixture, "scripts"), { recursive: true })
17+
for (const packageName of ["runtime-core", "runtime-playground", "cli"]) {
18+
await mkdir(join(fixture, "packages", packageName, "src"), { recursive: true })
19+
await writeFile(join(fixture, "packages", packageName, "src", "index.ts"), `${packageName}-v1`)
20+
}
21+
await writeFile(join(fixture, "tsconfig.json"), "config-v1")
1722
await copyFile(join(root, "bin/wp-codebox-source.mjs"), join(fixture, "bin/wp-codebox-source.mjs"))
1823

1924
await writeFile(
@@ -28,23 +33,64 @@ try {
2833

2934
await writeFile(
3035
join(fixture, "scripts/build-fixture.mjs"),
31-
`import { mkdir, writeFile } from "node:fs/promises"\n` +
36+
`import { access, mkdir, readFile, writeFile } from "node:fs/promises"\n` +
37+
`try { await access("fail-build"); console.error("fixture build failed intentionally"); process.exit(23) } catch (error) { if (error.code !== "ENOENT") throw error; }\n` +
38+
`const inputs = ["packages/runtime-core/src/index.ts", "packages/runtime-playground/src/index.ts", "packages/cli/src/index.ts", "tsconfig.json"]\n` +
39+
`const snapshot = await Promise.all(inputs.map(async (path) => await readFile(path, "utf8")))\n` +
40+
`let builds = 0\n` +
41+
`try { builds = Number(await readFile("build-count.txt", "utf8")) } catch (error) { if (error.code !== "ENOENT") throw error; }\n` +
3242
`await mkdir("packages/cli/dist", { recursive: true })\n` +
33-
`await writeFile("build-ran.txt", "yes")\n` +
34-
`await writeFile("packages/cli/dist/index.js", "console.log(JSON.stringify({ built: true, args: process.argv.slice(2), cwd: process.cwd() }))\\n")\n`,
43+
`await writeFile("build-count.txt", String(builds + 1))\n` +
44+
`await writeFile("packages/cli/dist/index.js", "const snapshot = " + JSON.stringify(snapshot) + "; console.log(JSON.stringify({ snapshot, args: process.argv.slice(2), cwd: process.cwd() })); if (process.argv.includes('exit-17')) process.exit(17)\\n")\n`,
3545
)
3646

37-
const { stdout, stderr } = await execFileAsync(process.execPath, [join(fixture, "bin/wp-codebox-source.mjs"), "commands", "--json"], { cwd: consumerWorkspace })
38-
const output = JSON.parse(stdout)
47+
const firstRun = await runLauncher("commands", "--json")
48+
const output = JSON.parse(firstRun.stdout)
3949

40-
assert.match(stderr, /dist entrypoint is absent/)
41-
assert.match(stderr, /> build/)
42-
assert.equal(output.built, true)
50+
assert.match(firstRun.stderr, /dist entrypoint is absent/)
51+
assert.match(firstRun.stderr, /> build/)
4352
assert.deepEqual(output.args, ["commands", "--json"])
4453
assert.equal(await realpath(output.cwd), await realpath(consumerWorkspace))
54+
assert.deepEqual(output.snapshot, ["runtime-core-v1", "runtime-playground-v1", "cli-v1", "config-v1"])
55+
56+
const currentRun = JSON.parse((await runLauncher("commands", "--json")).stdout)
57+
assert.deepEqual(currentRun.snapshot, output.snapshot, "current dist still passes through the incremental build")
58+
59+
const staleInputs = [
60+
["packages/runtime-core/src/index.ts", "runtime-core-v2", 0],
61+
["packages/runtime-playground/src/index.ts", "runtime-playground-v2", 1],
62+
["packages/cli/src/index.ts", "cli-v2", 2],
63+
["tsconfig.json", "config-v2", 3],
64+
] as const
65+
for (const [path, value, snapshotIndex] of staleInputs) {
66+
await writeFile(join(fixture, path), value)
67+
const rebuilt = JSON.parse((await runLauncher("commands", "--json")).stdout)
68+
assert.equal(rebuilt.snapshot[snapshotIndex], value, `${path} did not invalidate generated output`)
69+
}
70+
assert.equal(await readFile(join(fixture, "build-count.txt"), "utf8"), "6")
71+
72+
await writeFile(join(fixture, "fail-build"), "yes")
73+
await assert.rejects(runLauncher("commands", "--json"), (error: NodeJS.ErrnoException & { stderr?: string, stdout?: string }) => {
74+
assert.equal(error.code, 23)
75+
assert.equal(error.stdout, "", "stale CLI output must not run after a failed build")
76+
assert.match(error.stderr ?? "", /fixture build failed intentionally/)
77+
assert.match(error.stderr ?? "", /failed to build the source checkout \(exit 23\)/)
78+
return true
79+
})
80+
await rm(join(fixture, "fail-build"))
81+
82+
await assert.rejects(runLauncher("exit-17"), (error: NodeJS.ErrnoException & { code?: number, stderr?: string }) => {
83+
assert.equal(error.code, 17)
84+
assert.doesNotMatch(error.stderr ?? "", /source entrypoint failed/)
85+
return true
86+
})
4587

4688
console.log("Source checkout entrypoint smoke passed")
4789
} finally {
4890
await rm(fixture, { recursive: true, force: true })
4991
await rm(consumerWorkspace, { recursive: true, force: true })
5092
}
93+
94+
async function runLauncher(...args: string[]) {
95+
return await execFileAsync(process.execPath, [join(fixture, "bin/wp-codebox-source.mjs"), ...args], { cwd: consumerWorkspace })
96+
}

0 commit comments

Comments
 (0)