Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
24 changes: 24 additions & 0 deletions scripts/ci/product_input_identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,28 @@
# Changing it changes product bytes, so it has to invalidate reuse.
PRODUCT_WORKER_PREFIXES = ("workers/cmux-paste-text/",)

# Developer and maintenance tooling that neither the Xcode build nor any macOS
# CI lane reads: no build phase, compile helper, bundled-resource script, or
# ci-macos.yml / test-e2e.yml step names them, and no native test executes
# them. agent-chat/ is the standalone chat server a user starts with cmux-chat;
# the app only connects to it. Each keeps its own Linux guard. Keep this exact:
# scripts/ also holds the build phases' helpers, which must stay product inputs.
NON_PRODUCT_TOOLING_PREFIXES = (
".claude/",
"agent-chat/",
"scripts/git-hooks/",
)
NON_PRODUCT_TOOLING = frozenset({
"scripts/benchmark-dev-fleet-warm-slots.py",
"scripts/check-pbxproj.sh",
"scripts/check-test-determinism.py",
"scripts/dev-fleet-warm-slot.py",
"scripts/install-git-hooks.sh",
"scripts/merge-xcstrings.py",
"scripts/normalize-pbxproj.py",
"scripts/prune_nightly_release_assets.py",
})

REQUIRED_PRODUCT_JOB_ENV_KEYS = frozenset({
"CMUX_CI_XCODE_APP",
"CMUX_CI_REQUIRED_MACOS_SDK_MAJOR",
Expand Down Expand Up @@ -116,6 +138,8 @@ def reaches_product(path: str) -> bool:
return True
if path.startswith("scripts/ci/"):
return False
if path in NON_PRODUCT_TOOLING or path.startswith(NON_PRODUCT_TOOLING_PREFIXES):
return False
if path.startswith((".github/", "tests/", "tests_v2/", "docs/", "design/", "plans/", "ios/", "web/", "workers/", "config/iroh/", "cmux-tui/", "cmux-browser/", "daemon/remote/")):
return False
if path in {".vercelignore", "vercel.json"}:
Expand Down
50 changes: 50 additions & 0 deletions tests/test_reuse_app_host_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,56 @@ def mutate_admission(old: str, new: str) -> str:
self.assertTrue(identity.reaches_product("scripts/ci/compile-app-host-test-product.sh"))
self.assertTrue(identity.reaches_product("cmuxTests/WorkspaceTests.swift"))

def test_developer_tooling_outside_the_build_does_not_reach_product(self):
"""Editing these must not force a compile: no build or macOS lane reads them."""
identity = reuse.product_inputs
tooling = (
".claude/commands/review.md",
"agent-chat/server.ts",
"agent-chat/src/components/Chat.tsx",
"scripts/git-hooks/pre-commit",
"scripts/benchmark-dev-fleet-warm-slots.py",
"scripts/check-pbxproj.sh",
"scripts/check-test-determinism.py",
"scripts/dev-fleet-warm-slot.py",
"scripts/install-git-hooks.sh",
"scripts/merge-xcstrings.py",
"scripts/normalize-pbxproj.py",
"scripts/prune_nightly_release_assets.py",
)
for path in tooling:
self.assertFalse(identity.reaches_product(path), path)

# Neighbours that the build does read stay product inputs.
for path in (
"scripts/build-app-bundled-resources.sh",
"scripts/build-plain-text-paste-worker.sh",
"scripts/setup.sh",
"skills/cmux-cua/SKILL.md",
".gitattributes",
):
self.assertTrue(identity.reaches_product(path), path)

# Drift guard: if the Xcode project, the compile script, or either
# product workflow starts naming one of these, it is a build input again.
root = Path(__file__).resolve().parents[1]
readers = {
name: (root / name).read_text()
for name in (
"cmux.xcodeproj/project.pbxproj",
"scripts/ci/compile-app-host-test-product.sh",
"scripts/build-app-bundled-resources.sh",
".github/workflows/ci-macos.yml",
".github/workflows/test-e2e.yml",
)
}
# Check the module's own lists, not the samples above, so a reader
# naming any file under an excluded prefix fails here too.
needles = sorted(identity.NON_PRODUCT_TOOLING) + list(identity.NON_PRODUCT_TOOLING_PREFIXES)
for needle in needles:
for name, text in readers.items():
self.assertNotIn(needle, text, f"{name} reads {needle}")

def test_product_identity_binds_the_e2e_build_recipe(self):
identity = reuse.product_inputs
root = Path(__file__).resolve().parents[1]
Expand Down