Skip to content

[Fix] Let a work-item switch leave the build watch running instead of restarting it - #517

Merged
juanmaguitar merged 4 commits into
trunkfrom
juanmaguitar/fix-510-ticket-switch-keeps-watch
Sep 23, 2026
Merged

juanmaguitar merged 4 commits into
trunkfrom
juanmaguitar/fix-510-ticket-switch-keeps-watch

Conversation

@juanmaguitar

@juanmaguitar juanmaguitar commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

Why

On a Gutenberg site with the build watch running, linking an issue, switching work items or unlinking paused the watch for the checkout and resumed it after. Gutenberg's npm run dev removes build/ and rebuilds every package when it starts, so a branch switch that changed nothing under packages/ cost a full rebuild: about 28 s on macOS, about 8 minutes on Windows, with nothing on screen saying why. Core's grunt _watch resumes for free, which is why this never showed there.

Seen on the 21 Sep 2026 pass of #508: a number typed into the GitHub issue field linked issue #83258 and the watch rebuilt for five minutes.

What changes

The root cause is that every work-item switch planned itself as a whole-tree change (planWatchImpact({ wholeTree: true }) in ticketSwitchLifecycleRef.begin), and a whole-tree change always pauses a live watch. That is right for the switch that puts back a pull request parked on a work item, because that one chains an install and a build of its own (#506). It is wrong for a plain link, unlink or switch, which only moves the checkout.

The rebuild such a switch needs is the selective one a running watch already does: Git writes the files of the new branch, the watch recompiles those packages. That is the same bargain the app makes for a src-only patch (#262), and planWatchImpact already knew how to answer it. The switch only had to stop claiming to be whole-tree.

  • planTicketSwitchImpact (src/renderer/update-plan.cjs) asks it with the switch's real shape, from two numbers: the pull request checked out now and the one the target work item has parked. It also takes the watch's state rather than a boolean, because a watch that has started and not reached its ready line is rebuilding build/ from scratch, not watching: a checkout landing under it would leave the packages it had already written on the old tree's output. Such a switch keeps the pause it had on trunk. Only a watch that is actually watching is handed the change. prTransition = fromPr !== toPr mirrors ticketPrImpact in main, which decides the same thing from the two refs after the fact.
  • The decision has to be made before the checkout starts, and sites:set-ticket only reports prTransition afterwards. So branches:list now reports savedPr per work item, from savedPrRef in src/main.js: the rule ticketCheckoutRef already applied, extracted so both sides read the record the same way. No extra Git read, since the existence check reuses the ref list the handler already has.
  • On a switch that keeps the watch, complete hands the change to it (handOffToWatch), so the Build watcher tab reads (compiling) until it goes quiet (Nothing on screen says the build watch is still compiling a pull request just applied #492), the same signal an applied patch gets.
  • A restore begin could not foresee (a retry of a failed switch, where main reads the ref the switch was leaving and the renderer cannot) still pauses, late, before its install.

Deliberately not in it: a switch still runs no npm install when the two branches disagree on package-lock.json, exactly as before.

How to test this

Platforms: either, but macOS or Windows on a real Gutenberg site is what exercises the bug. The unit suite cannot reach it (no journey starts a build watch), so this is the pass that matters. Build: current head.

Starting state: a Gutenberg site with node_modules installed and a first build done, no work item linked, no pull request checked out. From the worktree, npm run build:once before launching, since src/renderer/index.js is a build product.

  1. Click Start build watch. Wait until the Build watcher tab reads (watching).
  2. Type an issue number into GitHub issue number or URL and click Link issue. Expected: the switch narrates itself as usual, the tab goes to (compiling) for a few seconds and back to (watching). The watch is never (paused), and its log shows no npm run dev start banner and no from-scratch rebuild of every package.
  3. Click Unlink, then Continue working on that issue from the card below. Expected: the same, both times.
  4. Now check out a pull request on that issue, then Unlink, then link the issue again. Expected: this switch does pause the watch, the checklist shows the build step skipped and names the resumed watch, and the applied banner follows the rebuild the way [Fix] Let the applied banner follow the build watch instead of turning green at checkout #511 left it.
  5. Stop the watch and link another issue. Expected: nothing rebuilds, as before.
  6. Click Start build watch and, while the tab still reads (building), link an issue. Expected: this one does pause the watch, because it has not finished starting; the tab goes (paused) and the switch resumes it. Windows is where this is easy to hit, the window being minutes rather than seconds.

What must not have happened: no full rebuild on steps 2 and 3 (the whole point: on Windows that is eight minutes), the watch not left (paused) on any path, the dev server not interrupted, the parked pull request in step 4 still restored with its install and build, and a build/ that mixes two trees after step 6.

The unit tests that carry the decisions fail on the old code: planTicketSwitchImpact did not exist, and branches:list returned no savedPr (checked). What they cannot see is the watch process itself, which is what the steps above are for.

Done on macOS 27 (M5 Pro), on the Buildkite artifact for 4cdedc8, on a built Gutenberg site, with issue #83341 for steps 2 to 4, pull request #83350 for step 4, and #83324 for step 5. All five steps as written.

The evidence for steps 2 and 3 is the Build watcher log itself: one Running npm run dev… for the whole run, the one from Start build watch at 15:04:31, ending in Watching for changes. Linking the issue, unlinking it and switching back to it added nothing to that log: no second start banner, no Paused while another operation uses the build, no from-scratch rebuild of every package. On trunk each of those three switches costs one.

Done again on Windows 11 (UTM VM, arm64 host), on the Buildkite artifact for 781e3be, the rebased head, on a built Gutenberg site at C:\Users\JuanMa\Desktop\demo-gutenberg-3. All six steps pass, and Windows is what answers the two questions macOS could not:

  • Step 6, the switch taken while the watch is still starting. The watch was paused and resumed, as the fix intends. The window is minutes here rather than seconds, which is how this case was found in the first place.
  • The file handles. Step 2's checkout rewrote files with the watch alive and nothing failed: no error from Git, no watch exit. The risk this PR declared open is closed. The watch then recompiled selectively, package by package (rebundled / rebuilt per package in its tab, block-library the longest at 19.7 s), rather than running the clean from-scratch cycle it runs on trunk, where the same switch costs about eight minutes.

Steps 4, 5 and 6 behaved as on macOS: the plain switch back to a second work item kept the watch, the parked pull request paused it and handed the rebuild to the resumed watch, and with the watch stopped nothing rebuilt.

Two lines the watch prints on Windows are worth knowing and are not this PR's: Gutenberg's own dev.mjs starts by reporting 'ps' is not recognized as an internal or external command, and a watch killed mid-TypeScript-build (which is what a pause is) prints TypeScript compilation failed before it exits. The state machine is unaffected — the watch stayed paused and resumed correctly — but the tab reads like a build error when nothing is wrong.

Step 4 behaved as it must: applying the pull request paused the watch and handed the rebuild to it, the applied banner sat amber reading PR #83350 is applied. The site is rebuilding. while the tab rebuilt, and turned green with the Checked out the pull request toast when the watch was watching again (#508, #511). Step 5, with the watch stopped, rebuilt nothing.

Risks and limitations

  • Nothing disables Link issue while the watch is starting, so step 6's switch is offered throughout that window; it is now planned as a pause rather than refused. Disabling the ticket actions while the watch owns build/ is the same family as Start dev server and Start build watch are reachable while a build is running outside the wizard #503 and is not done here.
  • The watch now compiles while the checkout is still landing. It will briefly see a half-written tree and may print resolution errors into its tab before the final write re-triggers it. Self-correcting, but noisier than the silence the paused watch gave.
  • Windows file handles. Answered by the Windows pass above: the checkout rewrote files under a live watch without an error on either side.
  • On Windows the watch prints TypeScript compilation failed when a pause kills it mid-build, so the tab reads like a failure although the pause is deliberate and the resume is clean. Cosmetic, pre-existing, and not made more likely by this PR, which pauses in one more case than before.
  • A switch between two branches whose package-lock.json differ still installs nothing. Unchanged from trunk, and out of scope here.
  • The renderer predicts the switch's shape from state it already holds (pullRequest from site:status, savedPr from branches:list, both reloaded after every switch) rather than asking main. A prediction that is wrong can only be wrong towards a needless pause, and the one case where it was is fixed in the second commit.

Related

Fixes #510. Follow-up to #508 and #506. Part of #251.


Design decisions and alternatives considered

A new IPC handler that previews the switch would have been more faithful, since main would answer with the same function that later performs the switch. Rejected: it adds a round trip in front of a switch that already has one, and it does not remove the disagreement, it only moves it, since the record can still change between the preview and the switch. Reporting savedPr on a list the panel already reloads after every switch costs nothing and is read from the same record.

Asking Gutenberg's watch not to rebuild on start is the other way to make the pause cheap. Out of scope: npm run dev has no such flag, and the upstream --no-clean work is not this repository's.

A journey (layer 4) would replace the manual pass, per AGENTS.md. Nothing in tests/e2e/ starts a build watch today, so it would mean building that fixture first: a fake dev script that prints the ready line and stays up, and an assertion that the process was never restarted. Worth doing, and deliberately not done here, so the fix can ship in beta.2.

Review outcome (required — see AGENTS.md)

4 findings: 2 [fix here] · 2 [follow-up]. Both [fix here] fixed in 4cdedc8, one [follow-up] fixed with them, one recorded above.

  • 🟡 architecture/tests, [fix here]: the saved-pull-request lookup lived in index.jsx, where the suite cannot reach it (§1). Fixed: moved to savedPrForSwitch in src/renderer/ticket-branch-list.cjs, with its own tests.

  • 🟡 performance, [fix here]: re-linking the work item already in hand while its pull request is checked out read the row, found nothing (a branch records its parked pull request only when it is left), and paused the watch for a switch main performs as a no-op. The exact cost this PR removes, in the one case it was adding back. Fixed: linking the item in hand answers with the pull request already checked out.

  • 🔵 tests, [follow-up]: the branches:list test covered both negative reasons but not the ordinary row with no parked pull request. Fixed in the same commit.

  • 🔵 cross-platform, [follow-up]: the watch compiling a half-checked-out tree, and the Windows handle question. Recorded under Risks; no code change.

  • Manual pass: completed on macOS, on 4cdedc8; see How to test this.

  • Review: completed, by a fresh agent context (Explore subagent, review standard in .github/instructions/code-review.instructions.md); reviewed head 964f651 (now 9fc3315, same tree, commit message reworded) / base origin/trunk; npm run lint clean and npm test 1625 pass / 0 fail / 2 skipped at that head.

  • Since review: 964f651 to 4cdedc8, the review's own findings, plus their tests. npm run lint clean, npm test 1630 tests / 1628 pass / 0 fail / 2 skipped at the new head. CodeRabbit requested on 4cdedc8 by comment once the PR left draft.

Screenshots or recording

Nothing on screen changes shape. What changes is what the Build watcher tab says during a work-item switch, and above all what its log does not say: on this branch a link, unlink or switch adds nothing to it, where on trunk each one starts npm run dev again and rebuilds every package. The log excerpt in How to test this is that evidence.

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: ecd8e2ce-fec1-4407-afc3-7159700769ec

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/fix-510-ticket-switch-keeps-watch branch from 8a72ac9 to 4cdedc8 Compare September 22, 2026 11:16
@juanmaguitar
juanmaguitar marked this pull request as ready for review September 22, 2026 15:12
@juanmaguitar

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

juanmaguitar and others added 3 commits September 23, 2026 07:46
… restarting it

Linking an issue, switching work items or unlinking planned itself as a
whole-tree change, so a running build watch was paused for the checkout and
resumed after. On Gutenberg a resumed `npm run dev` removes build/ and
rebuilds every package, so a switch that moved two files paid a full rebuild:
about 28 s on macOS, about 8 minutes on Windows, with nothing on screen
saying why. Core's grunt _watch resumes for free, which is why it never
showed there.

The rebuild such a switch needs is the selective one a running watch already
does, the same bargain a src-only patch makes (#262). planWatchImpact knew
how to answer that; the switch only had to stop claiming to be whole-tree.
planTicketSwitchImpact asks it with the switch's real shape, from the pull
request checked out now and the one parked on the work item being switched
to, and only that restore still pauses the watch, because it chains an
install and a build of its own (#506). The decision is made before the
checkout starts, so branches:list now reports the parked pull request per
work item, read from the same record ticketCheckoutRef consults. A restore
begin did not foresee still pauses late, before its install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… right for the item in hand

Two findings from the pre-PR review.

The lookup that decides whether a switch restores a pull request sat in
index.jsx, where nothing in the suite can reach it (AGENTS.md section 1):
every combination of its output was tested through planTicketSwitchImpact
while the code producing that output had no test at all. It moves to
ticket-branch-list.cjs, beside the other readings of the same branch list.

And it was wrong for one switch. A branch records its parked pull request
only when it is left, so re-linking the work item you are already on, which a
ticket link can do (#464), read the row, found nothing, and took "leaving a
pull request" from the one checked out on it. That paused the watch, and main
performed the switch as a no-op, so the resumed watch rebuilt from scratch
for a switch that moved nothing: the exact cost this branch removes, in the
one case it was adding it back. Linking the item in hand now answers with the
pull request already checked out, so the two sides agree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tem switch

Found on the Windows pass: nothing disables Link issue while the watch
rebuilds, and on Windows that window is minutes. A switch taken there was
planned as live-watch, so the checkout landed under a watch that is not
watching yet but rebuilding build/ from scratch. The packages it had
already written would keep the old tree's output and the tab would reach
(watching) over a build/ that mixes both.

planTicketSwitchImpact now takes the watch's state rather than a boolean
and treats 'building' as whole-tree, so that switch takes the pause it
took on trunk: one rebuild more in a narrow window, against a build/ that
cannot be trusted. Only a watch that is actually watching is handed the
change. The guide says so on both pages.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar
juanmaguitar force-pushed the juanmaguitar/fix-510-ticket-switch-keeps-watch branch from 068b19a to 781e3be Compare September 23, 2026 05:48
Both follow-ups from the review of 781e3be. applied-layer.cjs called
update-plan.cjs dependency-free, which stopped being true when the switch
decision took watchOccupiesBuild from its single definition; the comment
now says what the family actually shares, which is being DOM-free. And the
Core half of the new switch test claimed the switch waits for the one-shot
build, when the terminal lock refuses it before the decision is reached:
the case pins the decision, not a path the app can walk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juanmaguitar
juanmaguitar merged commit 6cb471c into trunk Sep 23, 2026
10 checks passed
@juanmaguitar
juanmaguitar deleted the juanmaguitar/fix-510-ticket-switch-keeps-watch branch September 23, 2026 12:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Linking an issue or switching tickets on a Gutenberg site pauses and restarts the build watch, a full rebuild for a branch switch that needs none

1 participant