Repository navigation
Move a site's trunk update out of SiteRow into a hook - #585
Conversation
The fifth domain of #554. useTrunkUpdate holds the update's chain: the fetch and reset, the install when the lockfile moved, the rebuild, the hand-off to a resumed watch where the watcher rebuilds everything, the question it asks first about edits in the tree, and the retry. It is a chain and not a process of its own, so what it runs through comes in as arguments: the script runner, the terminal's lock and writer, the build watch's pause and resume, and the functions that re-read the site once the tree has changed. Its functions are plain closures, as they were. The trunk's date and the incomplete marker stay in SiteRow with the rest of what the site's status says. The hook is called above the ticket card's gate, which reads the update's stage. Pausing the watch and bringing it back had no journey with a watch running; #582 left that for here. Two journeys do it now, on Core where the update builds, and where the resumed watch does the update's build. Both were written against the inline code and pass on both. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From the review of the previous commit. "Stops the watcher before it touches the tree" checked only the stop. The stand-in now keeps what the file about to be replaced held when the watcher was stopped, and the journey reads it; with the watcher stopped only after the reset, it goes red on that line. "The one thing that can be pressed" checked two controls, and says so. A check that the incomplete marker was gone from the screen could not fail where it stood, since nothing had re-read the site yet; the store is the check there. The watcher's first line is said only once the main process has answered that it started, or it would be said to nobody. The script the watcher is on Gutenberg is a characterisation. build-watch.spec.js said no journey pauses the watch; the update's now does. And the journey that saves edits before an update reads that the notice names the file they went to, which nothing had checked. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
From the review of the previous commit. A comment only: checking out a pull request and leaving one pause the watch too, and their journey runs with no watch running, like the others the header lists. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: WordPress/contributor-toolkit/.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 |
) ## Why `SiteRow` holds the state of eight separate things in one function, and #554 splits it into hooks by domain so the redesign in #542 can rebuild the screen around pieces it can hold. Six have left. These are the last two on the issue's list, and they come together: the ticket a site is working on, and the chain that applies someone else's patch or pull request. ## What changes Two hooks, in one PR because each calls the other. - **`src/renderer/hooks/use-site-ticket.jsx` is the ticket.** Linking one, leaving it, switching between the ones with work on the site, moving one onto the current trunk and deleting one's work, with the question a switch asks when trunk has loose edits. - **`src/renderer/hooks/use-apply-patch.jsx` is the apply chain.** What a ticket offers to apply (its linked pull requests and its Trac attachments), the preview, and the chain that applies a patch, takes it off, checks a pull request out or leaves one, and rebuilds the site around the result. - **They reach each other through three refs, as the inline code did.** A ticket switch can put a parked pull request back, which ends in the apply chain's install and build; and a checkout refused for trunk's loose edits is tried again from the ticket's panel. The ticket hook is called first, because the apply chain needs the terminal and the build watch, which are set up further down. So the ticket hook makes the refs and the apply hook fills them. - **What the site's status says stays in `SiteRow`**: which ticket is linked, which patch is applied, which pull request is checked out. The status loader writes them, and both hooks read them. - **One function in `SiteRow` is now memoised**, `applyDiscardToNote`. It touches a ref and a setter and nothing else. Inside a hook it has to be listed as a dependency, and unmemoised it would have given two of the ticket's callbacks a new identity on every render. **The journeys #585 left for here are in a new `watch-during-changes.spec.js`**: a ticket switch, a patch and a pull request's checkout with the build watch running. They were written against the inline code first and pass on both. **One assertion from #585 is corrected in `trunk-update-watch.spec.js`.** "The update is not complete while the watch is still rebuilding" waited for the announcement to be absent, and the announcement goes away by itself, so it could not fail. It now counts the request that records a completed update, which the page sends in the same turn as it hears the watcher's line. ## How to test this Platforms: any. Use the Buildkite build for the current head of this branch. The journeys carry this. From the repository root, `npm run test:e2e` runs them all; the ones that walk these two hooks are `ticket-branches`, `ticket-rebase`, `patch-apply`, `pr-checkout`, `store-persistence`, `merge-in-progress`, `gutenberg-site`, `review-changes` and the new `watch-during-changes`. - The existing ones are unchanged: linking, unlinking, switching and deleting tickets; moving a ticket onto trunk; applying and reverting a patch file, with its refusals; checking a pull request out and leaving it, with ticket work and edits kept on their own branches; and all of it surviving a restart. - The first new journey, on Core with the watch running, asserts that linking a ticket and applying or reverting a patch of source files stop nothing and build nothing, with the watch's tab saying it is compiling; that a ticket linked by hand has its Trac page read once without anything else being pressed; and that a pull request's checkout stops the watcher, lands, runs the build itself, announces nothing until that build ends, and brings the watch back. - The second, on a built Gutenberg checkout, asserts that a pull request's checkout stops the watcher, runs no build, brings the watcher back, and announces the checkout only when the watch prints its ready line. What they cannot reach is the network and a real build: **Starting state:** a Core site that has finished setup, with no ticket linked. 1. In the ticket card, type an open Trac ticket's number that has a pull request, and click "Link ticket". The ticket's title and its linked pull requests appear without anything else being clicked. 2. Click "Apply…" on a pull request, then "Apply and rebuild". The site's terminal shows the checkout, then the build; when it ends, a message reads "Checked out the pull request". 3. Click "Revert this PR". The terminal shows the way back and a rebuild; the ticket card is as it was after step 1. 4. Edit a file, then click "Unlink". The panel asks nothing: the edit stays on the ticket's branch. The ticket is listed under the tickets with work on this site, and "Continue working on #" and its number brings the edit back. **What must not have happened:** - The edit made in step 4 is not lost by unlinking and continuing. - `node_modules` was not reinstalled in steps 2 or 3 unless the pull request changes the lockfile: the step list names the install as skipped. ## Risks and limitations - **This is the largest move of the series**: about 790 lines leave `index.jsx` for two hooks, which is over the size where this repository asks whether to split. It was kept whole because the two halves call each other, and a split would have meant an interface between them that the next PR deleted. The check that it is only a move is mechanical and is in the review outcome below: every line removed from `index.jsx` is in one of the two hooks, apart from imports and four dependency lists. - **The apply hook takes thirty-one arguments.** It is the chain that touches everything: the runner, the terminal, the watch, the status, and the ticket hook's refs and setters. - **Four dependency lists changed**, all by gaining values that were already stable: two state setters and a ref that are now arguments in the ticket hook, the memoised `applyDiscardToNote`, and a ref in the apply hook's effect. - **Effects run in a different order relative to the setup chain's.** The effects that read a ticket's pull requests and fill the refs used to be declared after the setup chain's and are now run before them, where the apply hook is called. Neither reads the other's state. - **Not covered by a journey with the watch running:** leaving a pull request, a ticket switch that puts a parked pull request back, a patch that changes the lockfile, a switch while the watch is still building, and a hand-off to the watch that a later apply or a stop by hand cuts short. All of it is moved code that behaved this way before. - **No journey reads the unsubmitted-changes note after a switch**, with or without a watch. The ticket hook re-measures it in four places. - **One journey failed once in three full runs**, and not in a way that points at this change: in the second run the app's window closed under `pr-checkout.spec.js:177` three seconds in ("Target page, context or browser has been closed"). That file then passed five times in a row and the next full run was 55 of 55. Windows opened by the journeys take keyboard focus on the machine they run on, and it was in use. - **The manual steps above were not run.** - **Checked on macOS only.** Windows is covered by CI. - **Pre-PR review: 4 findings fixed, 2 left as follow-ups, none in the moved code.** All were in journeys or comments. Details are in the review outcome below. ## Related Part of #554: the ticket and the apply chain, the last two domains on its list, after the trunk update in #585. Part of #542. --- <details> <summary>Design decisions and alternatives considered</summary> **Two hooks, not one.** A ticket is linked, switched and deleted without anything being applied, and the panel that does it is a different card. Together they would be one hook of nine hundred lines with two jobs. **What a ticket offers to apply is in the apply hook.** The linked pull requests and the Trac attachments are fetched for the ticket, but they are read by the panel that previews and applies them, reading them clears the apply's error, and a failed apply counts them. In the ticket hook they would need the apply hook's functions, and the apply hook would need them back. **The refs between the two hooks were kept as refs.** `ticketSwitchLifecycleRef` and `retryPrSwitchRef` exist because the functions they hold are declared after the code that calls them, and are re-created on every render so that they see current state. That is still true with the caller in one hook and the functions in another. A cleaner interface would be the apply hook passed to the ticket hook as an argument, which the order of calls does not allow. **The linked ticket, the applied patch and the checked-out pull request stay in `SiteRow`.** `loadStatus` sets them with the rest of what the main process says about the site. They are the next thing to move, as a hook for the site's status, and that is outside #554's list. </details> <details> <summary>Review outcome (required — see AGENTS.md)</summary> Run as the repository's review procedure, with the judgement pass in a fresh context that took no part in the change. First pass on `82d7071` against `600b277`; second pass on the fix-up `4acbe42`; the head is `005a80e`, which adds the one comment the second pass asked for. **First pass: 4 to fix here, 3 follow-ups (one finding counted in both). Nothing in architecture, security, performance or cross-platform; the move was found faithful.** It repeated the line-by-line comparison, checked that each function body is intact and in its old order, that every returned name is read only after its hook is called and every argument is declared before it, which effects changed order relative to `SiteRow`'s and that none reads what the other writes, that the three refs are filled before anything can call them, and that late callbacks read the same render's values as before. Fixed: - **The absence read in `trunk-update-watch.spec.js` was not sound**, and the comment added with it said it was. The update announces only after the main process answers the request that records it, so one look at the screen could come back before a wrong announcement. The journey now counts that request, on the app's real handler, and keeps the look at the screen for an announcement made without completing. - **Nothing asserted that a ticket linked by hand has its page read (#292)**, which is what one of the three refs between the hooks is for. The new journey counts the read and expects exactly one. - **A wait that could be late on a slow runner.** The watch's tab says "compiling" for three seconds and was looked at after the announcement; it is now looked at first. - **Stale words**: two journey headers that this change made untrue, and four comments in the hooks that pointed at code now in the other hook. Left as follow-ups, both in Risks above: the flows with the watch running that have no journey, and the unsubmitted-changes note. **Second pass: 1 to fix here, 1 follow-up.** The count of completion requests is how the app records an update today and not a promise, so it is now marked as a characterisation. The follow-up: inner waits of 60 seconds in `pr-checkout.spec.js` and `trunk-update-watch.spec.js` cannot be reached inside the 60-second budget of a test; the ones in the new file are 30. Noted and left: the stand-in for the runner in `watch-during-changes.spec.js` is a near copy of the one in `trunk-update-watch.spec.js`. **Checks, on macOS, on the head of this branch:** | Check | Result | | --- | --- | | `npx eslint . --max-warnings=0` | clean | | `npm test` | 1697 pass, 2 skipped | | `npm run test:electron` | 1699 pass | | `npm run test:e2e` (journeys) | 55 of 55 on the head; one failure in an earlier run, described in Risks | | `npm run test:e2e:packaged` | 15 of 15, on `4acbe42`; the head differs from it by a comment in a journey | | `watch-during-changes.spec.js` as first written, against trunk's inline `index.jsx` | pass | **That it is a move.** Comparing the lines removed from `index.jsx` with the lines of the two hooks, every removed line is in a hook except the import lines, four dependency lists, the first line of `applyDiscardToNote` and one heading comment. The comments corrected after the review are the only lines of the hooks that differ from what was removed. No name a hook returns is read in `SiteRow` above the hook's call. **The assertions were audited by breaking what they claim**, one break at a time in the moved code, and running the journeys that should notice. 26 breaks: 23 turn a journey red on the line that states the claim, 3 do not. | Break | Caught by | | --- | --- | | The apply chain does not tell a switch what to call | `ticket-branches` | | A checkout refused for loose edits cannot be tried again | `pr-checkout` | | A checkout refused for loose edits does not ask about them | `pr-checkout` | | A link made by hand does not ask for the ticket's details | `pr-checkout` | | A switch is not refused while a command runs | `pr-checkout` | | The apply chain is not told which pull request is checked out | `pr-checkout` | | The apply chain is not told which ticket is linked | `pr-checkout` | | The apply chain is not told this site is in front | `pr-checkout` | | A switch does not re-read the branch list | `ticket-branches` | | The branch list is not read when the site opens | `store-persistence` | | A ticket switch is not handed to the watch | `watch-during-changes` | | A patch of source files is not handed to the watch | `watch-during-changes` | | A patch of source files is built all the same | `watch-during-changes` | | A pull request's checkout leaves the watch running | `watch-during-changes` | | The watch is not brought back when the apply ends | `watch-during-changes` | | The checkout is announced before its build ends | `watch-during-changes` | | The checkout builds even where the watch will | `watch-during-changes` | | The checkout is announced before the watch is ready | `watch-during-changes` | | The watch being ready is not announced | `watch-during-changes` | | The update is announced, without completing, before the watch is ready | `trunk-update-watch` | | The update completes before the watch is ready | `trunk-update-watch` | | The apply chain is handed another ref for the ticket just linked | `watch-during-changes` | | A ticket's page is read twice on a link | `watch-during-changes` | | A switch does not re-read the site | **nothing**: the end of the switch re-reads it anyway | | The unsubmitted-changes note is not measured again after a switch | **nothing**: no journey reads that note | | An applied patch is not recorded on the screen | **nothing**: the build that follows re-reads the site | The three that nothing catches were not caught before the move either. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Why
SiteRowholds the state of eight separate things in one function, and #554 splits it into hooks by domain so the redesign in #542 can rebuild the screen around pieces it can hold. Five have left. Three remain, and they are coming in two PRs: this one is the trunk update, and the ticket and the apply chain, which call each other in both directions, follow together.What changes
src/renderer/hooks/use-trunk-update.jsxis new. It holds the update's chain: the fetch and reset, the install when the lockfile moved, the rebuild, the hand-off to a resumed watch on projects whose watcher rebuilds everything, the question the update asks first when the tree has edits in it, and the retry after a run that was cut short.SiteRow. They are read from the site's status with everything else the status says; the hook only asks for the status to be read again.The journey owed from #582 is here. Pausing the build watch and bringing it back had no journey with a watch running. A new
trunk-update-watch.spec.jshas two: the update with the watch running on Core, and on a project where the resumed watch does the update's build (#507). Both were written against the inline code first and pass on both.How to test this
Platforms: any. Use the Buildkite build for the current head of this branch.
The journeys carry this, from the repository root:
npx playwright test --project=journeys tests/e2e/journeys/trunk-update.spec.js tests/e2e/journeys/trunk-update-watch.spec.js.trunk-update.spec.jsis the update with no watch running: a clean tree, a linked ticket, and the question about edits in the tree. It gains one assertion: after saving edits and updating, the notice names the file they went to.In both new journeys the fetch and the reset are real, against an origin on disk; the install and the scripts are answered by stubs.
What they cannot reach is a real install and a real build:
Starting state: a Core site that has finished setup, whose trunk is behind
wordpress-develop, with the build watch running and one file edited.What must not have happened:
node_moduleswas not reinstalled if the lockfile did not change: the card names the install step as skipped.Risks and limitations
Related
Part of #554: the trunk update, after the dev server in #583. It carries the pause-and-resume journey that #582 left for it. Part of #542.
Design decisions and alternatives considered
The trunk's date and the incomplete marker are not the update's.
loadStatussets them with the rest of what the main process says about the site, and the staleness notice and the sidebar read them whether or not an update ever runs. Moving them would mean the status loader writing into the update's hook.Plain functions, not
useCallback.beginTrunkUpdate,finishUpdateand the rest were closures recreated on every render, called from handlers and from each other. Memoising them here would add twenty-item dependency lists and change when they see fresh state, which is the kind of change a move should not make.The dirty-tree question's state moved with the update. The dialog that asks it is a component of its own (#570) and holds nothing; what it shows was the update's all along, and the two answers it offers both end by starting the update.
A new journey file, not more tests in
trunk-update.spec.js. The existing three run the real build script of the fixture. The new two need the runner answered by a stub, so that a watch can be "running" and a build can be made to fail or to wait.Review outcome (required — see AGENTS.md)
First pass: 5 [fix here] · 2 [follow-up]. It diffed the old blocks against the hook with indentation removed and found one blank line of difference.
build-watch.spec.jsstill said no journey pauses the watch.index.jsx.Second pass, on 54620e5 alone: 0 [fix here] · 1 [follow-up], applied in 07f5ddc: the header of
build-watch.spec.jslisted what pauses the watch and left out checking out a pull request and leaving one.Both passes confirmed that no moved name is read before the hook is called, that each of the twenty arguments is the value the old closures read, and that the callbacks which fire long after they were made (a fetch ending, a build ending, the watch being ready) see the same render's values as before.
54620e5 → 07f5ddcchecked. The difference is one comment. It was not reviewed again by a fresh context.Checks, macOS, local:
eslint . --max-warnings=0npm run test:e2enpm run test:e2e:packagedAfter 309d129 every change is in the journeys.
Audit of the journeys, on the commit each line was written for, one break at a time, each restored before the next. Every one goes red on the line that makes the claim:
One break was not caught, in a journey this PR does not change: with "discard my edits, then update" made to discard nothing,
trunk-update.spec.jsstill passes, because the update's own reset removes the edits a moment later. The screen and the tree end the same either way.🤖 Generated with Claude Code