Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
4d51423
test: red test, a global notification center passed as a center argum…
teamleaderleo Oct 9, 2026
63ab4a0
ci: the notification-center lint also fails a global center handed to…
teamleaderleo Oct 9, 2026
0d94be2
fix(messageslab): a Chief subagent link check is a URL property, not …
lawrencecchen Oct 10, 2026
521b219
Merge remote-tracking branch 'origin/feat-cmux-next' into feat-cmux-n…
lawrencecchen Oct 10, 2026
20cd2f6
ci: a base autofix bot commits generated files, tree inputs and fmt (…
teamleaderleo Oct 10, 2026
7af5d8b
fix(release-rails): resolve capabilities re-exported from another cmu…
teamleaderleo Oct 10, 2026
1e0c0bb
ci: format staged files before commit (#18808)
teamleaderleo Oct 10, 2026
08cd108
ci: check rustfmt on the cmux-tui packages a PR touches (#18800)
teamleaderleo Oct 10, 2026
7a52911
ci: add mini-first Swift canary for cmux-next (#18781)
teamleaderleo Oct 10, 2026
c980cc5
cmux-next: repin app FFI to current base release (#18776)
teamleaderleo Oct 10, 2026
8da37f7
cmux-next: same-tree cmux-tui is a hard check only for PRs that chang…
teamleaderleo Oct 10, 2026
1beff16
fix(ci): keep cmux-next minis first through bot retries (#18615)
teamleaderleo Oct 10, 2026
5c0a0fc
docs(cmux-next): map UI parity reference sources (#18561)
teamleaderleo Oct 10, 2026
c08fa90
fix(nightly): bound the publish notes step at 10 minutes (#18404)
teamleaderleo Oct 10, 2026
0e4c30a
ci(cmux-next): retry bad R2 binary reads and let the tree gate pin it…
teamleaderleo Oct 10, 2026
d808aa1
ci(gallery): capture narrow responsive states (#18901)
teamleaderleo Oct 10, 2026
7a6cbb4
Merge feat-cmux-next into ci/notice-lint-default-argument
teamleaderleo Oct 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/scripts/cmux-tui-fmt-changed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Run `cargo fmt --check` on each cmux-tui package a change touches.

Usage: cmux-tui-fmt-changed.py [--repo ROOT] [--base SHA]

PR CI never runs cmux-tui.yml (dispatch only), so #18706 landed unformatted
Rust and broke base `cargo fmt --check`. This checks the tracked packages
under cmux-tui/ owning a changed .rs file or Cargo.toml between --base and
HEAD; with no usable base, or when a rustfmt config or the toolchain pin
changes, it checks every tracked package. Vendored crates (cmux-tui/vendor/)
keep their upstream formatting and are never checked. rustfmt reads metadata only
(`--no-deps`), so nothing builds and no dependency downloads. Every touched
package is checked even after one fails, and each red one is named.
"""

from __future__ import annotations

import argparse
from pathlib import Path, PurePosixPath
import subprocess
import sys


ROOT = "cmux-tui"
VENDOR = f"{ROOT}/vendor/"
EVERYTHING = frozenset({"rustfmt.toml", ".rustfmt.toml", "rust-toolchain.toml"})


def git(repo: Path, *args: str) -> str:
return subprocess.run(["git", "-C", str(repo), *args], check=True, capture_output=True, text=True).stdout


def packages(repo: Path) -> set[str]:
"""Tracked package directories (a Cargo.toml with a [package] table) under cmux-tui/."""
found = set()
for path in git(repo, "ls-files", "-z", "--", ROOT).split("\0"):
if PurePosixPath(path).name != "Cargo.toml" or path.startswith(VENDOR) or not (repo / path).is_file():
continue
if any(line.strip() == "[package]" for line in (repo / path).read_text(encoding="utf-8").splitlines()):
found.add(str(PurePosixPath(path).parent))
return found


def changed(repo: Path, base: str) -> list[str] | None:
"""Paths under cmux-tui/ changed since `base`, or None when there is no usable base."""
if not base or set(base) == {"0"}:
return None
try:
git(repo, "cat-file", "-e", f"{base}^{{commit}}")
except subprocess.CalledProcessError:
print(f"cmux-tui-fmt-changed: base {base} is not in this checkout; checking every package")
return None
return [path for path in git(repo, "diff", "--name-only", "-z", base, "HEAD", "--", ROOT).split("\0") if path]


def manifest_dirs(repo: Path, base: str) -> set[str]:
"""Directories with a Cargo.toml at `base` or tracked now: a deleted package's files stop there."""
listed = git(repo, "ls-tree", "-r", "-z", "--name-only", base, "--", ROOT).split("\0")
listed += git(repo, "ls-files", "-z", "--", ROOT).split("\0")
return {str(PurePosixPath(path).parent) for path in listed if PurePosixPath(path).name == "Cargo.toml"}


def touched(paths: list[str], known: set[str], boundaries: set[str]) -> set[str]:
"""The current packages owning changed Rust sources or manifests (the nearest Cargo.toml up)."""
owners = set()
for path in paths:
pure = PurePosixPath(path)
if path.startswith(VENDOR) or (pure.suffix != ".rs" and pure.name != "Cargo.toml"):
continue
owner = next((str(parent) for parent in pure.parents if str(parent) in boundaries), None)
if owner in known:
owners.add(owner)
return owners


def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument("--repo", type=Path)
parser.add_argument("--base", default="")
args = parser.parse_args(argv)
repo = (args.repo or Path(git(Path.cwd(), "rev-parse", "--show-toplevel").strip())).resolve()
known = packages(repo)
paths = changed(repo, args.base.strip())
if paths is None or any(PurePosixPath(path).name in EVERYTHING for path in paths):
selected = known
else:
selected = touched(paths, known, manifest_dirs(repo, args.base.strip()))
if not selected:
print("cmux-tui-fmt-changed: no cmux-tui package changed")
return 0
red = []
for package in sorted(selected):
manifest = repo / package / "Cargo.toml"
print(f"cargo fmt --check --manifest-path {package}/Cargo.toml", flush=True)
# From cmux-tui/ so rustup uses its rust-toolchain.toml pin.
if subprocess.run(["cargo", "fmt", "--check", "--manifest-path", str(manifest)], cwd=repo / ROOT).returncode:
red.append(package)
for package in red:
print(f"::error title=cmux-tui rustfmt::{package} is not rustfmt-clean; "
f"run `cargo fmt --manifest-path {package}/Cargo.toml` from the repository root and commit the result")
print(f"cmux-tui-fmt-changed: {len(selected) - len(red)}/{len(selected)} packages rustfmt-clean")
return 1 if red else 0


if __name__ == "__main__":
sys.exit(main())
170 changes: 170 additions & 0 deletions .github/scripts/test_cmux_tui_fmt_changed.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Behavior tests for the changed-crate cmux-tui rustfmt check.

PR CI never ran cmux-tui.yml (dispatch only), so #18706 landed unformatted Rust
and broke base `cargo fmt --check`. cmux-tui-fmt-changed.py runs
`cargo fmt --check` on each package a change touches; these tests drive it on
a scratch repository with a fake cargo that records its calls.
"""

from __future__ import annotations

import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest


SCRIPT = Path(__file__).with_name("cmux-tui-fmt-changed.py")
FAKE_CARGO = """#!/usr/bin/env bash
printf '%s\\n' "$*" >> "$CARGO_LOG"
for arg in "$@"; do
if [[ "$arg" == *"/${FAIL_PACKAGE:-none}/Cargo.toml" ]]; then
echo "Diff in $arg"
exit 1
fi
done
"""


class FmtChangedTests(unittest.TestCase):
def setUp(self) -> None:
self.tmp = Path(tempfile.mkdtemp())
self.repo = self.tmp / "repo"
# The workspace root is a package too, as in the real tree.
self.write("cmux-tui/Cargo.toml", '[package]\nname = "root"\n\n[workspace]\nmembers = ["crates/a", "crates/b"]\n')
self.write("cmux-tui/src/main.rs", "fn main() {}\n")
self.write("cmux-tui/rustfmt.toml", "max_width = 100\n")
self.write("cmux-tui/crates/a/Cargo.toml", '[package]\nname = "a"\n')
self.write("cmux-tui/crates/a/src/lib.rs", "pub fn a() {}\n")
self.write("cmux-tui/crates/a/src/deep/mod.rs", "pub fn d() {}\n")
self.write("cmux-tui/crates/b/Cargo.toml", '[package]\nname = "b"\n')
self.write("cmux-tui/crates/b/src/lib.rs", "pub fn b() {}\n")
# Its own workspace, outside the cmux-tui workspace members.
self.write("cmux-tui/crates/ffi/Cargo.toml", '[package]\nname = "ffi"\n\n[workspace]\n')
self.write("cmux-tui/crates/ffi/src/lib.rs", "pub fn f() {}\n")
self.write("cmux-tui/target/debug/build/x/Cargo.toml", '[package]\nname = "built"\n')
# Vendored upstream code keeps upstream formatting.
self.write("cmux-tui/vendor/up/Cargo.toml", '[package]\nname = "up"\n')
self.write("cmux-tui/vendor/up/src/lib.rs", "pub fn u() {}\n")
self.write("README.md", "readme\n")
(self.repo / ".gitignore").write_text("cmux-tui/target/\n")
self.git("init", "-q")
self.git("add", ".")
self.git("commit", "-qm", "base")
self.base = self.git("rev-parse", "HEAD").strip()
bin_dir = self.tmp / "bin"
bin_dir.mkdir()
cargo = bin_dir / "cargo"
cargo.write_text(FAKE_CARGO)
cargo.chmod(0o755)
self.log = self.tmp / "cargo.log"
self.env = {**os.environ, "PATH": f"{bin_dir}:{os.environ['PATH']}", "CARGO_LOG": str(self.log)}

def write(self, path: str, text: str) -> None:
target = self.repo / path
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(text)

def git(self, *args: str) -> str:
return subprocess.run(
["git", "-C", str(self.repo), "-c", "user.name=t", "-c", "user.email=t@t",
"-c", "commit.gpgsign=false", *args],
check=True, capture_output=True, text=True,
).stdout

def change(self, path: str, text: str = "pub fn changed() {}\n") -> None:
self.write(path, text)
self.git("add", "-A")
self.git("commit", "-qm", "change")

def run_check(self, *args: str, fail: str | None = None) -> subprocess.CompletedProcess[str]:
env = dict(self.env)
if fail:
env["FAIL_PACKAGE"] = fail
return subprocess.run(
[sys.executable, str(SCRIPT), "--repo", str(self.repo), *args],
env=env, capture_output=True, text=True,
)

def checked(self) -> list[str]:
if not self.log.exists():
return []
manifests = []
for line in self.log.read_text().splitlines():
words = line.split()
self.assertEqual(words[:2], ["fmt", "--check"], line)
manifests.append(str(Path(words[words.index("--manifest-path") + 1]).relative_to(self.repo)))
return manifests

def test_a_changed_rust_file_checks_only_its_package(self) -> None:
self.change("cmux-tui/crates/a/src/deep/mod.rs")
result = self.run_check("--base", self.base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.checked(), ["cmux-tui/crates/a/Cargo.toml"])

def test_each_touched_package_is_checked_once(self) -> None:
self.write("cmux-tui/crates/a/src/lib.rs", "pub fn a2() {}\n")
self.write("cmux-tui/crates/a/src/deep/mod.rs", "pub fn d2() {}\n")
self.change("cmux-tui/crates/ffi/src/lib.rs")
result = self.run_check("--base", self.base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(sorted(self.checked()), ["cmux-tui/crates/a/Cargo.toml", "cmux-tui/crates/ffi/Cargo.toml"])

def test_an_unformatted_package_fails_and_names_it(self) -> None:
self.change("cmux-tui/crates/b/src/lib.rs")
result = self.run_check("--base", self.base, fail="b")
self.assertNotEqual(result.returncode, 0)
self.assertIn("cmux-tui/crates/b", result.stdout + result.stderr)
self.assertIn("cargo fmt", result.stdout + result.stderr)

def test_one_red_package_does_not_hide_the_others(self) -> None:
self.write("cmux-tui/crates/a/src/lib.rs", "pub fn a2() {}\n")
self.change("cmux-tui/crates/b/src/lib.rs")
result = self.run_check("--base", self.base, fail="a")
self.assertNotEqual(result.returncode, 0)
self.assertEqual(sorted(self.checked()), ["cmux-tui/crates/a/Cargo.toml", "cmux-tui/crates/b/Cargo.toml"])

def test_a_change_outside_rust_packages_checks_nothing(self) -> None:
self.change("README.md", "new readme\n")
result = self.run_check("--base", self.base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.checked(), [])

def test_a_rustfmt_config_change_checks_every_package(self) -> None:
self.change("cmux-tui/rustfmt.toml", "max_width = 90\n")
result = self.run_check("--base", self.base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(sorted(self.checked()), [
"cmux-tui/Cargo.toml", "cmux-tui/crates/a/Cargo.toml", "cmux-tui/crates/b/Cargo.toml",
"cmux-tui/crates/ffi/Cargo.toml"])

def test_no_base_checks_every_tracked_package(self) -> None:
for base in ([], ["--base", ""], ["--base", "0" * 40]):
with self.subTest(base=base):
self.log.unlink(missing_ok=True)
result = self.run_check(*base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
# Ignored build output is never a package to check.
self.assertEqual(sorted(self.checked()), [
"cmux-tui/Cargo.toml", "cmux-tui/crates/a/Cargo.toml", "cmux-tui/crates/b/Cargo.toml",
"cmux-tui/crates/ffi/Cargo.toml"])

def test_a_vendored_package_is_never_checked(self) -> None:
self.change("cmux-tui/vendor/up/src/lib.rs")
result = self.run_check("--base", self.base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.checked(), [])

def test_a_deleted_package_is_skipped(self) -> None:
self.git("rm", "-rq", "cmux-tui/crates/b")
self.git("commit", "-qm", "drop b")
result = self.run_check("--base", self.base)
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
self.assertEqual(self.checked(), [])


if __name__ == "__main__":
unittest.main()
118 changes: 118 additions & 0 deletions .github/workflows/cmux-next-base-autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: cmux-next base autofix

# After each cmux-next run on a feat-cmux-next push, repairs the base's
# mechanical reds in one commit on top of that push, so no pull request has to
# carry them: the generated files the Mac generated-files job rebuilt (its
# cmux-next-generated-patch artifact: action contracts, CI target graph) and
# cargo fmt in cmux-tui (scripts/cmux-next/base-autofix.sh). A frozen path
# (scripts/cmux-next/base-autofix-frozen.txt, including every workflow file)
# is reported and never written; missing cmux-tui tree inputs are reported
# (their fix edits cmux-tui-artifacts.yml); the app FFI pin stays with
# app-ffi-release.yml's app-ffi-repin pull request.
#
# Push identity: github-actions[bot] through this job's GITHUB_TOKEN
# (contents: write). No ruleset or branch protection applies to
# feat-cmux-next (2026-10-09). GITHUB_TOKEN cannot push workflow files, and its
# pushes start no workflow: the repair commit is verified by the next push's
# cmux-next run, which is enough for mechanical fixes, and the bot cannot loop.
#
# Dry run until the repo variable CMUX_NEXT_BASE_AUTOFIX is 1: it repairs and
# commits locally, and the summary shows what it would push.
on:
workflow_run:
workflows: [cmux-next]
types: [completed]
branches: [feat-cmux-next]

permissions: {}

concurrency:
group: cmux-next-base-autofix
cancel-in-progress: true

jobs:
autofix:
name: autofix feat-cmux-next
if: ${{ github.repository == 'manaflow-ai/cmux' && github.event.workflow_run.event == 'push' && github.event.workflow_run.path == '.github/workflows/cmux-next.yml' && github.event.workflow_run.head_branch == 'feat-cmux-next' && github.event.workflow_run.head_repository.full_name == github.repository }}
runs-on: ${{ github.repository_owner != 'manaflow-ai' && 'ubuntu-24.04' || 'blacksmith-4vcpu-ubuntu-2404' }} # ephemeral-required: holds a contents write token for feat-cmux-next; ephemeral Blacksmith VM
timeout-minutes: 20
permissions:
actions: read
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false

# Absent when the run regenerated nothing.
- name: Download the regenerated files
id: generated
continue-on-error: true
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
with:
name: cmux-next-generated-patch
path: ${{ runner.temp }}/generated
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}

- name: Set up pinned cmux-tui Rust
uses: ./.github/actions/setup-cmux-tui-rust

- name: Repair
run: |
set -euo pipefail
generated=()
[[ -d "$RUNNER_TEMP/generated" ]] && generated=(--generated "$RUNNER_TEMP/generated")
scripts/cmux-next/base-autofix.sh ${generated[@]+"${generated[@]}"} "$GITHUB_WORKSPACE" | tee "$RUNNER_TEMP/autofix.txt"
{ echo "### Base autofix at ${GITHUB_SHA:0:12}"; echo; sed 's/^/- /' "$RUNNER_TEMP/autofix.txt"; } >> "$GITHUB_STEP_SUMMARY"

- name: Commit the repairs
id: commit
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [[ -z "$(git status --porcelain)" ]]; then
echo "Nothing to repair at ${HEAD_SHA:0:12}." | tee -a "$GITHUB_STEP_SUMMARY"
echo "changed=false" >> "$GITHUB_OUTPUT"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add -A
git commit --quiet -m "chore(next): base autofix at ${HEAD_SHA:0:12}" \
-m "$(grep '^fixed: ' "$RUNNER_TEMP/autofix.txt" | sed 's/^fixed: /- /')" \
-m "scripts/cmux-next/base-autofix.sh: $RUN_URL"
git show --stat --oneline HEAD | sed -n '1,40p' >> "$GITHUB_STEP_SUMMARY"
echo "changed=true" >> "$GITHUB_OUTPUT"

- name: Push the repairs
if: steps.commit.outputs.changed == 'true'
env:
# The checkout keeps no credentials; the token is used only here.
TOKEN: ${{ github.token }}
ENABLED: ${{ vars.CMUX_NEXT_BASE_AUTOFIX == '1' }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
set -euo pipefail
if [[ "$ENABLED" != true ]]; then
echo "Dry run (CMUX_NEXT_BASE_AUTOFIX is not 1): would push $(git rev-parse --short HEAD) to feat-cmux-next." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
auth="$(printf 'x-access-token:%s' "$TOKEN" | base64 -w0)"
echo "::add-mask::$auth"
remote="https://github.com/$GITHUB_REPOSITORY.git"
with_auth=(env GIT_CONFIG_COUNT=1 GIT_CONFIG_KEY_0="http.https://github.com/.extraheader" GIT_CONFIG_VALUE_0="AUTHORIZATION: basic $auth")
current="$("${with_auth[@]}" git ls-remote "$remote" refs/heads/feat-cmux-next | awk '{print $1}')"
if [[ "$current" != "$HEAD_SHA" ]]; then
echo "feat-cmux-next moved to ${current:0:12} after ${HEAD_SHA:0:12}; the next run repairs it." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
# A plain push: it fails rather than overwrite a commit pushed meanwhile.
if ! "${with_auth[@]}" git push --quiet "$remote" HEAD:refs/heads/feat-cmux-next; then
echo "feat-cmux-next moved during the repair; the next run repairs it." | tee -a "$GITHUB_STEP_SUMMARY"
exit 0
fi
echo "Pushed $(git rev-parse --short HEAD) to feat-cmux-next." | tee -a "$GITHUB_STEP_SUMMARY"
Loading
Loading