Skip to content

[Add] Work on a Gutenberg issue and open a pull request to it (#251) - #269

Closed
juanmaguitar wants to merge 3 commits into
juanmaguitar/gutenberg-work-item-and-pr-flowfrom
juanmaguitar/gutenberg-issue-and-pr-authoring
Closed

juanmaguitar wants to merge 3 commits into
juanmaguitar/gutenberg-work-item-and-pr-flowfrom
juanmaguitar/gutenberg-issue-and-pr-authoring

Conversation

@juanmaguitar

Copy link
Copy Markdown
Collaborator

Why

The last piece of #251. A Gutenberg site can now clone, build, serve (#255, #261) and have a
Gutenberg pull request applied to it (#264) — but a contributor still could not work on a
Gutenberg issue and open a pull request for it
. Linking a work item and authoring a PR were both
Trac- and wordpress-develop-shaped.

Stacked on #264. The final PR of the Gutenberg feature, which merges atomically — see #255.
Do not merge alone.

What changes

  • src/renderer/github-issue.cjs (new) — the Gutenberg counterpart of trac-ticket.cjs.
    Accepts 1234, #1234 and an issue URL with the noise a copy-paste brings; refuses another
    repository's issue, another host, and — by name, because it is the obvious mistake — a pull
    request URL pasted instead of the issue it fixes
    .
  • src/work-item.cjs (new) — one interface over the two kinds, chosen by the site's project
    type. The rest of the app asks "parse this", "where does it live", "what is it called" without
    knowing which it is. Defaults to Trac, so a site with no project type is untouched.
  • sites:set-ticket validates through the site's provider. Both kinds parse to a number, so the
    ticket/<id> branch key and every reader of tracTicket are unchanged — no migration.
  • github-pr.cjs takes the project's upstream, base branch and branch prefix through the deps
    each helper already receives; buildPullRequestBody takes the project's citation line. A
    Gutenberg PR goes to WordPress/gutenberg, on a fix/issue-<n> branch, saying
    Fixes #<n> so merging it closes the issue.
  • The handoff patch header cites the same work item instead of deriving a Trac URL from the
    number.
  • UI follows the project: the work-item card, its input and the browse link. Both Trac-only
    surfaces are hidden on a Gutenberg site — the attachments panel, and the "Attach to Trac"
    destination.

testMode() deliberately still reads the environment override rather than the project, so a
Gutenberg site is not reported as sandboxed merely for not being wordpress-develop.

How to test this

Platforms: any. Needs a Gutenberg site from the earlier PRs, and a signed-in GitHub account for
step 4.

  1. On a Gutenberg site, the work-item card says GitHub issue. Paste
    https://github.com/WordPress/gutenberg/issues/71234 (or #71234). → Linked; "Open on GitHub"
    goes to the issue. A core.trac.wordpress.org URL is refused here, and a pull request URL is
    refused with "That is a pull request. Link the issue it fixes instead."
  2. The Trac attachments panel and the Attach to Trac destination are not shown.
  3. Make an edit, then Save patch for handoff. → The header says
    # Issue: https://github.com/WordPress/gutenberg/issues/71234, not a Trac URL.
  4. Open a pull request. → It targets WordPress/gutenberg, the branch is fix/issue-71234, the
    title defaults to Issue #71234, and the body's first line is Fixes #71234.
  5. On a Core site, everything is as before: "Trac ticket" card, 62281 links, attachments panel
    present, handoff header says # Ticket: …/ticket/62281, PR targets wordpress-develop on
    trac-62281 citing the Trac URL.

What must not have happened:

  • A Gutenberg pull request must never reach wordpress-develop — that is a real PR on the wrong
    project. test/github-pr.test.cjs drives the whole sequence (fork → ref → sync → tree → commit →
    branch → PR) against a fake API and asserts no call touches wordpress-develop, and the mirror
    test asserts a project-less run touches no gutenberg.
  • A Gutenberg site must not be offered a Core patch, or a button that saves a file and then does
    nothing.
  • WP_DEV_ENV_GITHUB_UPSTREAM must still redirect a run to a sandbox (it is checked before the
    project, on purpose).

Automated: npm run lint clean; 844 tests pass. New: test/github-issue.test.cjs,
test/work-item.test.cjs; extended github-pr, patch-provenance, ipc-wiring.

Risks and limitations

  • Some Core framing remains in copy this PR did not touch (e.g. "Edited files in src/?", the
    stale-trunk note's "may not apply on Trac"). Cosmetic on a Gutenberg site; a copy pass is a
    follow-up.
  • workItemProvider('github-issue') with no repoPath would build a github.com/undefined/...
    URL. Not reachable — both call sites pass the site's upstream — but the two halves of that object
    disagree about their contract, worth tightening later.

Related

Part of #251 — completes it. Stacked on #264.


Review outcome (required — see AGENTS.md)

5 [fix here] · 1 [follow-up] — all 5 fixed. Ran the review in
.github/instructions/code-review.instructions.md; judgement pass in a fresh subagent. Lint clean,
844 tests pass.

  • 🟡 Architecture · fixed — the "Attach to Trac" destination was the one Trac surface left
    ungated; on a Gutenberg site its button saved a patch and then silently did nothing
    (attachUrlFor is null). Now hidden, like the attachments panel.
  • 🟡 Security · fixed — parseIssueRef's URL branch skipped the bounds check its bare-number
    branch applied, so /issues/0 produced a falsy id (a site on ticket/0 that reads as unlinked
    and cannot open a PR) and a 20-digit path produced a float-named branch. Both branches now share
    one guard; regression tests added.
  • 🟡 Architecture · fixed — the handoff patch header derived a Trac URL from the number, so a
    Gutenberg patch cited a core.trac ticket sharing its issue number. It now takes the work item from
    the caller.
  • 🟡 Tests · fixed — nothing pinned that a Gutenberg run targets the right repo through the whole
    flow (only the pure helpers were tested). Added an end-to-end openPullRequest test per project
    asserting no call reaches the other one, plus a Gutenberg github:open-pr wiring test.
  • 🔵 Architecture · fixed — the per-project no-ticket message was unreachable behind the
    renderer's message map; dropped the duplicate so the main-process one (which knows the project)
    is what the user sees.
  • 🔵 Architecture · [follow-up] — urlFor without a repoPath (documented in Risks; unreachable).
  • Verified clean: Core byte-identity of upstream()/baseBranchFor()/branchNameFor()/
    buildPullRequestBody() with no project; the env override still wins over a project;
    testMode() ignoring the project is correct; the attachments gating leaves no orphaned effect or
    loader that could open a Trac window on a Gutenberg site; parseIssueRef rejects
    https://github.com@evil.com/… and builds its URL from the configured repo, not the input.

@juanmaguitar juanmaguitar added the gutenberg-contributions Support Gutenberg as a contribution target (#251) label Aug 11, 2026
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/gutenberg-issue-and-pr-authoring branch from f788bcf to d50f572 Compare August 11, 2026 10:57
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/gutenberg-issue-and-pr-authoring branch from d50f572 to 4aa9978 Compare August 11, 2026 13:49
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/gutenberg-issue-and-pr-authoring branch from 4aa9978 to c01ab5d Compare August 12, 2026 05:19
juanmaguitar and others added 2 commits August 12, 2026 08:33
The last half of the Gutenberg flow: linking a work item and opening a pull
request were both Trac- and wordpress-develop-shaped.

- src/renderer/github-issue.cjs: the Gutenberg counterpart of trac-ticket.cjs.
  Accepts `1234`, `#1234` and an issue URL with the noise a copy-paste brings;
  refuses another repository's issue, another host, and — by name, because it is
  the obvious mistake — a pull request URL pasted instead of the issue it fixes.
  Both branches share one bounds check, so a pasted `/issues/0` or a twenty-digit
  path cannot become a falsy id or a branch named after a float.
- src/work-item.cjs: one interface over the two, chosen by the site's project
  type. The rest of the app asks "parse this", "where does it live", "what is it
  called" without knowing which kind it is. Defaults to Trac, so a site with no
  project type behaves exactly as before.
- sites:set-ticket validates through the site's provider, so a Gutenberg site
  accepts an issue where a Core site accepts a ticket. Both parse to a number,
  so the `ticket/<id>` branch key and every reader of `tracTicket` are unchanged.
- github-pr.cjs takes the project's upstream, base branch and branch prefix
  through the `deps` each helper already receives, and buildPullRequestBody takes
  the project's citation line. A Gutenberg pull request goes to
  WordPress/gutenberg on a `fix/issue-<n>` branch and says `Fixes #<n>`, so
  merging it closes the issue.
- The handoff patch header cites the same work item, rather than deriving a Trac
  URL from the number — a Gutenberg patch must not cite a core.trac ticket that
  merely shares its issue number.
- The work-item card, its input and the browse link follow the project. Both
  Trac-only surfaces are hidden for a Gutenberg site: the attachments panel,
  which would offer a Core patch for apply into a Gutenberg checkout, and the
  "Attach to Trac" destination, whose button would otherwise save a file and
  then have nowhere to send it.

testMode() deliberately keeps reading the environment override rather than the
project, so a Gutenberg site is not reported as sandboxed simply for not being
wordpress-develop.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pull-request panel was type-aware everywhere except its copy, which is the
only part a contributor reads:

- The title hint promised "Ticket #71234" on a Gutenberg site while the handler
  sent "Issue #71234". Both now read defaultPrTitle from the provider, so the
  promise and the thing promised cannot drift apart again.

- Two prose blocks were Core-only and false for Gutenberg — "nobody watches the
  pull request list", "nothing is merged on GitHub", "triage and props live on
  the ticket". On Gutenberg the pull request IS the review venue and IS what
  gets merged, and this audience is the least able to notice the app is
  describing a different project. Gated to Core until the Gutenberg
  counterpart is written.

- The ref placeholder was duplicated as an inline ternary at two call sites,
  and the second sits inside a Trac-only block, so its Gutenberg branch could
  never render. Both now read refPlaceholder from the provider.

Also pins the per-project handoff header from both sides: the builder and the
handler were each tested, but the label and URL the handler derives from the
provider were unexercised, so a regression could cite a Core Trac ticket on a
Gutenberg patch with the suite green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/gutenberg-issue-and-pr-authoring branch from c01ab5d to ed844b8 Compare August 12, 2026 06:37
juanmaguitar added a commit that referenced this pull request Aug 12, 2026
## Why

`v1.0.0-beta.1` shipped on 10 August. Twenty changes have landed on
`trunk` since — the setup chain, the decoupled build watch, the
failed-apply explanation, the ticket's own facts, the toasts. That is
the release candidate for 1.0, so the version moves to `1.0.0-rc.1`
before the tag is cut.

## What changes

`package.json` and `package-lock.json`, and nothing else. Written by
`npm version 1.0.0-rc.1 --no-git-tag-version` rather than by hand, so
the lockfile's two copies of the string move with it.

`git grep 1.0.0-beta` outside the lockfile returns nothing, so no doc,
workflow or script carries the version. `electron-builder` derives the
artefact names from `package.json`, so the assets become:

- `wordpress-contributor-toolkit-1.0.0-rc.1-mac-arm64.dmg`
- `wordpress-contributor-toolkit-1.0.0-rc.1-win-x64.exe`
- `wordpress-contributor-toolkit-1.0.0-rc.1-linux-x86_64.AppImage`

The `rc.1` shape (not `rc1`) keeps the same form as `beta.1`, so the
filenames stay in one series.

## Scope

The open Gutenberg stack (#255 → #261 → #264 → #269 → #283) is
deliberately **not** in this release candidate. An RC stabilises what is
there; a feature of that size belongs in the release after it.

## Review

No behaviour changes, so the review standard has nothing to grade beyond
the diff itself: two version strings, produced by npm, verified against
`git grep`. Lint and unit tests run on this branch.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar

Copy link
Copy Markdown
Collaborator Author

Closing along with the rest of the stack — see #255 for the reasoning: trunk has rewritten src/main.js and src/renderer/index.jsx out from under this branch since 907cb65, so rebasing would be a semantic rebase, not a textual one.

The branch stays, so the work is retrievable. What ports forward cleanly here: src/work-item.cjs, src/renderer/github-issue.cjs and their tests.

juanmaguitar added a commit that referenced this pull request Sep 11, 2026
…395)

> Rebased out of the Gutenberg stack (#251) and retargeted at trunk. The
rest of that stack — #255, #261, #264, #269 — is closed; this change
never depended on it, it only sat on top of it. Continues #283, which
could not be retargeted because GitHub locks the base of a stacked PR.

## Why

Building a Gutenberg site never finished. The wizard sat on "Run build"
forever while the app spawned processes without bound — over 1,300 in a
few minutes, until the machine was unusable and the app had to be
killed. Nothing was ever written to `build/`.

The same checkout builds fine outside the app, so this was never a
Gutenberg problem. Core sites never hit it either: their build is Grunt,
which does not reach the code path below.

Fixes #275.

## What changes

Root cause: the `node`/`npm`/`npx` shims this app puts on `PATH` are
Electron running under `ELECTRON_RUN_AS_NODE`, and Electron keeps
`process.versions.electron` set in that mode. `yargs` reads exactly that
to decide where a command's arguments begin — "electron set,
`defaultApp` unset" reads as a packaged Electron app whose argv carries
no script path — so **every yargs-based tool started through a shim
treats its own executable path as the first argument it was given**.

For a task runner that extra argument is a command to run: itself, with
no arguments. The copy it starts does the same, forever. Each link
spawns exactly one child, which is why the process tree is an unbounded
chain rather than a fan-out.

Argument shifting is the general failure here; the runaway processes are
only its loudest form. Other tools reached through the shim have been
misreading their arguments quietly.

The fix: each shim now `--require`s a small module that hides the
Electron version from the process it starts. Two decisions worth naming,
because both were arrived at by measurement rather than by reasoning:

- **An argument, not `NODE_OPTIONS`.** `win-spawn-patch.js` uses
`NODE_OPTIONS` and is untouched — it is right there, because it must
reach a process several levels down that we never invoke ourselves. Here
we *are* the one invoking the process, and `NODE_OPTIONS` did not
survive every chain reliably in testing. An argument cannot fail to be
inherited, and it confines the patch to processes that actually go
through the shim.
- **Only `versions.electron` is hidden, not `versions.chrome`.** Hiding
both was the plan; it broke Gutenberg's bundling step outright. Build
tooling reads `chrome` to decide what it is compiling *for*, which is a
question about the output, not about who is running the compiler.

Deliberately not in this PR: `versions.v8` still carries its `-electron`
suffix (tools parse it as a version number), and the shim directory is
still a predictable path under `os.tmpdir()`.

## How to test this

Platforms: **any** for the suite. The manual path below was driven on
macOS; Windows is covered by unit tests only — see Risks.

**Starting state:** a throwaway package whose only script is
`concurrently "npm run a" "npm run b"`, with `concurrently@9`, in a site
the app can run a script in. The original Gutenberg path is no longer
reachable from trunk — that support lived in the closed stack — and it
was never needed to see this: the trigger is the task runner, not
Gutenberg.

1. Run the script through the app.
2. While it runs, watch the process count: `ps -A | grep -c
concurrently` on macOS/Linux.
3. It finishes in about a second.

**What must not have happened:** the process count must stay flat — on
the broken code the same package reached ~50 processes in three seconds
and never recovered. The run must also *finish*: a run that merely stops
spawning but hangs is the earlier, subtler half of this bug.

Which test covers it, and yes, I checked it fails on the old code:
`tests/unit/ipc-wiring.test.cjs` → *"npm:run-script"* now asserts the
shims `ensureNodeShimDir` really wrote carry the preload. Blanking the
preload path at all six `main.js` call sites — the exact way this
regresses — left the entire suite green before that assertion existed,
and now fails it. Under `npm run test:electron`,
`tests/unit/electron-node-compat.test.cjs` → *"without the preload the
child still looks like Electron"* pins the runtime condition itself.

## Risks and limitations

Review outcome: **4 `[fix here]` · 3 `[follow-up]` — all 4 fixed.**

- **Windows is unit-tested, not hand-tested.** The generated
`.cmd`/`.bat` content is asserted directly (quoting, `set` ordering,
`%*` last, backslashes kept), and the review checked it line by line,
but no one ran a Gutenberg build on a real Windows machine. Buildkite
has a signed artifact for this branch if someone wants to.
- **Two preload delivery mechanisms now exist** (`NODE_OPTIONS` in
`buildChildEnv` for `win-spawn-patch.js`, `--require` in the shims and
in the patch's redirects for `electron-node-compat.js`), with different
reach and different quoting rules. Nothing ties them together beyond the
comments in `node-shims.cjs` and `win-spawn-patch.js`. Consolidating the
choice into one documented place is a follow-up.
- **The preload reaches forks and the Windows redirects, not every
descendant.** `child_process.fork` inherits `execArgv`, so worker pools
are covered, and since the 2026-09-10 rebase the Windows spawn patch
re-attaches the `--require` to the `node`/`npm`/`npx` spawns it
redirects past the shim. A descendant started with an explicit
`spawn(process.execPath, …)`, or a `worker_threads` worker, inherits
`ELECTRON_RUN_AS_NODE` and sees `versions.electron` again. No such case
is known to be reachable today; `NODE_OPTIONS` would cover them, at the
cost of the reliability problem that ruled it out.
- The shim directory remains world-readable and predictably named under
`os.tmpdir()`. This PR adds one more file to a directory that already
holds executable shims, so it extends an existing exposure rather than
introducing one — but it is worth closing with `mkdtempSync` for all of
them.

## Related

Fixes #275. Part of #251, whose remaining PRs are closed pending a redo
against current trunk.

---

<details>
<summary>Design decisions and alternatives considered</summary>

**Preferring a real system Node over the shim.** Verified to work — the
same Gutenberg build completes in 30s through the app's own spawn path
once `node` on `PATH` is a real Node. Rejected because it does nothing
for a contributor with no Node installed, which is precisely the case
the shims exist for: the app's promise is zero prerequisites.

**Neutralising only yargs' branch** (setting `process.defaultApp`, the
other half of its condition). Narrower, and it would have fixed the
runaway. Rejected because it leaves every other library that asks "am I
inside Electron?" answering wrongly, which is the general bug.

**`NODE_OPTIONS` for the compat preload.** Implemented first, then
abandoned: measured, it did not survive every chain from the app down to
a task runner's children, while the same preload passed as an argument
did. `win-spawn-patch.js` keeps using it because it has no alternative.

**Where the shim content lives.** Moved out of `main.js` into
`src/node-shims.cjs` as pure string building, so the property that
matters — every shim, on every platform, carries the preload — is a unit
test rather than something only a real Windows machine could show.

</details>

<details>
<summary>Review outcome (required — see AGENTS.md)</summary>

**4 `[fix here]` · 3 `[follow-up]` — all 4 `[fix here]` fixed.** Run per
`.github/instructions/code-review.instructions.md`, with the judgement
pass given to a subagent with fresh context. Deterministic layer: lint
clean, 889 tests pass on both Node runtimes.

Fixed:

1. **Nothing tested the wiring that ships the fix.** The reviewer
mutated all six `main.js` call sites to pass no preload path and the
suite stayed green on both runtimes — the bug could be fully
reintroduced without a single red test. The unit tests covered
`node-shims.cjs`'s parameters, not the decision to hand it the path. Now
`ipc-wiring` reads the shims from disk.
2. **`nodeCompatPath` was passed to `buildChildEnv`, which does not
accept it.** Silently dropped, and it read as though descendants were
covered through the environment — the exact misreading that would
justify removing a `--require` from a shim later. Argument removed.
3. **Two Electron-only tests returned early instead of skipping**, so on
the system Node they reported as passing while asserting nothing. Now
`t.skip()`, and the two passes no longer report identical counts.
4. **A failed preload copy was reported with `process.stderr.write`**,
which `electron-log` does not hook, so a packaged app recorded nothing
on the one path that decides whether builds run away — and the write
itself sat outside a `try`. Now goes through the app's logger.

Deferred, with reasons:

- **The test reimplements yargs' `hideBin` heuristic** rather than
importing it, so it pins our model of the dependency rather than the
dependency. Verified faithful against `yargs` as vendored today.
Importing from a transitive dependency in a test is its own trap; left
as is, and the comment says what it models.
- **The preload does not reach `worker_threads` or an explicit
`spawn(process.execPath, …)`.** No reachable case today; noted under
Risks so the next reader does not take "an argument always survives" as
covering more than it does.
- **The shim directory is a predictable path in `os.tmpdir()`.**
Pre-existing for the shims and `win-spawn-patch.js`; fixing it properly
means `mkdtempSync` for all of them, which is a change to code this PR
does not otherwise touch.

</details>

<details>
<summary>Implementation notes</summary>

How the root cause was isolated, since the trail is not obvious from the
diff:

1. The process tree was a chain of `bash → Electron → bash → Electron`,
every one of them running `concurrently` — 25 copies **with no
arguments** alongside a single correct invocation.
2. Instrumenting the task runner's `spawn` showed the original process
launching *three* children for two commands: its own path, then the two
real ones.
3. That pointed at argument parsing rather than at process management,
and from there to `hideBin`'s Electron branch.
4. A throwaway package reproduced it in three seconds with no Gutenberg
involved — and only with `concurrently@9`, which still uses that yargs
path; `@10` does not, which is why a first attempt to reproduce failed
and briefly looked like the trigger was elsewhere.

`versions.chrome` is the interesting negative result: hiding it removed
no recursion (already gone) and broke the bundling step, and there is
now a test whose only job is to stop someone widening the set back.

</details>



---

**Rebase and re-review, 2026-09-10.** Replayed onto trunk at `74e700a`
(past the bundled-Git engine, #420 and #422) with no conflicts. The
self-review was re-run against the current standard with a fresh-context
subagent: **1 `[fix here]` · 1 `[follow-up]`**.

- Fixed, in `65c1f7e`: cross-platform 🔴. The compat preload travelled
only inside the shims, but on Windows `win-spawn-patch.js` rewrites
`spawn('node', …)` straight to Electron's binary, skipping the shim and
its `--require`. A tool reached that way saw `versions.electron` again.
The route #275 itself takes (`npm run` → `cmd.exe` → `concurrently.cmd`
→ `node` on PATH) does go through the shim, so the reported failure was
covered; the redirect route was not. `buildChildEnv` now exports
`WPTK_NODE_COMPAT_PATH` and the patch prepends the same `--require` to
its three redirects. Tests by injection in `win-spawn-patch.test.cjs`
and `npm-runner.test.cjs`, both red before the change.
- Follow-up: the two preload mechanisms, listed under Risks.

Re-verified on that head: lint clean, `npm test` 1273 pass / 2 skipped,
`npm run test:electron` 1275 pass / 0 skipped.

**Manual pass, macOS, 2026-09-10.** The throwaway package from "How to
test this" (`concurrently@9.2.4`, `build` = `concurrently "npm run a"
"npm run b"`), run through shims generated by this branch's
`node-shims.cjs` against the repo's Electron binary. Old-style shims (no
preload): 86 `concurrently` processes after six seconds, still climbing,
never finished. This branch's shims: both scripts printed, exit 0, no
leftover process.

**Earlier rebase note.** Replayed onto trunk (`a0fcbc9`) from the closed
stack; `src/main.js` and `ipc-wiring` merged without conflict. One extra
commit points the two new tests at the `tests/unit/` layout, since they
were written before #377 moved unit tests a directory deeper — nothing
they assert changed. Re-verified on trunk: lint clean, `npm test` 1058
pass / 0 fail / 2 skipped (the two Electron-runtime tests).



---

**Rebase and validation decision, 2026-09-11.** Replayed onto trunk
`dd8bc22` with no conflicts. Lint clean, `npm test` 1260 pass / 2
skipped, `npm run test:electron` 1262 pass / 0 skipped.

**The Windows manual pass was dropped on purpose, not forgotten.**
Recording the reasoning, because the earlier Risks section says someone
should run it.

There is no reachable trigger from the current product surface. The
app's terminal runs only `build`, `build:dev`, `dev`, `test`, `watch`
and `grunt`, and in `wordpress-develop` every one of those is Grunt,
which parses arguments with nopt rather than yargs. The Gutenberg build
that produced the original report is not reachable from trunk: that
support lived in a stack that is now closed. An attempt to stage the
failure by hand on Windows, with a throwaway `concurrently@9` script
installed into a site, ran into the same wall from the other side: the
terminal will not run a script outside that list.

So what this PR fixes is real but currently latent, and the evidence
available matches that status:

- macOS, by hand, on the branch's own shims: old shims reached 86
`concurrently` processes in six seconds and never finished; these shims
exit 0 and stay flat.
- Windows, by unit test: the generated `.cmd`/`.bat` content is asserted
directly (quoting, `set` ordering, `%*` last, backslashes kept),
`win-spawn-patch` and `npm-runner` cover the redirect route that
re-attaches the preload, and `ipc-wiring` reads the shims
`ensureNodeShimDir` actually wrote so the fix cannot be removed from the
call sites without a red test.

What stays open, plainly: nobody has watched a yargs-based task runner
start, misparse and recover under these shims on a real Windows machine.
If that path becomes reachable again, returning Gutenberg support being
the obvious case, run the pass before trusting this on Windows.

This is also why the change is not treated as a release blocker: it
removes a hazard rather than repairing a failure a contributor can hit
today.


**CodeRabbit round, 2026-09-11: 1 `[fix here]` fixed · 1 `[follow-up]`
filed · 1 declined.**

- Fixed in `50a1216`: a failed copy of the compat preload logged a line
and wrote the shims without `--require`, which is the runaway state
reached silently. Now `ensureNodeShimDir` forgets the directory and
throws, and `runNpmWithEngineRetry` reports it through the same "Failed
to start" surface as a spawn failure. The realistic trigger is the file
missing from the packaged bundle. Wiring test by injection, red before
the change. Note for the two Playground handlers that also call
`spawnRunner`: they now reject instead of starting without the preload,
which is the right outcome for a broken package.
- Follow-up #445: the POSIX shims interpolate paths into bash between
double quotes, so `$` and backticks in a path expand. Pre-existing in
`main.js` before this branch moved the strings; not a security boundary,
since whoever controls the app's environment already has `NODE_OPTIONS`.
- Declined: running the two Electron-only tests on the system Node by
planting a fake `process.versions.electron`. That would assert the
fixture, not the runtime; the skips are explicit by design (fix 3 of the
first review) and CI runs both passes, so both branches execute.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gutenberg-contributions Support Gutenberg as a contribution target (#251)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant