Repository navigation
[Fix] Run Electron as Node through the Helper bundle on macOS so build workers leave the Dock alone - #519
Conversation
…d workers leave the Dock alone On macOS a Gutenberg build filled the Dock with "exec" tiles, one per build worker, and npm's own run put one up too. Every one of them is Electron running as Node through the app's shims, and libuv's darwin process.title implementation checks the process in with LaunchServices, which registers it under the Info.plist of the bundle the binary lives in. The main bundle has no LSUIElement, so each became a Foreground app. Core's grunt sets no title, which is why only Gutenberg showed it. nodeExecPath() in src/node-shims.cjs is now the one answer to which binary the shims and the runner spawns exec: on macOS the Helper bundle beside the main binary, whose Info.plist carries LSUIElement and which is the same Electron, falling back to process.execPath when no Helper is there; process.execPath untouched everywhere else, with no filesystem probe. Both ensureNodeShimDir and spawnRunner in main.js use it, so the runners stop registering too. The deep-link registration keeps process.execPath on purpose: that one must name the app. Fixes #518. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…acOS has no Helper From the self-review of c958c96. The resolver was tested but not its wiring, which is where #518 lived: spawning the runner on process.execPath again, or dropping execPath from buildChildEnv so NODE named a different binary than the runner, both passed the suite. The four runner tests now assert that buildChildEnv is told the binary the runner was spawned on, and the shim test that the node shim execs what nodeExecPath resolves; each regression fails four tests or one. The review instructions' new invariant is now literally true: it names the two uses of process.execPath that stay, the Windows spawn patch (same path there by construction) and the wpct:// registration (not a spawn). A macOS bundle with no Helper beside its binary logs one line instead of silently looking like the fix never shipped; a bare node binary, as under the unit suite, has nothing to look for and stays quiet. The resolver's comment now says the Helper registers as a UIElement, which is what lsappinfo shows, rather than "nothing". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. 📝 WalkthroughWalkthroughThe change adds Assessment against linked issues
Assessment against linked issues: Out-of-scope changes Merge Risk: ⚪ Minimal · up to The macOS Helper selection and runner wiring are covered without an identified regression. The change is ready to merge after normal checks. 🚥 Pre-merge checks | ✅ 1✅ Passed checks (1 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
✅ Action performedReview finished.
|
…tests hold on Windows CI on 5febca8: the four darwin cases of nodeExecPath failed on the Windows runner. The platform is injected, so that branch runs there too, and path.join wrote backslashes into a macOS bundle path. A bundle path is POSIX by definition; path.posix makes the derivation the same on every host. No change on macOS, where path is path.posix already, and the non-darwin branch still returns before any path work. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…528) ## Why On macOS, "Apply and rebuild" fails on Gutenberg pull requests based on trunk from before 17 April 2026, such as [WordPress/gutenberg#76292](WordPress/gutenberg#76292). The screen says only `Build failed: Command failed: npm run --if-present --workspaces --silent build`, exit 1. Without `--silent`, `@wordpress/theme`'s `build:tokens` fails: ``` [ERROR:electron/shell/app/node_main.cc:153] Node.js environment variables are disabled because this process is invoked by other apps. ✗ [config] Cannot find module '.../packages/theme/bin/terrazzo-plugin-mode-overrides/index' ``` Reported in #525. ## What changes Root cause: the Gutenberg script runs `cross-env NODE_OPTIONS=--import=esbuild-esm-loader/register tz build`. `tz` starts through our bash `node` shim, and on macOS a signed Electron removes `NODE_OPTIONS` from any process whose parent is not part of the app, so the `--import` never arrives. It is not a regression from #519: the main binary fails the same way as the Helper, and the same build passes on the system `node`. The macOS `node`/`npm`/`npx` shims now split `NODE_OPTIONS` the way Node does (quotes, backslash escapes, no globbing) and pass the options as arguments, after our `--require` and before the CLI and the caller's own arguments. Then they unset the variable, so an unsigned Electron (`npm start`, CI) does not apply every option twice and dev behaves like the release. Unset, not emptied: a signed Electron prints a `node_main.cc:153` warning whenever the variable exists, even empty. Linux and Windows shims are byte-identical to before. Deliberately not in it: only the shim's own child gets the options. What that child starts inherits no `NODE_OPTIONS`, as before this PR (follow-up below). ## How to test this Platforms: **macOS** only; the change is macOS only. Build: the Buildkite artifact for the current head. **Starting state:** a Gutenberg site on trunk, no work item linked, no pull request checked out. A copy you can throw away is best: the build rewrites `packages/theme/tokens/color.json`. 1. In **Check out a pull request**, enter `76292`, check it out and let the install finish. 2. Click **Apply and rebuild**. Expected: the build finishes. On beta.2 it fails with the message above. 3. Unlink to go back to trunk and rebuild. Expected: the trunk build finishes as before. **What must not have happened:** no `node_main.cc:153 Node.js environment variables are disabled` lines in the terminal; no Dock tiles while the build runs (#518). If the root build then fails on `routes/dashboard/package.json`, that is a different bug (a trunk-only route left behind with only `node_modules`, same with the system `node`); delete `routes/dashboard/` on the test site and rebuild. I reproduced the failure and checked the fix on a copy of a real site on the PR 76292 commit, running the app's exact command, `npm run --if-present --workspaces --silent build`: - the installed beta.2 shims: exit 1; - shims generated from this branch, with the same Helper: exit 0; - the system `node`: exit 0. The new tests in `tests/unit/node-shims.test.cjs` run the real shim under `/bin/bash` (3.2, as macOS ships it) and fail on the old code. ## Risks and limitations - An option that Node accepts only in `NODE_OPTIONS` but not as a flag: none found. Command-line-only flags are now accepted, which is more lenient, not broken. - Malformed `NODE_OPTIONS` (unterminated quote) is passed through as text where Node would refuse it. - Self-review (fresh context): no bugs; 2 fix-here findings applied (the variable cleared for the call, `/bin/bash` in the tests) and the wording corrected. 1 follow-up, the grandchild gap above. - The first version of that fix emptied the variable instead of unsetting it, and the signed Buildkite build then printed the warning for every process. The unit tests use a stand-in binary, and CI never runs a signed Electron, so only the test's expectation guards this now (it asserts the variable arrives unset). - Checked on the copy with the installed signed Helper: the workspace build and the full `npm run build` exit 0 with no warnings. ## Related Fixes #525. Found while testing #524. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Why
On macOS, a Gutenberg build (full build,
npm run dev, a PR apply, a trunk update) fills the Dock with black tiles titled exec, up to a dozen at a time, one per build worker, and they come and go with the build. A Core build shows none. Seen on the v1.2.0-beta.1 manual pass and reported as #518; it is the macOS sibling of the Windows console windows of #497.Cause, pinned and verified. Every
nodethe build runs is the app's shim, which runs the main bundle binary withELECTRON_RUN_AS_NODE=1. When such a process setsprocess.title, libuv's darwin implementation checks it in with LaunchServices, which registers it under theInfo.plistof the bundle the binary lives in. The main bundle has noLSUIElement, so each one becomes a Foreground app with a Dock tile. npm sets its own title and Gutenberg's build workers set theirs toexec; Core's grunt sets none, which is why only Gutenberg ever showed it. Without a title assignment nothing registers, spawning children included, so it is the title, not the spawn.What changes
nodeExecPath()insrc/node-shims.cjsis now the one answer to which binary the shims exec and the runners are spawned on. On macOS it is the… Helper.appbinary besideprocess.execPath, derived from the executable's own name so it holds for the packaged bundle and fornpm start'sElectron.appalike; when no Helper is there it falls back toprocess.execPath, which is what the app did before. On Windows and Linux it returnsprocess.execPathand never touches the filesystem: #497 and #512 fixed their symptom another way.The Helper is the same Electron (identical
process.versions, node 24.18.0 and electron 43.2.0), signed by the same team with the same entitlements (allow-jit,allow-unsigned-executable-memory,disable-library-validation), and itsInfo.plistis the only one of the two that carriesLSUIElement. BothensureNodeShimDir(thenode,npm,npxshims) andspawnRunner(the four runners, which are npm and set a title too) use it, andbuildChildEnvgets the same path soNODEagrees with the runner it is set in. The deep-link registration keepsprocess.execPathon purpose: that one has to name the app.Deliberately not in it: nothing changes on Windows or Linux, and no attempt to stop the workers setting a title, which is theirs to set.
AGENTS.mdand the first invariant of.github/instructions/code-review.instructions.mdnow name the resolver, so a new Node spawn onprocess.execPathreads as a regression.How to test this
Platforms: macOS; the change is a no-op elsewhere. Build: the Buildkite artifact for the current head, or
npm startfrom the repository root (the devElectron.apphas a Helper of its own, so the fix applies there too). Use a Gutenberg site you do not mind rebuilding.Starting state: a Gutenberg site past the install step.
lsappinfo list | grep -c '"exec"'. Expected:0.wpct://link from the browser. Expected: unchanged, the deep-link registration still names the app.What must not have happened: a build that fails to start (the fallback path), a build slower than trunk (the Helper lacks an entitlement), a Windows or Linux build that behaves differently in any way.
Mechanism, proven without the UI, on this head, with the dev
Electron.app: both shim variants written bynodeShim, each run asnode -e "process.title='exec'", and the child looked up inlsappinfo:type="Foreground"— a Dock tiletype="UIElement"— no tileThe same two commands on the installed v1.2.0-beta.1 app gave the same two answers and are in the issue.
Done on macOS 27 (M5 Pro), on
5febca8, on a built Gutenberg site (gutenberg-toma-2, on a work-item branch) with the Dock in view: a Gutenberg build ran with no exec tile appearing, andlsappinfo list | grep -c '"exec"'during the build answered0, where the same command on v1.2.0-beta.1 answered eleven. The build finished as before. The same check on a Core site also answered0, unchanged.Tests:
tests/unit/node-shims.test.cjs, five new cases: the packaged bundle and the devElectron.appeach resolve to their Helper, a missing Helper falls back to the main binary and probes exactly one path, Windows and Linux return the binary untouched without touching the filesystem, and a Helper path with its four spaces survives into a working shim. Lint clean, 1646 unit tests pass, all 33 journeys pass.Risks and limitations
shims: no Helper bundle found beside …), so a repackaging or a future Electron layout that misses the derivation does not read as the fix never having shipped. Builds still run either way.process.title; the fix changes what LaunchServices does with that, not the workers.Related
Fixes #518. Sibling of #497 (Windows), fixed by #512. Part of #251, v1.2.0-beta.2.
Design decisions and alternatives considered
LSUIElementon the main bundle. Rejected: the app itself must be a Foreground app with a Dock tile and a menu bar.process.titlefrom the children, through the preload the shims already carry. Rejected: it would be the app rewriting a property of every tool it runs, for a cosmetic effect, and would break any tool that reads its own title back.npm startrunsElectron.app, whose Helper isElectron Helper; a product-name rule would leave development on the old behaviour and fix the bug only where nobody sees it.npm run devwas one of those seen.Review outcome (required — see AGENTS.md)
2 [fix here] · 1 [follow-up] on
c958c96— both fix-here fixed and the follow-up taken in5febca8.Fixed: the resolver was tested but its wiring was not, and the wiring is where the bug lived. Reverting
spawnRunnertoprocess.execPath, or droppingexecPathfrombuildChildEnvsoNODEnamed a different binary than the runner, both passed the suite. The four runner tests now assert thatbuildChildEnvis told the binary the runner was spawned on, andassertShimsPreloadCompatthat the node shim execs whatnodeExecPath()resolves. Checked by mutation: the first regression fails one test, the second fails four.Fixed: the new invariant in
code-review.instructions.mdsaid any Node spawn onprocess.execPathis a regression, which the Windows spawn patch and thewpct://registration both contradicted. It now names those two as the uses that stay, and why.Taken: the silent fallback now logs one line from
ensureNodeShimDirwhen a macOS bundle has no Helper beside its binary, gated to executables inside a.appso a bare node binary under the unit suite stays quiet.Style: the resolver's comment said the Helper "registers nothing";
lsappinfoshows it registers as aUIElement, and the comment now says so.Judged and found sound, not findings: the Helper is the same Electron with the same entitlements (
allow-jit,allow-unsigned-executable-memory,disable-library-validation, checked withcodesignon the installed app); crashpad is not started underELECTRON_RUN_AS_NODE; the derivation reads the executable's own name so both real layouts resolve; the change returns before touchingfsorpathoff macOS. One thing the reviewer asked to be watched on the manual pass rather than asserted: whether the Helper's bundle id changes TCC attribution for a site under~/Documentsor~/Desktop.Review: completed —
/self-review(freshExploresubagent, judgement pass; lint, unit and journeys inline); reviewed headc958c96/ base6cb471c(trunk); evidence: findings above; outcome: 2 fixed, 1 taken.Since review:
c958c96 → 5febca8, the fixes above; then5febca8 → 6af5710, one line: CI's Windows unit run failed the four darwin cases ofnodeExecPath, because the platform is injected andpath.joinwrote backslashes into a macOS bundle path on that host; the derivation now usespath.posix, which is whatpathalready is on macOS. Lint clean, 1646 unit tests pass, all 33 journeys pass on5febca8(the last commit touches only the darwin branch of the resolver and its tests).CodeRabbit: requested on
5febca8by comment.Screenshots or recording
The change is in what is absent: the Dock during a Gutenberg build. The
lsappinfotable and the pass above are the evidence.🤖 Generated with Claude Code