ci(cmux-next media): let tours run side by side on one Mac
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 withdebug.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_snapshotused to be captured withscreencapture -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'swindow <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 recordscapture_window. This route leaves out child windows (sheets, popovers) thatdebug.window_snapshotincludes; 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-nexttag.
The lock lets go when the job's
Runner.Workerexits, 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.pyand the newtests/test_media_tag_lock.py(linux-guard lane), red then green:- At 3b6b7404 (tests only) against the old
tour.pyand with no lock script,test_the_tour_never_activates_the_appandtest_without_a_window_snapshot_the_tour_uses_a_screen_routefail, 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.
- At 3b6b7404 (tests only) against the old
actionlint .github/workflows/cmux-next-media.ymlis 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.pywas in the linux-guard lane but no workflow ran it;ci-guards.ymlnow 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-*.pcmtour failures (14 of 22 recent failed tours) are not from the prune: every one I checked (jobs 118285853270, 118197207461, 118076888558, 118006980751) followedmedia_module_cache_stamp.py clearremovingExplicitPrecompiledModulesafter a binary pin change, while the build's dependency records still point at those .pcm files. The prune only removes whole idlecmux-ci-media-*dirs, and now only under the tag lock. #22181 fixes that clear. - Base reds on this head, not from this PR:
release-notaryfailsNEXT post-publication dependency contractthe same way on feat-cmux-next (job 118292665776), andcmux-next checksfails 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