From d6886294b5de2ed31fe87c46c7554166df1ec720 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 16 Sep 2026 02:24:20 -0700 Subject: [PATCH 1/6] test: catch undelivered TUI releases and unchecked wheel identity --- .../scripts/test_check_release_delivery.py | 68 +++++++++++++++++++ tests/test_tui_publish_workflow_security.py | 17 +++++ 2 files changed, 85 insertions(+) create mode 100644 cmux-tui/scripts/test_check_release_delivery.py diff --git a/cmux-tui/scripts/test_check_release_delivery.py b/cmux-tui/scripts/test_check_release_delivery.py new file mode 100644 index 000000000000..ab0a7b9e3266 --- /dev/null +++ b/cmux-tui/scripts/test_check_release_delivery.py @@ -0,0 +1,68 @@ +import datetime as dt +import unittest + +from check_release_delivery import assess_delivery, newest_stable_tag, wheel_names + + +class ReleaseDeliveryTests(unittest.TestCase): + def setUp(self): + self.now = dt.datetime(2026, 9, 16, tzinfo=dt.timezone.utc) + self.version = "0.13.1" + self.pypi = { + "info": {"version": self.version}, + "releases": { + self.version: [ + {"filename": name, "yanked": False} + for name in wheel_names(self.version) + ] + }, + } + self.npm = {"version": self.version} + + def assess(self, age=dt.timedelta(days=21)): + return assess_delivery( + self.version, self.now - age, self.pypi, self.npm, self.now, 7200 + ) + + def test_forgotten_publishing_approval_is_failure_not_release_success(self): + self.pypi = {"info": {"version": "0.12.1"}, "releases": {}} + result = self.assess() + self.assertEqual(result["status"], "failed") + self.assertIn("PyPI latest is 0.12.1", " ".join(result["problems"])) + + def test_build_and_approval_grace_does_not_claim_delivery(self): + self.pypi = {"info": {"version": "0.12.1"}, "releases": {}} + self.assertEqual(self.assess(dt.timedelta(minutes=20))["status"], "pending") + + def test_both_registries_and_all_platform_wheels_are_required(self): + self.assertEqual(self.assess()["status"], "complete") + self.pypi["releases"][self.version].pop() + result = self.assess() + self.assertEqual(result["status"], "failed") + self.assertIn("missing PyPI wheels", " ".join(result["problems"])) + + def test_yanked_wheel_does_not_count_as_available(self): + self.pypi["releases"][self.version][0]["yanked"] = True + self.assertEqual(self.assess()["status"], "failed") + + def test_pypi_success_does_not_hide_stale_npm(self): + self.npm["version"] = "0.12.1" + self.assertIn("npm latest is 0.12.1", " ".join(self.assess()["problems"])) + + def test_newer_registry_release_does_not_fail_older_tag_check(self): + self.pypi["info"]["version"] = "0.14.0" + self.npm["version"] = "0.14.0" + self.assertEqual(self.assess()["status"], "complete") + + def test_tag_selection_uses_numeric_versions_and_ignores_prereleases(self): + refs = [ + {"ref": "refs/tags/cmux-tui-v0.9.9"}, + {"ref": "refs/tags/cmux-tui-v0.13.1"}, + {"ref": "refs/tags/cmux-tui-v1.0.0-rc.1"}, + {"ref": "refs/tags/v5.0.0"}, + ] + self.assertEqual(newest_stable_tag(refs), refs[1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_tui_publish_workflow_security.py b/tests/test_tui_publish_workflow_security.py index 0f335f2ea804..57c41b741154 100644 --- a/tests/test_tui_publish_workflow_security.py +++ b/tests/test_tui_publish_workflow_security.py @@ -1598,6 +1598,23 @@ def test_stable_release_builds_and_tests_once_before_dispatching_publishers() -> assert "workflow_call:" not in workflow(name) +def test_tui_delivery_is_checked_independently_of_artifact_completion() -> None: + delivery = workflow("cmux-tui-release-delivery.yml") + triggers = workflow_triggers(delivery) + assert triggers["workflow_run"]["workflows"] == ["cmux-tui release binaries"] + assert "schedule" in triggers + assert "contents: read" in delivery + assert "contents: write" not in delivery + assert "check_release_delivery.py" in delivery + assert "head_sha" not in delivery + assert "persist-credentials: false" in delivery + build = workflow("cmux-tui-build-package.yml") + wheel_smoke = build.split("- name: Smoke verify PyPI wheels", 1)[1].split("- name:", 1)[0] + assert "/tmp/cmux-tui-wheel-smoke/bin/cmux remote-probe --json" in wheel_smoke + assert 'probe.get("build_identity")' in wheel_smoke + assert 'probe.get("distribution_version")' in wheel_smoke + + def test_relay_publisher_owns_the_cmux_relay_dist_tags_exclusively() -> None: # The chatmux machine relay publishes ONLY through the cmux-relay-v* tag # family. If the coordinated TUI publish or the nightly lane ever grows a From d7c9365a27eda1a0ac4f1e0967d09a021d94747d Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 16 Sep 2026 02:28:39 -0700 Subject: [PATCH 2/6] fix: verify TUI registry delivery and installed wheel identity --- .github/workflows/cmux-tui-build-package.yml | 16 +++ .../workflows/cmux-tui-release-delivery.yml | 35 ++++++ .github/workflows/cmux-tui-sdks.yml | 5 + cmux-tui/dist/RELEASING-TUI.md | 33 ++++- cmux-tui/scripts/check_release_delivery.py | 118 ++++++++++++++++++ tests/test_tui_publish_workflow_security.py | 28 ++++- 6 files changed, 232 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/cmux-tui-release-delivery.yml create mode 100644 cmux-tui/scripts/check_release_delivery.py diff --git a/.github/workflows/cmux-tui-build-package.yml b/.github/workflows/cmux-tui-build-package.yml index 11df08ec191b..36d79bb98c26 100644 --- a/.github/workflows/cmux-tui-build-package.yml +++ b/.github/workflows/cmux-tui-build-package.yml @@ -740,6 +740,22 @@ jobs: /tmp/cmux-tui-wheel-smoke/bin/python -m pip install --no-index --find-links dist/pypi-wheels cmux=="$PYPI_VERSION" /tmp/cmux-tui-wheel-smoke/bin/cmux --help >/tmp/cmux-help.txt grep -i usage /tmp/cmux-help.txt + CMUX_TUI_WHEEL_PROBE="$(/tmp/cmux-tui-wheel-smoke/bin/cmux remote-probe --json)" + CMUX_TUI_EXPECTED_BUILD_IDENTITY="$(git rev-parse HEAD)" + export CMUX_TUI_WHEEL_PROBE CMUX_TUI_EXPECTED_BUILD_IDENTITY + python3 - <<'PY' + import json + import os + + probe = json.loads(os.environ["CMUX_TUI_WHEEL_PROBE"]) + expected = { + "build_identity": os.environ["CMUX_TUI_EXPECTED_BUILD_IDENTITY"], + "distribution_version": os.environ["NPM_VERSION"], + } + for key, value in expected.items(): + if probe.get(key) != value: + raise SystemExit(f"installed wheel {key} {probe.get(key)!r} != {value!r}") + PY - name: Upload npm package directories if: inputs.package_npm diff --git a/.github/workflows/cmux-tui-release-delivery.yml b/.github/workflows/cmux-tui-release-delivery.yml new file mode 100644 index 000000000000..4972654f5706 --- /dev/null +++ b/.github/workflows/cmux-tui-release-delivery.yml @@ -0,0 +1,35 @@ +name: cmux-tui registry delivery + +on: + workflow_dispatch: + workflow_run: + workflows: [cmux-tui release binaries] + types: [completed] + schedule: + - cron: "17 */6 * * *" + +permissions: + contents: read + +concurrency: + group: cmux-tui-registry-delivery + cancel-in-progress: false + +jobs: + delivery: + name: verify registry delivery + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + # Always use the trusted default branch, never workflow_run's source. + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: main + persist-credentials: false + fetch-depth: 1 + - name: Check newest stable TUI release on PyPI and npm + env: + GH_TOKEN: ${{ github.token }} + # The post-build check may still be inside the two-hour delivery grace + # period. The scheduled check catches forgotten approvals after it. + run: python3 cmux-tui/scripts/check_release_delivery.py diff --git a/.github/workflows/cmux-tui-sdks.yml b/.github/workflows/cmux-tui-sdks.yml index 05c9f3926641..cbe9cc84b9c8 100644 --- a/.github/workflows/cmux-tui-sdks.yml +++ b/.github/workflows/cmux-tui-sdks.yml @@ -14,6 +14,7 @@ on: - ".github/workflows/cmux-tui-nightly.yml" - ".github/workflows/cmux-tui-release-cut.yml" - ".github/workflows/cmux-tui-release.yml" + - ".github/workflows/cmux-tui-release-delivery.yml" - ".github/workflows/cmux-tui-sdks.yml" - ".github/workflows/cmux-tui-spec.yml" - ".github/workflows/relay-publish-npm.yml" @@ -45,6 +46,7 @@ on: - ".github/workflows/cmux-tui-nightly.yml" - ".github/workflows/cmux-tui-release-cut.yml" - ".github/workflows/cmux-tui-release.yml" + - ".github/workflows/cmux-tui-release-delivery.yml" - ".github/workflows/cmux-tui-sdks.yml" - ".github/workflows/cmux-tui-spec.yml" - ".github/workflows/relay-publish-npm.yml" @@ -131,6 +133,9 @@ jobs: - name: Test PyPI post-upload reconciliation helper run: bash tests/test_tui_pypi_postpublish_reconcile.sh + - name: Test TUI registry delivery check + run: python3 cmux-tui/scripts/test_check_release_delivery.py + - name: Test TUI package artifact contract run: python3 tests/test_tui_package_contract.py diff --git a/cmux-tui/dist/RELEASING-TUI.md b/cmux-tui/dist/RELEASING-TUI.md index 2ca30958660a..1ae015fce0d5 100644 --- a/cmux-tui/dist/RELEASING-TUI.md +++ b/cmux-tui/dist/RELEASING-TUI.md @@ -141,7 +141,20 @@ extra build. ## Cutting a Stable Release -Use `.github/workflows/cmux-tui-release-cut.yml` from `main`. +Release tags require the maintainer identity under the repository's +`release tags: Lawrence only (tag-is-consent)` ruleset. The release-cut +workflow's default `GITHUB_TOKEN` cannot create them. Keep that protection: +create and push an annotated `cmux-tui-vX.Y.Z` tag on the selected `main` +revision using the maintainer's authenticated Git client. The tag push runs +`cmux-tui-release.yml` without publishing. Once it succeeds, dispatch +`tui-publish-npm.yml` and `tui-publish-pypi.yml` on that exact tag, passing the +version and successful artifact run ID (and `confirm_tui_cmux=true` for npm). +Complete both environment approvals, then verify registry delivery below. + +The older `.github/workflows/cmux-tui-release-cut.yml` flow can still recover +the coordinated dispatch when the tag already exists at the workflow's source +revision. Its tag-creation step requires a publishing identity permitted by +the ruleset and otherwise fails: - Select `patch`, `minor`, or `major`, or provide an explicit `X.Y.Z` version. - The workflow reads the latest reachable `cmux-tui-vX.Y.Z` tag, validates the @@ -159,6 +172,24 @@ Use `.github/workflows/cmux-tui-release-cut.yml` from `main`. - A manual `git push origin cmux-tui-vX.Y.Z` runs the artifact workflow without publishing. Use the release-cut workflow for a coordinated stable release. +A successful release-cut or artifact run does not mean users received the +release. Follow both publisher runs through their environment approvals and +post-upload verification. `cmux-tui registry delivery` independently compares +the newest stable tag with PyPI and npm, including all six non-yanked PyPI +wheels. It runs after artifact completion and every six hours, with a two-hour +grace period for compilation and approval. It never approves or publishes +anything. This separate check avoids the circular wait between the artifact +run and the publishers that require that run to have completed. + +For a local delivery check, run +`python3 cmux-tui/scripts/check_release_delivery.py --grace-seconds 0`. +After delivery, verify a clean `uvx --refresh cmux@latest` installation. An +existing `uv tool install cmux==X.Y.Z` takes precedence for plain `uvx cmux`; +replace that pin with `uv tool install --upgrade cmux` to update it. Session +state written by newer development versions can require a newer stable +package. Keep development runs in a dedicated `--session` instead of `main`; +never reset personal state to make an older package start. + ## Publishing Both registry workflows are manual-dispatch only. They require the ID of a diff --git a/cmux-tui/scripts/check_release_delivery.py b/cmux-tui/scripts/check_release_delivery.py new file mode 100644 index 000000000000..e772cc3ecf60 --- /dev/null +++ b/cmux-tui/scripts/check_release_delivery.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python3 +"""Check registry delivery independently of the TUI build/publisher dependency chain.""" + +from __future__ import annotations + +import argparse +import datetime as dt +import json +import os +import re +import urllib.request + + +TAG_PREFIX = "refs/tags/cmux-tui-v" +PLATFORMS = ( + "macosx_11_0_arm64", + "macosx_10_12_x86_64", + "manylinux_2_17_aarch64.manylinux2014_aarch64", + "manylinux_2_17_x86_64.manylinux2014_x86_64", + "musllinux_1_2_aarch64", + "musllinux_1_2_x86_64", +) + + +def version_tuple(value: str) -> tuple[int, ...]: + if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", value): + raise ValueError(f"expected stable X.Y.Z version, got {value!r}") + return tuple(map(int, value.split("."))) + + +def newest_stable_tag(refs: list[dict]) -> dict: + stable = [ + ref for ref in refs + if re.fullmatch(re.escape(TAG_PREFIX) + r"[0-9]+\.[0-9]+\.[0-9]+", ref["ref"]) + ] + if not stable: + raise ValueError("no stable cmux-tui release tags found") + return max(stable, key=lambda ref: version_tuple(ref["ref"][len(TAG_PREFIX):])) + + +def wheel_names(version: str) -> set[str]: + return {f"cmux-{version}-py3-none-{platform}.whl" for platform in PLATFORMS} + + +def assess_delivery( + version: str, + tagged_at: dt.datetime, + pypi: dict, + npm: dict, + now: dt.datetime, + grace_seconds: int, +) -> dict: + problems = [] + for name, published in (("PyPI", pypi["info"]["version"]), ("npm", npm["version"])): + if version_tuple(published) < version_tuple(version): + problems.append(f"{name} latest is {published}; expected at least {version}") + available = { + wheel["filename"] for wheel in pypi.get("releases", {}).get(version, []) + if not wheel.get("yanked", False) + } + missing = wheel_names(version) - available + if missing: + problems.append("missing PyPI wheels: " + ", ".join(sorted(missing))) + age = (now - tagged_at).total_seconds() + status = "complete" if not problems else "pending" if age < grace_seconds else "failed" + return {"version": version, "status": status, "problems": problems} + + +def fetch_json(url: str, token: str = ""): + headers = {"User-Agent": "cmux-tui-release-delivery/1", "Accept": "application/json"} + if token: + headers["Authorization"] = f"Bearer {token}" + request = urllib.request.Request(url, headers=headers) + with urllib.request.urlopen(request, timeout=30) as response: + return json.load(response) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--grace-seconds", type=int, default=7200) + args = parser.parse_args() + if args.grace_seconds < 0: + parser.error("--grace-seconds must be nonnegative") + repository = "manaflow-ai/cmux" + api = f"https://api.github.com/repos/{repository}" + token = os.environ.get("GH_TOKEN", "") + refs = fetch_json(f"{api}/git/matching-refs/tags/cmux-tui-v", token) + tag = newest_stable_tag(refs) + version = tag["ref"][len(TAG_PREFIX):] + obj = tag["object"] + if obj["type"] == "tag": + annotation = fetch_json(f"{api}/git/tags/{obj['sha']}", token) + tagged_at = annotation["tagger"]["date"] + else: + commit = fetch_json(f"{api}/commits/{obj['sha']}", token) + tagged_at = commit["commit"]["committer"]["date"] + result = assess_delivery( + version, + dt.datetime.fromisoformat(tagged_at.replace("Z", "+00:00")), + fetch_json("https://pypi.org/pypi/cmux/json"), + fetch_json("https://registry.npmjs.org/cmux/latest"), + dt.datetime.now(dt.timezone.utc), + args.grace_seconds, + ) + print(json.dumps(result, indent=2)) + if result["status"] == "complete": + return 0 + print( + "Inspect publisher failures or pending environment approvals at " + f"https://github.com/{repository}/actions/workflows/tui-publish-pypi.yml and " + f"https://github.com/{repository}/actions/workflows/tui-publish-npm.yml. " + "A successful artifact build is not a delivered release." + ) + return 1 if result["status"] == "failed" else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_tui_publish_workflow_security.py b/tests/test_tui_publish_workflow_security.py index 57c41b741154..0e7ae86592af 100644 --- a/tests/test_tui_publish_workflow_security.py +++ b/tests/test_tui_publish_workflow_security.py @@ -1611,8 +1611,32 @@ def test_tui_delivery_is_checked_independently_of_artifact_completion() -> None: build = workflow("cmux-tui-build-package.yml") wheel_smoke = build.split("- name: Smoke verify PyPI wheels", 1)[1].split("- name:", 1)[0] assert "/tmp/cmux-tui-wheel-smoke/bin/cmux remote-probe --json" in wheel_smoke - assert 'probe.get("build_identity")' in wheel_smoke - assert 'probe.get("distribution_version")' in wheel_smoke + assert '"build_identity": os.environ["CMUX_TUI_EXPECTED_BUILD_IDENTITY"]' in wheel_smoke + assert '"distribution_version": os.environ["NPM_VERSION"]' in wheel_smoke + + +def test_installed_pypi_wheel_probe_rejects_stale_executable() -> None: + document = yaml.safe_load(workflow("cmux-tui-build-package.yml")) + smoke = next( + step["run"] + for job in document["jobs"].values() + for step in job.get("steps", []) + if step.get("name") == "Smoke verify PyPI wheels" + ) + validation = smoke.rsplit("python3 - <<'PY'\n", 1)[1].split("\nPY", 1)[0] + expected = {"build_identity": "a" * 40, "distribution_version": "0.13.2"} + env = dict(os.environ, NPM_VERSION="0.13.2", CMUX_TUI_EXPECTED_BUILD_IDENTITY="a" * 40) + for key in (None, "build_identity", "distribution_version"): + probe = dict(expected) + if key: + probe[key] = "stale" + result = subprocess.run( + ["python3", "-c", validation], + env=dict(env, CMUX_TUI_WHEEL_PROBE=json.dumps(probe)), + capture_output=True, + text=True, + ) + assert (result.returncode == 0) == (key is None), result.stderr def test_relay_publisher_owns_the_cmux_relay_dist_tags_exclusively() -> None: From 57e5ff02d0c353e8eba1114d74951104cc21f2ee Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 16 Sep 2026 02:32:04 -0700 Subject: [PATCH 3/6] test: isolate native TUI publishing from separately deployed worker --- tests/test_tui_publish_workflow_security.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_tui_publish_workflow_security.py b/tests/test_tui_publish_workflow_security.py index 0e7ae86592af..e1e4dd969d8b 100644 --- a/tests/test_tui_publish_workflow_security.py +++ b/tests/test_tui_publish_workflow_security.py @@ -1639,6 +1639,15 @@ def test_installed_pypi_wheel_probe_rejects_stale_executable() -> None: assert (result.returncode == 0) == (key is None), result.stderr +def test_native_tui_releases_do_not_gate_on_separately_deployed_worker() -> None: + for name in ("cmux-tui-release.yml", "cmux-tui-nightly.yml"): + document = yaml.safe_load(workflow(name)) + assert document["jobs"]["build-package"]["with"]["build_cloudflare_relay"] is False + shared = workflow("cmux-tui-build-package.yml") + assert "if: inputs.build_cloudflare_relay" in shared + assert "npm audit --audit-level=high" in shared + + def test_relay_publisher_owns_the_cmux_relay_dist_tags_exclusively() -> None: # The chatmux machine relay publishes ONLY through the cmux-relay-v* tag # family. If the coordinated TUI publish or the nightly lane ever grows a From f18f629c83c0aa3021f37298b75bf3dd9829c147 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 16 Sep 2026 02:32:06 -0700 Subject: [PATCH 4/6] fix: scope TUI release verification to shipped native packages --- .github/workflows/cmux-tui-nightly.yml | 2 ++ .github/workflows/cmux-tui-release.yml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/.github/workflows/cmux-tui-nightly.yml b/.github/workflows/cmux-tui-nightly.yml index 758fce2a3aa2..93c6e44dfb2b 100644 --- a/.github/workflows/cmux-tui-nightly.yml +++ b/.github/workflows/cmux-tui-nightly.yml @@ -88,6 +88,8 @@ jobs: package_pypi: true include_windows: true attest_packages: true + # Match stable native packages; the Cloudflare Worker deploys separately. + build_cloudflare_relay: false # Pin the exact sha the version job resolved so every matrix leg builds # the same commit even if main advances mid-run. checkout_ref: ${{ needs.version.outputs.head_sha }} diff --git a/.github/workflows/cmux-tui-release.yml b/.github/workflows/cmux-tui-release.yml index 7629611ea927..4eff1ea41662 100644 --- a/.github/workflows/cmux-tui-release.yml +++ b/.github/workflows/cmux-tui-release.yml @@ -87,6 +87,9 @@ jobs: package_pypi: true include_windows: true attest_packages: true + # The Cloudflare Worker is deployed separately and is not shipped in + # these native packages. Keep its tests in its own verification lane. + build_cloudflare_relay: false dispatch-publishers: name: dispatch verified artifacts to registries From b35794ef70d53ca1ef547aeb5714e636d6e97141 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 16 Sep 2026 02:35:27 -0700 Subject: [PATCH 5/6] test: keep unpublished experimental Windows package out of default releases --- tests/test_tui_publish_workflow_security.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/test_tui_publish_workflow_security.py b/tests/test_tui_publish_workflow_security.py index e1e4dd969d8b..db1ad38f7dbe 100644 --- a/tests/test_tui_publish_workflow_security.py +++ b/tests/test_tui_publish_workflow_security.py @@ -1648,6 +1648,18 @@ def test_native_tui_releases_do_not_gate_on_separately_deployed_worker() -> None assert "npm audit --audit-level=high" in shared +def test_experimental_windows_is_opt_in_without_blocking_unix_publication() -> None: + for name in ("cmux-tui-release.yml", "cmux-tui-nightly.yml"): + document = yaml.load(workflow(name), Loader=yaml.BaseLoader) + assert document["on"]["workflow_dispatch"]["inputs"]["include_windows"]["default"] == "false" + assert document["jobs"]["build-package"]["with"]["include_windows"] == "${{ inputs.include_windows == true }}" + publisher = workflow("tui-publish-npm.yml") + assert 'if [[ -d dist/npm-packages/cmux-tui-win32-x64 ]]; then' in publisher + platform_block = publisher.split("packages=(", 1)[1].split(")", 1)[0] + assert "cmux-tui-win32-x64" not in platform_block + assert "packages+=(cmux-tui-win32-x64)" in publisher + + def test_relay_publisher_owns_the_cmux_relay_dist_tags_exclusively() -> None: # The chatmux machine relay publishes ONLY through the cmux-relay-v* tag # family. If the coordinated TUI publish or the nightly lane ever grows a From 8153aa445e4d825bca75d3f2f7629059708e0689 Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Wed, 16 Sep 2026 02:35:32 -0700 Subject: [PATCH 6/6] fix: make experimental Windows publishing opt-in --- .github/workflows/cmux-tui-nightly.yml | 8 +++++++- .github/workflows/cmux-tui-release.yml | 7 ++++++- .github/workflows/tui-publish-npm.yml | 10 ++++++++-- cmux-tui/dist/RELEASING-TUI.md | 5 ++++- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cmux-tui-nightly.yml b/.github/workflows/cmux-tui-nightly.yml index 93c6e44dfb2b..f080267d9004 100644 --- a/.github/workflows/cmux-tui-nightly.yml +++ b/.github/workflows/cmux-tui-nightly.yml @@ -4,6 +4,12 @@ on: # CI pause: the macOS Nightly and rolling iOS beta remain automatic; this # separate TUI package build remains available on demand. workflow_dispatch: + inputs: + include_windows: + description: "Include experimental Windows packages (requires registry publisher setup)" + required: true + default: false + type: boolean permissions: {} @@ -86,7 +92,7 @@ jobs: pypi_version: ${{ needs.version.outputs.pypi_version }} package_npm: true package_pypi: true - include_windows: true + include_windows: ${{ inputs.include_windows == true }} attest_packages: true # Match stable native packages; the Cloudflare Worker deploys separately. build_cloudflare_relay: false diff --git a/.github/workflows/cmux-tui-release.yml b/.github/workflows/cmux-tui-release.yml index 4eff1ea41662..188329dc11cf 100644 --- a/.github/workflows/cmux-tui-release.yml +++ b/.github/workflows/cmux-tui-release.yml @@ -10,6 +10,11 @@ on: description: "TUI package version to build, for example 0.1.0" required: true type: string + include_windows: + description: "Include experimental Windows packages (requires registry publisher setup)" + required: true + default: false + type: boolean publish_npm: description: "Publish the verified npm artifacts after the build" required: true @@ -85,7 +90,7 @@ jobs: version: ${{ needs.version.outputs.version }} package_npm: true package_pypi: true - include_windows: true + include_windows: ${{ inputs.include_windows == true }} attest_packages: true # The Cloudflare Worker is deployed separately and is not shipped in # these native packages. Keep its tests in its own verification lane. diff --git a/.github/workflows/tui-publish-npm.yml b/.github/workflows/tui-publish-npm.yml index 283a410db02d..02c809ef001c 100644 --- a/.github/workflows/tui-publish-npm.yml +++ b/.github/workflows/tui-publish-npm.yml @@ -183,10 +183,14 @@ jobs: - name: Revalidate packed npm contract run: | + contract_args=() + if [[ -d dist/npm-packages/cmux-tui-win32-x64 ]]; then + contract_args+=(--include-windows) + fi python3 cmux-tui/dist/scripts/validate_package_contract.py \ --npm-packages dist/npm-packages \ --version "${{ inputs.version }}" \ - --include-windows \ + "${contract_args[@]}" \ --install-npm-package cmux-tui-linux-x64 \ --install-npm-relay-package cmux-relay-linux-x64 @@ -228,8 +232,10 @@ jobs: cmux-tui-darwin-x64 cmux-tui-linux-x64 cmux-tui-linux-arm64 - cmux-tui-win32-x64 ) + if [[ -d dist/npm-packages/cmux-tui-win32-x64 ]]; then + packages+=(cmux-tui-win32-x64) + fi for package in "${packages[@]}"; do # A retried run must not fail on the packages an earlier attempt # already published at this exact version. diff --git a/cmux-tui/dist/RELEASING-TUI.md b/cmux-tui/dist/RELEASING-TUI.md index 1ae015fce0d5..f9eea8933358 100644 --- a/cmux-tui/dist/RELEASING-TUI.md +++ b/cmux-tui/dist/RELEASING-TUI.md @@ -29,7 +29,10 @@ scripts receive `--version`, so cutting a stable TUI release is just creating a on a separately published TUI package and must not silently degrade to a shell. - Windows TUI packages can still be built when the general release input - `include_windows` is enabled. The stable Rust machine-relay workflow excludes + `include_windows` is enabled. Stable and nightly releases default to Unix + until the experimental Windows package's registry publisher is configured; + the launcher advertises only the platforms included in its package set. + The stable Rust machine-relay workflow excludes Windows because `chatmux-relay` has no tested Windows PTY backend. Keep the chatmux Node relay as the Windows rollback lane until that backend exists. - PyPI `cmux`: platform wheels for `uvx cmux` / `pipx run cmux`.