ci(cmux-next): the DEV compile can record the fleet whole-app build's REF (off by default)

Merged
TE
9d ago
+691-2

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 computes scripts/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 -version and the SDK build.

    It then looks for an unexpired artifact named hybrid-ref-capture-<key> from the last 13 days, using the job's existing actions: read. The store keeps packs 14 days and the manifest keeps the same 14. If there is none, it sets capture=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.sh adds EMIT_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 .o and 176 of 176 .swiftmodule were identical with and without it. compile-dev-release.sh also writes build_mode, build_root and derived_data to 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 snapshot compile-dev-release.sh cloned while it still held the host lock (one clonefile(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.
  • Shared constant. The compiled-out browser-sources.json now comes from scripts/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.sh checks the bytes).

  • Settings. The decide and capture steps get the compile's settings as one HYBRID_REF_SETTINGS block, so only the compile step carries CMUX_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-error with 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-ci call on a mini does.

Agent session: agent session

šŸ¤– Generated with Claude Code