ci(cmux-next): the DEV compile can record the fleet whole-app build's REF (off by default)
Lets the per-landing Mac DEV Release compile record the fleet whole-app build's REF. The REF is the stored rules of one real Mac build of the same shape, which the fleet build plans its Swift compiles and links from. This is off by default, behind the repository variable CMUX_NEXT_HYBRID_REF_CAPTURE=1.
Leo, 2026-10-26: "but we have the fleet... why are we STILL TALKING ABOUT BLACKSMITH MACOS"
The only REF today (hybrid-11) was assembled by hand from a 2026-10-18 nightly build. The fleet path refuses the current tip because Package.resolved has since gained swift-atomics. The DEV app also has its own build shape, so it needs its own REF:
- singlefile for target
cmux; DEBUG_INFORMATION_FORMAT=dwarf;- an arm64 destination;
- the DEV bundle settings.
This change records one from the DEV compile itself, at the paths every mini shares in host mode. The private HQ side assembles the pack from it.
Change
-
Decide step (before the compile;
continue-on-error, 2 min timeout). It runs on a mini, with the variable on, and only when the compile isn't reused. It computesscripts/cmux-next/hybrid_ref_capture.py key, a sha256 over:- Package.resolved;
- every file of cmux.xcodeproj except xcuserdata;
- the macOS xcconfigs;
- the local macOS and shared package manifests;
- the compile recipe (
check-cmux-scheme-compile.sh,compile-dev-release.sh,lib/compilation-cache.sh); - the compile step's build-line settings;
xcodebuild -versionand the SDK build.
It then looks for an unexpired artifact named
hybrid-ref-capture-<key>from the last 13 days, using the job's existingactions: read. The store keeps packs 14 days and the manifest keeps the same 14. If there is none, it setscapture=true. In the last 48 h, 18 of 887 feat-cmux-next landings changed these inputs, so almost every landing skips the capture. -
The compile. When
capture=true,check-cmux-scheme-compile.shaddsEMIT_FRONTEND_COMMAND_LINES=YES, the only added xcodebuild setting. Measured on the nightly shape (HQ HYBRID.md section 9), it changed no output: 5767 of 5767.oand 176 of 176.swiftmodulewere identical with and without it.compile-dev-release.shalso writesbuild_mode,build_rootandderived_datato the step outputs. -
Capture step (the last step of the job;
continue-on-error, 10 min timeout). It runs only after a successful host-mode compile, from a snapshotcompile-dev-release.shcloned while it still held the host lock (oneclonefile(2)each for the tree and DerivedData), and only while the job is under 30 minutes, so it cannot reach the job's 60-minute limit.- Completeness check. Every SwiftDriver module must have a frontend compile line in the log. If any is missing, the capture is marked incomplete (
missing: [...]) and nothing is uploaded. - Packs. A complete capture writes these, deterministic (sorted, owner 0, mtime 0):
- the log;
- XCBuildData;
- DerivedData without objects, the compilation cache, logs, the app or SwiftPM's repositories;
- the workspace without .git, target/ or node_modules, with the compiled-out browser sources the compiler read;
- the committed Package.resolved.
- Upload. Each pack is split under 480 MiB and every part goes to the build fleet's private store with
cmux-ci artifact-put --lane hybrid-ref-dev. - Manifest. It is uploaded as the artifact
hybrid-ref-capture-<key>, or...-incomplete-<run>for an incomplete capture. It holds only names, sizes, sha256, paths and settings. No file content goes to this public repository.
- Completeness check. Every SwiftDriver module must have a frontend compile line in the log. If any is missing, the capture is marked incomplete (
-
Shared constant. The compiled-out
browser-sources.jsonnow comes fromscripts/cmux-next/lib/browser-sources-compiled-out.json, read by both the compile and the capture, with byte-identical content (scheme-compile-browser-import.test.shchecks the bytes). -
Settings. The decide and capture steps get the compile's settings as one
HYBRID_REF_SETTINGSblock, so only the compile step carriesCMUX_NEXT_BROWSER_DATA_IMPORT(test_cmux_next_dev_release_import_out.py). A test keeps the block equal to the compile step's env.
Tests
tests/test_cmux_next_hybrid_ref_capture.py (checks group and the linux-guard lane) checks:
- the key moves only with build-line inputs, and refuses an unknown setting;
- an incomplete capture uploads nothing;
- the packs are complete and deterministic, with nothing excluded leaking in;
- a split pack's parts join back to the whole;
- every upload carries its own sha256 and lane;
- the manifest has no file contents;
- the workflow invariants: off by default,
continue-on-errorwith a timeout, capture after the compile on host mode only, only the EMIT setting added, and no permission change.
Also passing: test_cmux_next_dev_release_import_out.py, test_cmux_next_checks_workflow.py, test_cmux_next_dev_release_host_root.py, scheme-compile-browser-import.test.sh.
Risk and what is not measured yet
- Off by default. Nothing changes until the variable is set.
- When it is on: a capture landing's compile prints frontend lines, which makes the log larger. The job holds the push compile slot for the capture time (bounded at 10 min, expected to be a few minutes), so the next landing's compile and that run's DEV publish wait that long. That happens on about 9 landings a day out of about 440. I'll measure the log size and capture time on the first run and post them here. If they are too high, the follow-up is moving the pack and upload to a controller job on the same mini.
- Not measured yet: whether a warm host-mode compile (cache hits) still prints a frontend line for every module. If it doesn't, the capture is marked incomplete, the DEV app is unaffected, and a follow-up adds a one-run cold path for that key.
- No new workflow permissions, secrets or environments. The store upload uses the mini's tailnet identity, as every
cmux-cicall on a mini does.
Agent session: agent session
š¤ Generated with Claude Code