base autofix: finish each run and land it on the newest tip

Merged
TE
15d ago
+507-165

This moves base autofix's push step on feat-cmux-next into scripts/cmux-next/base-autofix-push.sh and tightens what it lands on a moved tip. It no longer fixes the settings-export livelock by itself (see Not fixed here).

Background: base already stops cancelling runs in progress (#22038), and it already re-applies a repair file by file when the tip has moved (#22474). When the newer tip changed any input of a repaired file, the "Push the repairs" step leaves that file out and fails the run. That is the right thing to do: an export made at the old head could be wrong at the tip (a revert in between, say). This PR keeps that rule.

What changes

  • "Push the repairs" calls scripts/cmux-next/base-autofix-push.sh. It reads the same input areas (autofix-areas.sh) and holds every file whose areas BASE..tip touched, along with any output no area is known to build. A file that comes from a moved input never lands as it was made.
  • A file is also held when a newer commit wrote it: a PR's own regeneration, or a base autofix repair made from a newer tree. An older autofix commit is overwritten instead of blocking the file, as the old rule did when the tip had edited it at all.
  • Exports land whole or not at all: the schema, docs/mdm and the stamp go together, and so do the three action exports. Strings, docs shortcuts and app-host files built from an export that waits wait with it.
  • The defaults snapshot is never copied from the old tree. It is re-made on the tip with defaults_guard.py snapshot, and only when nothing else waits.
  • If the commits since BASE can't be read (a bad fetch, say), the run stops. Before, that read as "no newer writes".
  • A push that is refused while the branch didn't move fails at once. A lost race is retried up to CMUX_BASE_AUTOFIX_PUSH_ATTEMPTS times (5). If the tip already has the repair, nothing is pushed.
  • The script is in the workflow's push paths, and its test runs in cmux-next.yml.

Not fixed here: the settings inputs include CmuxNextSettings/Localizable.xcstrings, which changes with almost every merge, and the Mac export plus autofix take about 18 minutes. So most runs still hold the settings exports, as on base (for example, run 39553409406 left them out after the tip moved to 3e0b1c278ad9). The way out is to regenerate on the newest tip, or to have the settings area stop counting string-catalog-only changes. That is follow-up work.

Tests

  • python3 tests/test_cmux_next_base_autofix_finishes.py is base's suite from #22038/#22474, now driving the script instead of slicing the inline loop. Three of its cases were reworded:
    • Two retry cases expect attempt 2 of where the old loop reached attempt 3, because the script reads the tip before its first push.
    • The "conflict" case is now "a newer write of the file fails without pushing and names it", because the script applies no patch.
    • Every hold case is unchanged, including "a repair whose inputs changed meanwhile is left to the newer run".
    • Red against the previous head's script: 7 failures, including that inputs-moved case. Green at the head: 16 OK.
  • bash scripts/cmux-next/tests/base-autofix-push.test.sh runs in a shallow bot clone. Its cases:
    • 1โ€“6: plain push, moved tip, a newer edit of the file, an older autofix replaced, already landed, a deleted directory.
    • 7: moved settings inputs hold the exports.
    • 8: the workflow pushes through the script.
    • 9: one file waits and the rest lands.
    • 10: exports land whole.
    • 11: an older run never overwrites a newer one.
    • 12: the snapshot is re-made on the tip.
    • 13: bounded retries.
    • 14: a rejection with no move.
    • 15: unreadable history fails closed.
    • Green at the head: "PASS: base autofix lands its repair on a moved feat-cmux-next; files whose inputs moved or that a newer commit wrote wait".

Observed / Not run

  • Observed: both suites red on the previous script, then green. actionlint on the base autofix workflow is clean. prepush-gate.py reports no new violations.
  • Not run: the new push path hasn't run on GitHub yet; the first base run after the merge is the real check. The real defaults_guard.py snapshot on a moved tip is covered only by a stand-in. shellcheck isn't installed on huge-blue.

Changelog

none

Agent session: huge-blue

๐Ÿค– Generated with Claude Code