ci(cmux-next media): let tours run side by side on one Mac

Merged
TE
17d ago
+500-79

Media tours can now run several at once on one Mac, each capturing only its own app's window. That lets the runner hook drop its one-tour-per-Mac limit.

What changes:

  • The tour no longer activates its app, and launches it with CMUX_NEXT_NO_ACTIVATE=1. It drives the app through the app's own control socket and screenshots it with debug.window_snapshot, so being in front never mattered. Concurrent tours would otherwise trade focus and the key window back and forth.

  • The fallback captures the window, not the screen. A build without debug.window_snapshot used to be captured with screencapture -R, a rectangle of the display, which would include whatever covers the window, such as another tour's app. It now captures that window alone: screencapture -l <id>, or the screen-capture helper's window <id>. The window server id comes from the window list by the app's pid. With no id there is no shot, never the display. The manifest records capture_window. This route leaves out child windows (sheets, popovers) that debug.window_snapshot includes; it is only for old bases without that verb.

  • A tour holds its tag (scripts/ci/media_tag_lock.py, new step "Hold this tour's tag"). One tag's warm DerivedData, sockets and daemon session take one tour at a time; other tags never wait. The concurrency group already keeps two runs of a pull request apart, but not these cases:

    • a cancelled run that is still stopping;
    • a dispatch;
    • a merge tour beside a dispatch, which share the feat-cmux-next tag.

    The lock lets go when the job's Runner.Worker exits, or after 3 h. A second tour of a tag waits up to 60 min: longer than a warm merge tour holds the shared tag (54 min seen), while leaving the waiter 45+ min of its 120-min job for a cold build and the tour. A longer wait would only end in a job-timeout cancel instead of the "still held" error. When the pull request head predates the lock, the step takes the lock script and the lock-aware prune from the run's commit, so unrebased PRs still run and their prune never deletes a held tag.

  • The DerivedData prune skips a held tag and holds a tag's lock itself until its dir is gone, so a tour never builds into DerivedData that is being deleted, or has its DerivedData deleted by another pull request's tour.

What still needs the console, and stays on the console runner, is unchanged here: app-host XCTest (testmanagerd), E2E video recording and frame-pacing benches.

Validation

  • tests/test_cmux_next_tour.py and the new tests/test_media_tag_lock.py (linux-guard lane), red then green:
    • At 3b6b7404 (tests only) against the old tour.py and with no lock script, test_the_tour_never_activates_the_app and test_without_a_window_snapshot_the_tour_uses_a_screen_route fail, and so do 3 of the 4 lock tests.
    • At ff2f66f8 all 4 lock tests pass, and so do the tour's activation, screen-route and app-window tests.
    • At 1286a4a9 (tests only) the prune test fails: the old prune has no --lock-dir. At 7468a342 it passes, and so do the lock and tour tests.
  • actionlint .github/workflows/cmux-next-media.yml is clean.
  • At 071bcb06 (test only) the wait test fails against the 7200 s wait (7200 not less than or equal to 4500); at bd085a79 it passes. tests/test_media_tag_lock.py was in the linux-guard lane but no workflow ran it; ci-guards.yml now runs it in the release-notary group. All four tour/lock/prune tests pass on Huge Blue at bd085a79.
  • Screenshots now show the app's window as not key (CMUX_NEXT_NO_ACTIVATE), so focus styling looks inactive in tour media.
  • Missing ExplicitPrecompiledModules/Darwin-*.pcm tour failures (14 of 22 recent failed tours) are not from the prune: every one I checked (jobs 118285853270, 118197207461, 118076888558, 118006980751) followed media_module_cache_stamp.py clear removing ExplicitPrecompiledModules after a binary pin change, while the build's dependency records still point at those .pcm files. The prune only removes whole idle cmux-ci-media-* dirs, and now only under the tag lock. #22181 fixes that clear.
  • Base reds on this head, not from this PR: release-notary fails NEXT post-publication dependency contract the same way on feat-cmux-next (job 118292665776), and cmux-next checks fails the same six checks (state roots, concurrency, strings, type scale, script tests, web bundle key) as merged #22054 (job 118309312276), being fixed in #22117.
  • Old PR heads run their own tour.py, which still activates the app and captures a rectangle, until they merge main.
  • Not run yet: a tour on a Mac with two tours in flight. The runner-side change that lets them share a Mac follows this PR.

Runtime test: tests/test_media_tag_lock.py, tests/test_ci_prune_media_derived_data.py and tests/test_cmux_next_tour.py in the release-notary/preflight guard jobs at bd085a79

Changelog

none

Review: SHIP at bd085a791a90 (cmux-review subagent; correctness first)

šŸ¤– Generated with Claude Code