ci: skip the nightly publish when no changed path reaches the app
The Nightly rebuilt, signed, notarized, and published whenever main's HEAD differed from the commit the nightly tag points at. Web, documentation, and CI-only merges land between the changes that alter what the Nightly ships, so each of those runs spent the full macOS publishing pipeline republishing an app that was already shipped.
decide now compares the paths changed between the published commit and HEAD, and skips only when no changed path is an input to what the Nightly publishes. Replaying main's first-parent history for the seven days to 2026-09-23 through this gate, 182 of 573 pushes (32%) publish no app change and skip; the other 391 build. That bounds the share of eligible pushes, not runner minutes: the Nightly's serialized concurrency already cancels some of those runs before they reach the build.
What counts as an input
The base classification is scripts/ci/detect_ci_change_areas.py's existing router, but its release_build signal answers a narrower question — whether a change needs a Release compile. Several paths change the published bytes without compiling anything, and the router excuses them because a dedicated pull-request lane covers each. scripts/ci/nightly_build_inputs.py closes that gap by derivation rather than by a second list:
- Bundled paths are read back out of the
cmuxtarget's resource and copy-files phases incmux.xcodeproj. That is whereskills/cmux-cua,Resources/bin/open, andResources/bin/cmux-claude-wrapperenter the app; the last two are explicitly Release-neutral in the router. A new bundled resource cannot become skippable by being added somewhere this script does not know about. - The
cmux-cliproduct is copied into the bundle, so the router'scliarea counts as an input alongsiderelease_build.CLI/**ships without any Release compile covering it. - The cmux-tui client is chosen by the commits
scripts/ci/resolve-cmux-tui-client-commit.shlooks at, so itsPATHSlist — including.github/workflows/cmux-tui-artifacts.ymland.github/workflows/cmux-tui-build-package.yml— is read from that script. - What the workflow runs: the signing, delta, appcast, and publishing helpers, neutral for pull requests because no pull-request lane runs them. Derived from the workflow text, so a new step cannot leave its script invisible.
daemon/remote/, whose remote daemon assetsscripts/build_remote_daemon_release_assets.shpublishes beside the app.
When it builds anyway
Anything unproven builds: a non-fast-forward comparison, a file list at or past GitHub's 300-entry cap, a commit list that does not match total_commits, an unknown path, a rename out of a neutral directory, a classifier that fails or prints anything but JSON, or an API failure.
Force and manual dispatch never consult the check. The 08:47 UTC catch-up schedule is also excluded, so one build a day remains as a backstop and a path this check gets wrong costs at most a day of staleness. Over the sampled week that backstop would not have fired at all, because an app change landed every day. The six-hour cache warmup keeps its existing outputs, and NIGHTLY_PUSH_MIN_INTERVAL_HOURS is untouched.
decide now checks out one revision so the classifier can read the Xcode project and package graph.
Validation
python3 tests/test_ci_nightly_push_throttle.py: passed. The cases drive the realdecidescript under Node against a mocked compare API and run the realnightly_build_inputs.pyin the checkout, so the workflow and the contract cannot disagree about what reaches the app. Against main'snightly.ymlthe new cases fail. They also assert the checkout step is present and ordered before the decision, since without it the gate would fail open and silently never fire.- Every
ci-guards.ymltest command (136): passed, except./tests/test_ghostty_zig_version_sync.sh, which needs theghosttysubmodule this checkout does not have and fails the same way on main. actionlint .github/workflows/nightly.yml: passed.- Two guards read
decideby position rather than by meaning and needed repair:tests/test_ci_pull_request_caches_are_read_only.pytook the decision step by index, andtests/test_nightly_universal_build.shpinned theconst shouldBuildline while asserting something about the expression. Neither assertion changed. - No macOS build ran here. This changes only the build decision, so it establishes nothing about build or publish behaviour once a build is selected.
An independent agent review of the first commit found the CLI/, Resources/bin, cmux-tui-workflow, and commit-pagination gaps above; the review is posted below and every finding it raised is addressed in this branch.
Adopts the nightly half of #12610, rebased onto current main, which that PR's conflict is in. #12610's Vercel .git retention and Next production-type hook are not included here and remain open there.
🤖 Generated with Claude Code