ci: refuse classic API changes into main without a stated reason

Merged
TE
3d ago
+470-0

Summary

Classic cmux (cmux.com and its API, plus the relays) deploys on its own when a commit lands on main. Leo, 2026-11-04: "apparently the api if we change it it'll auto deploy so we wanna make extra sure we dont change it for clasisc".

  • .github/classic-auto-deploy.json is the one list of what auto-deploys classic from main (Vercel manaflow/cmux for cmux.com, the iroh-v2 and Cloudflare relay Workers, plus the deploys that carry no customer API, listed but not guarded) and which paths each one guards. The guarded set is cmux.com's server side: web/app/api, web/app/v1, web/app/vm, web/app/handler, web/app/authorize, web/app/lib, web/services, web/db, web/orpc, web/openapi, build config, and the relay sources. Markdown and test files are excluded.
  • scripts/ci/classic_api_guard.py (run by .github/workflows/classic-api-guard.yml, which takes it from the base commit and reads the head only as a list of changed paths) fails a PR into main that changes a guarded path unless its description has a line Classic API change: <reason>. Into feat-cmux-next it only warns, because feat-cmux-next deploys nothing classic (its merge-base with main is 2026-10-19 and nothing syncs it into main). New NEXT Cloud backend code goes to manaflow-ai/cmux-platform.
  • Editing the description re-runs the check. Light Linux job on the LINUX_RUNNER fleet route, with forks on Blacksmith 4 vCPU as the other light guards are. Runs on pull_request_target (Leo, 2026-11-04: "Sure let's do that then"), so GitHub takes the workflow from main and a PR cannot edit the check into a no-op; it never checks out the head, reads the description and changed-file list through the API only, uses a read-only token and passes no secrets. Ephemeral CI_TRUSTED_RUNNER selector like the other pull_request_target guards. Into main only; feat-cmux-next keeps its own warn-only pull_request copy.

A follow-up PR into feat-cmux-next adds the same files there, so the warning also runs on PRs into that branch.

Testing

  • python3 tests/test_classic_api_guard.py: 15 tests, OK. They cover fail without a reason, pass with one, a placeholder reason, warn-only on feat-cmux-next, unguarded and excluded paths, the relays, glob semantics, CLI exit codes, the config's shape, and the workflow's pull_request_target trigger, base checkout and absence of any head checkout or secret. Registered in tests/test-execution.toml (linux-guard) and run from ci-guards.yml.
  • validate_test_execution_registry.py --base-sha origin/main, test_ci_warm_sources.py, test_ci_guard_workflow_structure.py, test_ci_runs_on_values.py, test_ci_self_hosted_guard.sh, test_ci_no_github_hosted_macos.py: all pass locally.

Observed / Not run

Observed: the tests above. Not run: a live PR event. The first PR the check sees after this lands shows it running.

Changelog

none

Agent session: agent session


Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Adds a CI check that blocks PRs into main when they change classic cmux API paths unless the description includes Classic API change: <reason>. This makes the auto-deploy risk explicit before those changes reach production.

  • The guard and deploy-path list come from the base commit; it reads PR details and changed paths through the API without checking out PR code.
  • Incomplete changed-file listings also require a reason, and the guard’s tests run in CI.

Summary by CodeRabbit

  • New Features
    • Pull requests targeting main or feat-cmux-next receive checks when opened, updated, reopened, edited, or marked ready for review.
    • Changes to guarded deployment paths require a specific explanation in the pull request description. Missing explanations block changes targeting main; on feat-cmux-next, they produce a warning.
    • Checks account for excluded paths and incomplete changed-file listings, helping identify potentially missed guarded changes. A valid explanation can allow a check to pass when the listing is incomplete.