Repository navigation
[Add] Serve a Gutenberg site through Playground as a plugin in a stock WordPress (#251) - #471
Merged
juanmaguitar merged 7 commits intoSep 17, 2026
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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 |
juanmaguitar
added this pull request to stack #477
September 15, 2026 09:12
juanmaguitar
force-pushed
the
juanmaguitar/251-playground-serve
branch
from
September 16, 2026 10:42
fee96e5 to
ac39d80
Compare
…k WordPress (#251) A Core site's build/ is a whole WordPress, and the runner mounts it as the docroot before install and skips the download. A Gutenberg checkout is a plugin: Playground installs its stock WordPress, the checkout is mounted at wp-content/plugins/gutenberg and activated. playground-plan.cjs (ported from the closed #261) turns a strategy into the runCLI mount, install-mode and blueprint-step options, and names the constants a strategy needs on top of the shared debug and SMTP set: the plugin mount is the contributor's working tree, .git included, so it sets DISALLOW_FILE_MODS and DISALLOW_FILE_EDIT, otherwise Plugins > Delete in the served site removes the checkout. The runner takes one JSON argument instead of a bare build directory; playground:start reads the site's type and writes the config, and runs the runner from the checkout for a plugin mount, from build/ as before for a docroot. The map that playground:stop and the quit sweep walk is keyed as it was. Wiring tests pin the Core options byte for byte and the plugin mount separately; nothing on screen changes, since nothing can create a Gutenberg site yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ug that is not a name (#251) From the pre-PR review. The runner resolves the host paths it is given, and the new assertions compared them to POSIX literals, which on Windows resolve to a drive-rooted path: four red jobs. The expected values are resolved the same way now. Two things taken with it: the plugin slug is refused unless it is a plain name, since it becomes a path segment under the served plugins directory; and the docroot plan no longer adds the two empty arrays the old runner never sent, so the Core options are literally the ones they were. The comments say that a first Gutenberg serve needs the network for the WordPress release Playground then caches. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
juanmaguitar
force-pushed
the
juanmaguitar/251-playground-serve
branch
from
September 17, 2026 06:57
ac39d80 to
1283b18
Compare
juanmaguitar
added a commit
that referenced
this pull request
Sep 17, 2026
…251) (#476) ## Why A contributor who wants to work on the block editor has no way in: every site the app creates is a `wordpress-develop` checkout. The three PRs under this one taught the app to clone, build, watch and serve a Gutenberg checkout; none of them let anyone ask for one. This is the PR that does, now that the whole path works. #251 has the case. ## What changes The create-site dialog gains a **Contribute to** choice, **WordPress Core** or **Gutenberg**, each with a line saying what the app does with such a site today, read off the registry so the copy lives in one place. Core is selected by default. The choice goes to `wordpress:setup` as the `projectType` the stack already accepts, and the button and dialog are renamed **Create a site**, since "Create WordPress Core site" is now false. The rename reaches the two specs and the screenshot script that click it, and the two screenshots that show it are regenerated. A Gutenberg site is told apart on screen and shown only what applies. The sidebar row and the site header carry a **Gutenberg** tag; the checklist's words are the target's (the repository it clones, the packages it compiles, the WordPress it launches with the checkout as its plugin), from a `setup` block in the registry, and the type reaches the row from the moment the dialog closes because the placeholder carries the choice. The Trac-shaped cards show only where the work item is a Trac ticket: on a Gutenberg site the ticket card gives way to a line saying that working on a GitHub issue from here, with its branch and its pull request, comes in a later version; the apply card is **Check out a pull request** with no patch-file path; **Attach to Trac** is not offered while **Hand it to a mentor** still is; the pull-request destination says the flow is not available yet before it invites a sign-in; a `wpct://ticket` link that arrives while a Gutenberg site is in front is turned away by name, kept for a Core site, and the note hides on that site only. Every one of those strings lives in the registry's `cards` block, and a test pins that no card of a non-Trac target speaks of Trac. Docs: creating a site (the new field, what each choice clones, the tag and what a Gutenberg page leaves out), getting started, the setup wizard, the terminal (the Gutenberg script list, and why `test` is not on it), running the site (a Gutenberg site is a plugin inside a stock WordPress; what the file-modification lock means), Trac tickets and applying patches (which cards are Core-only). The opt-in real-setup lane now runs one job per target and platform (a Core setup alone takes half an hour on the Windows runner, and two in one job hit its limit): the Gutenberg variant also logs in and reads the Plugins screen (the checkout is the active Gutenberg, and cannot be deleted), and checks that stopping the watch leaves no process of its tree alive (POSIX; Windows is probed by the workflow only for the flow). Its name field is filled after the choice and read back until right: in the first seconds of a freshly started app the value Playwright filled came back with letters added or dropped (`real-setupes`, `real-setu`), which sent the site to a folder the test was not watching. Not reproduced by hand, nor in a warmed-up app across a dozen tries; noted here rather than hidden. Deliberately not in this PR: a GitHub issue as the work item (the ticket card still asks for a Trac ticket on a Gutenberg site, and its linked-PR list searches `wordpress-develop`), opening a pull request against `WordPress/gutenberg`, and `wpct://issue/N`. They are the next three PRs; until they land a Gutenberg site is for building, running and trying pull requests by checkout (which already fetches from the site's own origin), not for filing work. So that a Gutenberg site cannot open a pull request against `wordpress-develop` in the meantime, `github:open-pr` refuses any site that is not Core, with a sentence that points at the patch file; the choice's copy and the docs say only what is wired. ## How to test this Platforms: **macOS and Windows**; the Gutenberg path spawns npm and Playground through the platform shims. Build: the Buildkite artifact for the current head. The **Real WordPress setup** workflow is the pass: it now creates one site per target and, for Gutenberg, logs in and reads the Plugins screen (Gutenberg active, no **Delete** link anywhere) and checks that Stop leaves no watcher process behind (POSIX). Run locally by the author on macOS, 15 Sep, current head: the Gutenberg variant passed in 4.0 minutes end to end. Runs on this branch: https://github.com/WordPress/contributor-toolkit/actions/runs/34955889102 on `ecab2bd` with one job per target and platform: **Core green on macOS (2.7 min) and Windows; Gutenberg green on Windows end to end; Gutenberg on macOS red at the last step only**, the survivor check, which is #479's fix (see Risks). The two earlier runs (34948704875, 34949992162) ran both targets in one job and were cancelled on Windows at the 60-minute limit, which is why the jobs are split now. By hand, either platform, current head, in a folder you do not mind deleting: 1. Click **Create a site**. Expected: the dialog has three fields, and **Contribute to** is on **WordPress Core**, with a line under each choice. 2. Choose **Gutenberg**, name the site `gb`, pick a folder, click **Create site**. Expected: the folder created is `gb`, and the terminal shows the clone of `WordPress/gutenberg`. 3. Wait for the checklist to run install and build on its own. Expected: about a minute for the install and half a minute for the build on a fast machine; **This site is ready to work on**. 4. Click **Start dev server and finish the wizard**. Expected: the **Build watcher** tab shows `npm run dev` building the packages once and then `Watching for changes…`; the server URL appears; the site opens. 5. Log in (`admin` / `password`), open **Plugins**. Expected: Gutenberg is active with a **Deactivate** link; no plugin has **Delete**; there is no **Add New Plugin**. 6. Open **Posts > Add New**. Expected: the editor loads. 7. Click **Stop build watch**, then **Stop dev server**. Expected on macOS: `pgrep -f gutenberg-trunk` in a terminal (anywhere) prints nothing within a few seconds. 8. In the terminal panel, type `npm run grunt`. Expected: refused, with the Gutenberg list (`build`, `dev`, `test:unit`, `lint`, `lint:js`). 8a. Look at the sidebar and the header. Expected: a **Gutenberg** tag on the row and next to the status (a Core site wears **Core** in the same places, since `00181bf` (was `df4f989` before the rebase onto trunk at `576d218`): JuanMa's ask on 2026-09-16, a mixed list read as one tagged and the rest bare); the checklist steps read "Download Gutenberg", "Compile the Gutenberg packages…", "Launch a WordPress with this checkout as its Gutenberg plugin…"; no **Trac ticket** card, a **GitHub issue** card saying it comes later; the apply card reads **Check out a pull request** with no `.diff` link. 8b. With the Gutenberg site in front, open `wpct://ticket/62281` from a browser (or `open -a "<build>/dist/mac-arm64/WordPress Contributor Toolkit.app" "wpct://ticket/62281"` with every other copy quit). Expected: "Ticket #62281 cannot be linked to <site>", no **Link ticket** button, `git branch` in the checkout shows no `ticket/62281`; click **Hide**, switch to a Core site: the question is asked there. 9. On the Gutenberg site, open **Review & submit changes** and try **Open a pull request** (sign in if asked). Expected: refused with "Opening a pull request from this site is not supported yet — save the patch file instead."; nothing reaches GitHub. 10. Create a second site with the default choice. Expected: exactly what a site was before this stack: `grunt -- _watch` in the watcher tab, `grunt` accepted in the terminal, a pull request opens as before. **What must not have happened:** a Core site changing in any way (step 10); a Gutenberg site reaching GitHub's fork or pull-request endpoints (step 9); the served Gutenberg site offering **Delete** on its plugin (step 5); a watcher process outliving Stop (step 7); the checkout gaining files from the served site (`git status` in it is clean apart from `build*`, which Gutenberg ignores). ## Risks and limitations - Review outcome across six passes: 15 [fix here] fixed, 12 [follow-up] (8 taken, 4 deferred: `pgrep -f` misses argv-less helpers, the no-Delete assertion's strength, and two pre-existing Core sentences in the Review & submit modal's staleness banner and dirty-tree detail). - On a Gutenberg site that has never been built, the dev-server start runs `npm run build` and then `npm run dev`, which builds everything again before watching: about twenty seconds twice on a fast machine. And the watcher tab reads "watching" while `dev` is still in its first build. Both are the #470 review's follow-ups; deferred here because collapsing the two needs the watcher's own "Watching for changes" line as the ready signal, which is its own change. - `sites:add` (adopting a folder) still records every adopted checkout as Core. Adopting a Gutenberg checkout gives a site that builds with grunt; detection (`package.json` `name`) is a small follow-up. - **Stop does not end Gutenberg's watcher tree on a slow machine.** On the macOS runner, 3 of 3 runs left `tsc --build` (the native TypeScript binary Gutenberg's `npm run dev` runs) alive after Stop: it ignores the SIGTERM the app sends the process group while it is mid-build, and the app's escalation to SIGKILL after 3 s reaches only the direct child (`npm:kill` in `src/main.js`). Locally, where the first build had finished, it died 3 of 3 times. The fix belongs to `kill-tree.js` and `npm:kill` on trunk, so it is its own pull request (#479); this PR's real-setup lane is what found it, and stays red on macOS until that lands. - The process-tree check runs on POSIX only, and by argv (`pgrep -f <site path>`), so a helper whose arguments carry no path, such as esbuild's service, is not seen by it; on Windows the workflow proves the flow, not the kill (its `taskkill /T` path has injected unit coverage in `kill-tree.test.cjs`). - The Plugins-screen assertion that no **Delete** link exists only bites if the stock install ships an inactive plugin; a stronger probe (`plugin-install.php` refusing under `DISALLOW_FILE_MODS`) is a follow-up. - A first Gutenberg serve needs the network for the WordPress release Playground then caches. - The sidebar heading still reads "WordPress Core"; it is the app's title, so a follow-up rather than this PR. - The name-field mutation seen by the real-setup lane in a fresh app (above) is unexplained. If a contributor ever reports a site named with letters they did not type, this is where to look. - The sidebar row's accessible name is now `<label> Core` / `<label> Gutenberg`; the engine journeys' helper matches the label plus that word. Any other test that finds a row by exact name has to do the same. ## Related Part of #251. Stacked on #471. --- <details> <summary>Design decisions and alternatives considered</summary> - A radio, not a dropdown: two options, both worth reading, and the help line under it is where a first-time contributor learns the choice is permanent. - "Create a site" rather than "Create a contributor site": the sidebar button is short and the dialog says the rest. - The choice is exposed here, after serve, rather than in #469 with the field: a Gutenberg site the app could create but not run is a state the app must not produce. </details> <details> <summary>Review outcome (required — see AGENTS.md)</summary> Six passes, each by a fresh Explore subagent context (local `/self-review`), each on the head that moved since the previous one. Totals: **15 [fix here] fixed, 12 [follow-up] (8 taken, 4 deferred and recorded under Risks)**. - **Pass 1, `3c0c4ba`**: 4 fix / 4 follow-up. The choice's copy promised "where its pull requests go" while `github:open-pr` targeted wordpress-develop for every site (fixed: the handler refuses non-Core sites, copy narrowed); a docs claim about empty names (fixed); no PR-time coverage of the radio (fixed: `legacy-site.spec.js`); stale comment and unused `description` field (fixed: per-option descriptions). Follow-ups taken: mutation comment reworded, three docs pages and the clone string; deferred: `pgrep -f` misses argv-less helpers, the no-Delete assertion's strength. - **Pass 2, `28978e7`**: reconciled; (this pass was the CodeRabbit-style re-check of the first's fixes, no new findings beyond the ones carried). - **Pass 3, `ecab2bd`** (screen adaptations): 3 fix / 3 follow-up. Four card strings decided in `index.jsx` (fixed: registry `cards` block); the `wpct://ticket` prompt not gated by provider, which could put a Gutenberg site on a Trac ticket branch with no card naming it (fixed: `deepLinkNotice` takes the provider, `unsupported` state); PR preview URL hardcoded to wordpress-develop and the modal pointing at a card the page lacks (fixed). Follow-ups taken: mentor destination back outside the gate, a journey with a seeded Gutenberg site, two docs pages. - **Pass 4, `94a12cb`**: 2 fix / 3 follow-up. "Not now" on the refusal note cleared the ticket app-wide while the text promised to keep it (fixed: hidden per site); the submit sidebar still said "fork wordpress-develop" and counted "the other two" (fixed: upstream-aware, neutral counts). Follow-ups taken: journey asserts no ticket branch, unused `workItemNoun` removed, the last inline string moved to `cards`. - **Pass 5, `e4057cd`**: 2 fix / 3 follow-up. The hide reset keyed on the ticket number, so the same ticket twice did nothing (fixed: keyed on the arrival); the signed-out sign-in pitch survived the refusal (fixed: refusal first). Follow-ups taken: `prAfter` and the sign-in sentence into the registry; deferred: the reset re-firing on the null-and-back prop (taken in pass 6). - **Pass 6, `2f2ded7`**: 3 fix / 2 follow-up. No data-side guard that a non-Trac target's cards say nothing of Trac (fixed: registry test); a dead Gutenberg `signInCannot` (fixed: removed); the destination's `cost` line still promised the fork (fixed: `prCost` per type). Follow-ups: the reset keyed on the arrival stamp (taken); pre-existing Core wording in the staleness banner and the dirty-modal detail (deferred, not introduced here). - **Review:** completed — heads `3c0c4ba`, `28978e7`, `ecab2bd`, `94a12cb`, `e4057cd`, `2f2ded7` / base `fee96e5`; deterministic layer (lint, `npm test`, journeys) and the opt-in real setup run by the author on every head; outcome above - **Since review:** `2f2ded7 → 3023bd3` reviewed by a seventh fresh-context pass: all four of pass 6's findings resolved, no new findings; lint, bundle, `npm test` (1484) and 30/30 journeys green on this head </details> <details> <summary>Screenshots or recording</summary> `docs/public/screenshots/empty-state.png` and `create-site-modal.png` regenerated from the fixture tier (`npm run shots -- --only=…`). Before: "Create WordPress Core site" and two fields. After: "Create a site" and the **Contribute to** radio between the name and the location. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) <details> <summary>Stack correction review — 2026-09-17</summary> 0 remaining [fix here] · 0 new [follow-up] in this correction pass. No PR-local behavior changed; git range-diff reports every original commit unchanged (=) against its rebased base. - Current PR head: `08315156268f34ab94f6f0c81192f02642d48ce0`; configured base head: `244ca76c360e183a8da9f2343d7cf05fc05f5924`. Previous PR head: `ab2cfa708e325d58f8895113e1737bf20b76b3be`; only the inherited stack corrections and commit identities changed. - Reviewer: fresh Codex context `/root/review_fixes`. Completed five-dimension review of the correction delta from previously reviewed integrated head `bc8c4fda3fb78d51cd6f3abd5fc1b0f1e89c0095` to `c8c86677491012f9c9d4e99fe43b4ceff1c9647b`, then re-reviewed through final stack head `34550eec7a9031d2d5f03116801a60e313fa0559`. The additional runtime-deletion finding was fixed and verified; the original three findings remain resolved. Earlier review records above are historical. - Validation on the combined stack: lint; Node unit/integration/IPC suite (1,547 passed, two runtime skips); Electron suite (1,549 passed); 33 macOS journeys; documentation build. The real Playground restart/deletion regression passed on macOS. Windows CI results must be read on these new heads; this pass does not claim a fresh Windows real-setup run. - CodeRabbit's earlier check is not review coverage for these new heads. This local correction review is the review of record for the delta; no new bot review was requested. </details> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
juanmaguitar
added a commit
that referenced
this pull request
Sep 17, 2026
…quests that fix it (#251) (#480) ## Why A Gutenberg site could be built, run and used to try pull requests, but it had no work item: the card that on a Core site holds the Trac ticket said that working on a GitHub issue comes in a later version, the linked-PR search only knew `wordpress-develop`, and a pasted pull-request URL was checked against the wrong repository. This is the PR that gives a Gutenberg site its issue. #251 has the case. ## What changes Two stack-review corrections: Gutenberg's conflict/no-base guidance now preserves the issue branch and offers a backup plus mentor help, instead of instructing deletion followed by an unavailable patch import. Its mentor handoff header now uses the site's provider and links the actual GitHub issue (`# Issue: …`); Core retains its Trac header. **Reading an issue.** `src/renderer/github-issue.cjs` is the counterpart of `trac-ticket.cjs`: a bare number, `#71234` or a pasted issue URL (anchor, query, trailing slash included) resolve to an id; a pull-request URL is refused by name, because pasting the PR instead of the issue it fixes is the obvious mistake; an issue on another repository is refused, so a site cannot cite a number from a project it is not a checkout of; the URL branch gets the same bounds check as the typed one. `src/work-item.cjs` chooses between that parser and Trac's from the site's type, defaulting to Trac so a site made before project types existed keeps its behaviour with no migration. The registry holds what is true of one target (label, browse link, branch namespace); the provider holds what is true of one kind of work item. **Linking one.** `sites:set-ticket` parses through the site's provider. The stored key stays `tracTicket` for both kinds: renaming it touches every read on the card, the switcher and the patch handlers for no behaviour, and the decision is recorded above the handler. The branch is `issue/71234`, not `ticket/71234`: the noun the upstream uses, and a contributor reading `git branch` in their own client sees it. Core keeps `ticket/` untouched, so no existing site moves; every read in `ticket-branches.js` accepts both namespaces (a branch is read by its name alone), only the writers take the prefix, and a test walks the list against the registry and against the changes note's own grammar so a third namespace cannot be added to one side only. **The pull requests that fix it.** The linked-PR search goes to the site's own repository, and a row survives on the citation its work-item kind uses: on Gutenberg the forms GitHub itself reads as "for this issue" (`Fixes #71234`, `Closes WordPress/gutenberg#71234`, or the issue URL), so the list agrees with the issue page's own linked pull requests; a bare number in prose does not count, for the reason it never did on Trac. The cache is keyed per work item, and a work item is a number in a repository: Trac ticket 62281 and Gutenberg issue 62281 are different things. Core keeps the bare key it always had, so nothing cached is thrown away. The pasted-URL guard refuses a pull request from another repository by that repository's name. **Patch layout.** The path rewrite that steers a pre-`src/` Trac patch under `src/` was wordpress-develop's and ran on every patch. The registry's `patch.layout` now picks the mapper: `src-layout` is the rewrite as it was, `repo-relative` leaves every path as named, an unknown value throws rather than guessing. The layout rides from the site through preview, apply and the undo after a failed persist, so what is shown is what is written. On a Gutenberg site nothing reaches it yet (the picker and the attachments are hidden there); it is here so the layer is right before a caller arrives. **The card.** It shows on every site and takes its words from the provider and the registry: **GitHub issue**, *Working on issue #71234*, **Link issue**, **Open on GitHub**, the good-first-issues link. What is Trac's alone stays on Core sites: **Read details from Trac**, the attachments block, **Attach to Trac**, the patch-file picker. The pure copy modules take the site's noun rather than restating it (changes note, tickets card heading, trunk-moved notice and its refusals, next-action hint), the switch sentences name an `issue/` branch by its number, and the applied-PR box says the issue's work returns on revert rather than "returns to trunk". The placeholder card and the picker sentence that said issues come later are gone. **Docs:** `docs/guide/gutenberg-issues.md`, the counterpart of the Trac page, in the nav; the four pages that said issues come in a later version now point at it. **Left for PR 5 on purpose:** the **Open a pull request** card on a Gutenberg site still says it is not available and offers the patch file; `github:open-pr` still refuses a non-Core site; the "Link a Trac ticket to this site first" hint in that card is Core-only until then. `wpct://issue/N` is PR 6. ## How to test this Correction regressions, **either platform**, current head; from the repository root: `node --test tests/unit/ticket-trunk-notice.test.cjs tests/unit/ipc-wiring.test.cjs tests/unit/patch-provenance.test.cjs`. Both corrections were checked red before the fix and green afterwards. The recovery test covers conflict with/without named paths and unknown base; it forbids the unlink/delete/reapply sequence for Gutenberg. The real handoff export test reads the saved file, checks the correct issue URL and absence of Trac, and verifies the patch still parses; its Core counterpart preserves the old header. Platforms: **either**; nothing here spawns anything platform-specific beyond what #471 already covered. Build: the Buildkite artifact for the current head. The Gutenberg journey (`tests/e2e/journeys/gutenberg-site.spec.js`) walks the whole path on a Git fixture: the card asks for an issue, a pull-request URL is refused by name before anything is written, an issue URL links and lands on `issue/71234`, a `wordpress-develop` pull-request URL is refused, a `WordPress/gutenberg` one is checked out, the applied box says the issue's work returns, revert lands back on the issue's branch with its tree restored, and a `wpct://ticket` link is still turned away. `npm run test:e2e`: 30 passed on macOS at the current head. By hand, either platform, on a Gutenberg site (needs the network for GitHub): 1. In the **GitHub issue** card, paste `https://github.com/WordPress/gutenberg/pull/4496` and click **Link issue**. Expected: "That is a pull request. Link the issue it fixes instead."; `git branch` in the checkout is unchanged. 2. Paste `https://github.com/WordPress/wordpress-develop/issues/1` and link. Expected: "Only WordPress/gutenberg issues can be linked here." 3. Type a real open issue's number and link. Expected: *Working on issue #N*, **Open on GitHub** opens it, `git branch` shows `issue/N` and no `ticket/N`; no **Read details from Trac**, no **Trac attachments**. 4. Under **Linked pull requests**, expected: the pull requests the issue page lists under *linked pull requests*, and nothing that merely mentions the number. Pick one with **Apply…**: the preview lists its files, **Apply and rebuild** lands on `pr/NNNN`, the green box reads *PR #NNNN is applied* with *Your issue changes are saved separately and return when you revert this PR.*; **Revert this PR** returns to `issue/N`. 5. In **Check out a pull request**, paste a `wordpress-develop` pull-request URL. Expected: "Only WordPress/gutenberg pull requests can be applied here." 6. **Unlink**, then link the same issue again. Expected: *Your issues on this site* lists it in between; linking again switches back with no new branch. 7. On a Core site, link a Trac ticket, apply a Trac attachment, check out a `wordpress-develop` pull request. Expected: exactly what it was; the linked-PR cache key for a Core ticket is unchanged (`ticketPatches:<id>` in the store). **What must not have happened:** a Core site changing in any way (step 7); a `ticket/` branch appearing on a Gutenberg site (step 3); a `wordpress-develop` pull request cited by a Trac ticket showing under a Gutenberg issue of the same number (step 4, if such a number exists). ## Risks and limitations Gutenberg still has no patch importer or automatic conflict resolver. Recovery deliberately retains the branch and directs the contributor to save a backup and ask a mentor; simply adding import would label the restored work as an applied external patch and block submission as their own work. - Review outcome (self-review against `.github/instructions/code-review.instructions.md`, fresh agent context, head `98650dd` on base `juanmaguitar/251-contribute-to`, completed over the full diff; lint and unit suite clean): 5 [fix here], all fixed in `f96db22` (the noun threaded into the trunk-moved refusals, the action gate, the dirty-trunk question and the patch-review heading; the field's accessible name and the attach URL read from the provider; callerless `defaultPrTitle` and the diff fetch's repo option removed; `bodyCitesIssue` narrowed to GitHub's own rule). 1 [follow-up]: `parseIssueRef`'s default repository duplicates the registry's Gutenberg entry; every real caller threads the repository, so it only matters for a second GitHub-issue target. Two style notes: `showTracCards` now means three things at four call sites (the "no PR flow yet" one gets its own flag in PR 5). CodeRabbit: one review on `bdaf6a5`, 1 low finding (the undo test never sent the undo), fixed in `b354059`. - Found by hand on macOS after opening (2026-09-16), fixed in `6ee65ec`: linking an issue by hand armed #292's auto-read, which opened the Trac window on the Core ticket with the same number and painted its summary, status, milestone and attachments note under the issue. The auto-read and the scraped facts are Trac's alone in the renderer now, and `trac:list-attachments` refuses a non-Trac site before the window can open. Reproduced first by a journey assertion (a stubbed Trac records zero scrapes and none of its facts show) and an ipc-wiring test. - `git:fetch-pr-diff` stays Core-only and says so: nothing has invoked it since pull requests became checkouts (#458). Removing the handler and its preload entry is a follow-up (the packaged smoke test pins the API surface). - The renderer's changes note holds its own copy of the two branch namespaces as a regex: `ticket-branches.js` owns the list but reaches for Git and cannot be bundled. A test walks the list against the regex so they cannot drift apart unnoticed. - `bodyCitesIssue` matches GitHub's own linking rule: a closing keyword, followed by `#N`, `owner/repo#N` or the issue URL. A pull request that says "Related to #N" or drops the URL in prose is not listed, on GitHub or here; a contributor looking for it can paste its URL. - The "Link a Trac ticket to this site first" message in the pull-request card, and the card itself on a Gutenberg site, are PR 5's. - `sites:add` (adopting a folder) still records every adopted checkout as Core; carried over from #476. ## Related Part of #251. Stacked on #476 (rebased with it onto trunk `576d218` on 2026-09-16; the tickets card #472 moved keeps its place and shows on every site). 🤖 Generated with [Claude Code](https://claude.com/claude-code) <details> <summary>Stack correction review — 2026-09-17</summary> 0 remaining [fix here] · 0 new [follow-up] in this correction pass. Both recovery and mentor-attribution findings are resolved. - Current PR head: `7b7db146c92022fd5960d6e677f05406bc97831b`; configured base head: `08315156268f34ab94f6f0c81192f02642d48ce0`. Previous PR head: `4915881a062f65fe8e3e6fe5fac8d6e34e806066`. - Reviewer: fresh Codex context `/root/review_fixes`. Completed five-dimension review of the correction delta from previously reviewed integrated head `bc8c4fda3fb78d51cd6f3abd5fc1b0f1e89c0095` to `c8c86677491012f9c9d4e99fe43b4ceff1c9647b`, then re-reviewed through final stack head `34550eec7a9031d2d5f03116801a60e313fa0559`. The additional runtime-deletion finding was fixed and verified; the original three findings remain resolved. Earlier review records above are historical. - Validation on the combined stack: lint; Node unit/integration/IPC suite (1,547 passed, two runtime skips); Electron suite (1,549 passed); 33 macOS journeys; documentation build. The real Playground restart/deletion regression passed on macOS. Windows CI results must be read on these new heads; this pass does not claim a fresh Windows real-setup run. - CodeRabbit's earlier check is not review coverage for these new heads. This local correction review is the review of record for the delta; no new bot review was requested. </details> <details> <summary>Windows journey synchronization fix — 2026-09-17</summary> The PR checkout journey now waits for the pull-request input to reappear and become enabled after restoration, before inspecting Git, files and persisted settings. All previous assertions remain; no production code changed. Current head: `6b9598c44f171b4899abd945853f816556377519`; configured base: `08315156268f34ab94f6f0c81192f02642d48ce0`. Earlier review records are historical. Fresh reviewer `/root/review_ci_sync` completed the five-dimension review of the four-line correction against `7b7db146c92022fd5960d6e677f05406bc97831b`, published as `6b9598c44f171b4899abd945853f816556377519`: 0 [fix here], 0 [follow-up]. The rebased integrated tree differs from the previously reviewed stack only by those same four test lines. Reproduction: temporarily delay the real leave-PR handler after checkout and before metadata persistence; the original journey fails with expected ticket/60001, received pr/7. With the new wait, the same delayed run passes. The diagnostic delay was removed before committing. On the corrected #480 head, lint and build pass, Node tests report 1,539 passed and 2 runtime skips, and all 31 macOS journeys pass. From the repository root on macOS or Windows at the current head, run `npm run build:once && npm run test:e2e`. New-head macOS and Windows journeys and packaged smoke pass, as do lint, docs and macOS unit tests. The Windows Electron unit suite is still running. No new CodeRabbit review was requested; its check says automatic reviews are disabled. The integrated #486 stack also passes all 33 journeys locally on macOS. </details> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
juanmaguitar
added a commit
that referenced
this pull request
Sep 17, 2026
…487) ## Why The published guide at https://wordpress.github.io/contributor-toolkit/guide/getting-started.html is built from trunk on every push that touches `docs/`. Merging the Gutenberg stack therefore put five unreleased features in front of users: the latest release is v1.1.0, and nothing anyone can download has "Working on a GitHub issue", the Gutenberg site type, or the Gutenberg PR panel. This is not a docs mistake. Docs shipping in the same PR as the change they describe is the rule here, and the consequence of that rule is that trunk's guide is always ahead of the newest build. What was wrong is publishing trunk. ## What changes The site now tracks the latest **release**. Deploy runs only on `workflow_dispatch`, with a required `ref` input naming what to publish, so cutting a release advances the guide with one command: ```bash gh workflow run docs.yml --ref trunk -f ref=v1.2.0 ``` Two decisions a reviewer would otherwise have to reverse-engineer: **Why the ref is an input rather than the dispatch target.** Dispatching on tag `v1.1.0` would run the copy of `docs.yml` that exists *at that tag*, whose deploy job is gated on `github.ref == 'refs/heads/trunk'`. It would build and silently skip the deploy. So the dispatch happens on trunk and the ref to publish travels as an input. **Why the deploy guard still tests `github.ref`.** It means something narrower now, and the comment says so: it decides which copy of this workflow may hold the Pages OIDC token (the reviewed, merged one), not what gets published. `inputs.ref` decides that, and publishing an arbitrary ref is the point. The `github-pages` environment already has a deployment-branch policy allowing only `trunk`, so this is belt and braces. Pushes to trunk and pull requests still build, they just never deploy: a dead link fails when it is proposed and again when it lands, rather than at release time. Not in this PR: any change to `docs/` content. Trunk's guide stays as it is and goes live at v1.2.0. ## How to test this Platform: any. This is CI-only, nothing in the app changes. **Starting state:** this branch, and the repo's Actions tab. 1. Open this PR's **Docs** check. Expected: the `build site` job runs and passes; **no** `deploy to Pages` job appears. The old behaviour was the same for PRs, so this confirms nothing regressed. 2. In the run's summary page, look at the **Record what was built** step's output. Expected: a "Built `<sha>`" line naming the commit, and no "Not published" warning (that warning is for dispatches only). 3. After merge, go to Actions → Docs → **Run workflow**. Expected: a **"Git ref to publish"** box, required, with no prefilled value. 4. Run it with branch `trunk` and ref `v1.1.0`. Expected: build passes, `deploy to Pages` runs, the run summary names `v1.1.0` and commit `576d218`, and the live guide loses the Gutenberg pages: no "Working on a GitHub issue" in the sidebar, and `/guide/gutenberg-issues.html` 404s. 5. The sharp-edge path: run it with branch `v1.1.0` (the tag) instead of trunk. Expected: it builds, the deploy is skipped, and the run summary carries a "⚠️ Not published" line telling you to re-run with `--ref trunk -f ref=<tag>`. Without that line this would be an all-green run that published nothing. **What must not have happened:** - A deploy triggered by a plain push to trunk. The whole point is that merging docs no longer publishes them. - The site published from trunk while the input said `v1.1.0` — check the deployed page, not just the green check. - A release publish cancelled mid-flight by an unrelated docs merge. Workflow-level concurrency cancels jobs too, so dispatches get their own concurrency group; the job-level `docs-deploy` group does not cover this. ## Risks and limitations 3 [fix here] · 2 [follow-up] from the review pass, all 5 addressed. Detail in the collapsed block. - The guide is now stale by design between releases. Anyone reading the live site while trunk is ahead sees the last release, which is the intent, but it does mean a docs fix for a *released* bug also waits for the next release unless someone dispatches deliberately (which they can, at any time, with any tag). - Whoever dispatches decides what goes live. `inputs.ref` can name an unmerged branch or a PR merge ref, so "the site is reviewed code" is now a habit rather than a branch rule. The build job holds nothing worth stealing (`contents: read`, `persist-credentials: false`, and the deploy job checks nothing out), so this is not a token-escalation path. Called out in the file's own comment. - Steps 3-5 above cannot be exercised before merge: `workflow_dispatch` only offers the inputs declared in the copy of the file on the default branch. What *was* verified locally is that the v1.1.0 docs still build clean under today's toolchain (`npm ci --prefix docs` + `vitepress build` at tag `v1.1.0`, no dead links, 19 guide pages, no `gutenberg-issues.html`), so the restore dispatch is not going to fail on content. ## Related Follow-up to the Gutenberg docs stack: #486, #481, #480, #476, #471. --- <details> <summary>Design decisions and alternatives considered</summary> **Reverting `docs/` on trunk to its v1.1.0 state.** Rejected: it fights the repo rule that docs ship with the change that makes them true, and every reverted page would have to be un-reverted at v1.2.0, with the Gutenberg screenshots going in and out of the tree twice. **A long-lived `docs-live` branch that auto-deploys, fast-forwarded at each release.** Rejected as one more branch to maintain for the same outcome a dispatch already gives. **A default value for the `ref` input.** Rejected: a hardcoded default names one release tag and is wrong from the next release onwards. Resolving "latest release tag" in a step was the alternative, and it adds a `gh` call and a failure mode to a job that runs twice a year. Typing the tag also makes the run record say what went live. </details> <details> <summary>Review outcome (required — see AGENTS.md)</summary> 3 [fix here] · 2 [follow-up] — all 5 addressed in `628820c`. - 🟡 security, `docs.yml` summary step: the ref was interpolated into `run:` through `${{ }}`, which substitutes into the script's source before bash parses it, so a ref name carrying `$( )` or a quote would have been code. Now passed through `env:` and read as `"$REF"`, the way `download-stats.yml` passes its values, with `set -euo pipefail` added. Verified by running the step locally with `REF='v1.1.0$(echo PWNED)"'`: printed literally. - 🟡 architecture, concurrency: `docs-build-${{ github.ref }}` put a release dispatch and a routine trunk push in the same group, and workflow-level concurrency cancels the whole run, jobs included, so a docs merge could cancel a publish mid-flight. Dispatches are now keyed on `github.run_id`. - 🔵 architecture, checkout: `ref: ${{ inputs.ref || github.ref }}` was not the no-op it claimed for push events. Checkout's own default is `github.sha`; `github.ref` would build whatever the branch tip is when the step runs. Changed to `github.sha`. - 🔵 [follow-up] security: the header paragraph this diff removed was the one saying the deploy never runs on PR code. That property genuinely changes, so it is now stated on the deploy guard rather than dropped. - 🔵 [follow-up] architecture: a dispatch on a non-trunk ref was an all-green run that published nothing. The summary step now writes a "Not published" warning naming the correct invocation. The pass also confirmed, case by case, that the new guard holds: `pull_request` (fork included) fails the event test; a fork cannot dispatch at all; a branch or tag dispatch fails the ref test. - **Review:** completed — `/self-review` per AGENTS.md, judgement pass in a fresh subagent context given only the diff and `code-review.instructions.md`; reviewed head = working tree over base `c91908c` (uncommitted at review time; the reviewed content is now committed as `628820c` with the five findings applied on top). Deterministic layer: `npm run lint` clean, `npm test` 1547 pass / 0 fail / 2 skipped. `actionlint` is not installed here and Docker was not running, so the workflow was checked by YAML parse and by executing the new shell step locally against both event shapes. - **Since review:** `c91908c` → `628820c` — the five findings, plus one CONTRIBUTING.md wording fix. No re-review; the changes are the review's own remedies. </details> <details> <summary>Implementation notes</summary> `inputs` is a recognised context here because the workflow declares `on.workflow_dispatch.inputs`; on `push` and `pull_request` it evaluates to empty rather than failing expression evaluation, so `inputs.ref || github.sha` falls through cleanly. `CONTRIBUTING.md` changes in the same PR because its "The documentation site" section stated the old behaviour in prose, and this change makes that sentence false. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A Core site's
build/is a whole WordPress, and the Playground runner mounts it as the docroot before install and skips the download. A Gutenberg checkout is not a WordPress at all; it is a plugin, and there is nothing to mount as a docroot. #251 needs the served site to be a stock WordPress with the checkout mounted and activated as its Gutenberg plugin.What changes
The stock WordPress installation now belongs to the site's lifecycle: stopping and restarting preserves posts, settings and uploads; deleting the registered Gutenberg site stops its processes and removes the persistent installation before removing the checkout. The runner and cleanup canonicalize the same directory, including Windows casing and macOS path aliases. Failed cleanup keeps the checkout and registry entry retryable.
src/playground-plan.cjs(ported from the closed #261) turns a serve strategy into therunCLIoptions:docrootis what Core always sent (mount before install as/wordpress,install-from-existing-files-if-needed),plugin-mountuses Playground's persistentstartcommand to retain its WordPress installation across restarts, mounts the checkout atwp-content/plugins/gutenbergand adds anactivatePluginstep. It also names the constants a strategy needs on top of the shared debug and SMTP set: the plugin mount is the contributor's working tree,.gitand uncommitted work included, soplugin-mountsetsDISALLOW_FILE_MODSandDISALLOW_FILE_EDIT; without them Plugins > Delete in the served site removes the checkout. The cost is deliberate: the served site cannot install a second plugin or theme, and has no file editor. Losing an afternoon of work is worse than restarting a preview.The runner takes one JSON argument instead of a bare build directory.
playground:startreads the site's type, writes the config, and runs the runner from the checkout for a plugin mount, frombuild/as before for a docroot. The mapplayground:stopand the quit sweep walk is keyed as it was.Deliberately not in this PR: anything a contributor can see. Nothing can create a Gutenberg site yet; the choice arrives in the next PR, now that a Gutenberg site can be cloned, built, watched and served.
How to test this
Persistence regression, macOS verified; Windows supported by the same opt-in lane, current head. From the repository root, run
TOOLKIT_REAL_SETUP=1 npx --no-install playwright test --config tests/e2e/real-setup.config.js -g 'keeps posts'(PowerShell: set$env:TOOLKIT_REAL_SETUP = '1'first, then run the command without the POSIX assignment). This boots real WordPress, saves a post and upload, restarts and checks both, deletes the installation and checks that the same location starts fresh. It fails with the previousservercommand (empty title, missing upload) and passes with the correction. The IPC suite additionally covers registered/Core/unregistered deletion and a cleanup failure. The fixture and its persistent installation are removed after the test.Platforms: any for the Core path; macOS for the Gutenberg pass below (run by the author, 15 Sep, on
3c7d13c; the fixes since do not touch the plugin-mount path).Core, no visible change, from the repository root:
The runner test pins the Core options byte for byte against what the runner sent before (
mount-before-installof the build directory, the install mode, no plugin mount, no steps, no file-mods constants), and the plugin mount separately.Gutenberg, by hand, since the app cannot make such a site yet: with a built
WordPress/gutenbergcheckout on disk, runsrc/server-runner.json Electron's Node with the argument{"strategy":"plugin-mount","pluginDir":"<checkout>","pluginSlug":"gutenberg"}and the checkout as the working directory, then open theSERVER_URL:it prints.wp-content/plugins/gutenberg/assets.admin/password) and open Plugins. Expected: Gutenberg is listed with a Deactivate link; no plugin has a Delete link; there is no Add New Plugin.wp-content/plugins/gutenberg/build/.Observed on the author's run: up in 5 to 14 s, all three as expected.
What must not have happened: a Core site's Playground options changing in any way (the runner test is the guard); the served Gutenberg site offering a Delete link on its own plugin; anything written into the checkout by the served site.
Risks and limitations
The restart/deletion regression passed locally on macOS; its Windows network run has not been executed in this correction pass. Cleanup follows the installed Playground CLI's
~/.wordpress-playground/sites/<sha256(canonical cwd)>convention, pinned by the real CLI test. A checkout removed externally cannot be resolved safely, so deleting its stale registry entry does not guess which persistent directory to remove. Previously discarded temporary content cannot be recovered.JSON.stringifyand no shell in the spawn; the next PR, which exposes the choice, carries the Windows pass through its Buildkite artifact./wordpress/wp-content/database/.ht.sqlite) is Playground's stock install's underplugin-mount, the same as underdocroot; verified by the front page and admin loading, not by opening Adminer.DISALLOW_FILE_MODSalso blocks plugin and theme installs and core updates inside the served site. Deliberate, and documented in the next PR alongside the choice.Related
Part of #251. Stacked on #470.
Design decisions and alternatives considered
Review outcome (required — see AGENTS.md)
1 [fix here] · 4 [follow-up] — the [fix here] and three of the follow-ups fixed in
fee96e5.tests/unit/runner-wiring.test.cjs: the new mount assertions comparedhostPathto POSIX literals while the runner resolves them, which on Windows gives a drive-rooted path; four red CI jobs. Fixed: expected values resolved the same way.src/playground-plan.cjs:pluginSlugwent into the VFS path unvalidated; only the registry supplies it today, but the module is the seam. Fixed: a plain-name guard, with a test.src/main.jsplayground:start: two starts for one site can both pass the running-server guard, since awaits precede it (pre-existing, the SMTP await was already there; this PR adds a store read). Deferred as its own fix: the cheap change is a placeholder in the map at the guard.isPluginMountonce instead of three strategy comparisons; the runner harness comment no longer claims the Playground packages are not installed.Verified clean by the reviewer: the serve config is built from app-owned values and passed as argv with no shell (Windows backslashes survive
JSON.stringify/JSON.parse,path.resolveis a no-op on an absolute path);playground-plan.cjshas no requires or side effects so the loopback patch still precedes the CLI load; theDISALLOW_FILE_*trade-off is recorded and no remaining WordPress write path reaches an active plugin's directory with both set; running the runner from the checkout writes nothing into it; a plan failure reaches the renderer through the existing log and exit path; the Gutenberg wiring test fails if the handler stops reading the type./self-review); reviewed head3c7d13c/ base34d4005; deterministic layer (lint,npm test) run by the author; Playground option handling verified by the reviewer against the installed@wp-playground/clibundle; outcome above3c7d13c → fee96e5checked: only the fixes above; lint andnpm testre-run green (1480 pass, 2 pre-existing skips)Screenshots or recording
Nothing on screen changes in the app. The served Gutenberg site's Plugins page, from the manual pass above: Gutenberg active, no Delete links.
🤖 Generated with Claude Code
Stack correction review — 2026-09-17
0 remaining [fix here] · 0 new [follow-up] in this correction pass. Persistence and deletion lifecycle fixed; the original data-loss finding and the additional deletion/recreation finding are resolved.
244ca76c360e183a8da9f2343d7cf05fc05f5924; configured base head:d3bdaff0b805b55ef8c9a8f0c4093ef68064ab24. Previous PR head:1283b18bf0946eca9b5d1810b299eca1177baa69./root/review_fixes. Completed five-dimension review of the correction delta from previously reviewed integrated headbc8c4fda3fb78d51cd6f3abd5fc1b0f1e89c0095toc8c86677491012f9c9d4e99fe43b4ceff1c9647b, then re-reviewed through final stack head34550eec7a9031d2d5f03116801a60e313fa0559. The additional runtime-deletion finding was fixed and verified; the original three findings remain resolved. Earlier review records above are historical.