From f50f70dc412e9f0faff67ec2a20ac593e6729cc7 Mon Sep 17 00:00:00 2001 From: tiffanysun1 Date: Thu, 12 Mar 2026 02:19:51 -0700 Subject: [PATCH 1/4] Run version memory guard in CI --- .github/workflows/ci.yml | 12 ++++++++++++ .github/workflows/nightly.yml | 7 +++++++ .github/workflows/release.yml | 8 ++++++++ 3 files changed, 27 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7315d8edec9e..bbaee7cb1c16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,6 +151,18 @@ jobs: fi fi + - name: Run CLI version memory guard regression + run: | + set -euo pipefail + + CLI_BIN="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -print -quit)" + if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then + echo "cmux CLI binary not found in DerivedData" >&2 + exit 1 + fi + + CMUX_CLI_BIN="$CLI_BIN" python3 tests/test_cli_version_memory_guard.py + tests-depot: # Never run Depot jobs for fork pull requests (avoid billing on external PRs). if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5b3e21a9c3e1..5c46f0a313b9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -192,6 +192,13 @@ jobs: [[ "$APP_ARCHS" == *arm64* && "$APP_ARCHS" == *x86_64* ]] [[ "$CLI_ARCHS" == *arm64* && "$CLI_ARCHS" == *x86_64* ]] + - name: Run CLI version memory guard regression + run: | + set -euo pipefail + CLI_BINARY="build-universal/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" + [ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; } + CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py + - name: Check whether build commit is still current main HEAD if: needs.decide.outputs.should_publish == 'true' id: current_head diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec935c6322f9..6a58f07f658e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -134,6 +134,14 @@ jobs: -clonedSourcePackagesDirPath .spm-cache \ CODE_SIGNING_ALLOWED=NO build + - name: Run CLI version memory guard regression + if: steps.guard_release_assets.outputs.skip_all != 'true' + run: | + set -euo pipefail + CLI_BINARY="build/Build/Products/Release/cmux.app/Contents/Resources/bin/cmux" + [ -x "$CLI_BINARY" ] || { echo "cmux CLI binary not found at $CLI_BINARY" >&2; exit 1; } + CMUX_CLI_BIN="$CLI_BINARY" python3 tests/test_cli_version_memory_guard.py + - name: Inject Sparkle keys into Info.plist if: steps.guard_release_assets.outputs.skip_all != 'true' run: | From f41a83177856a011d409ebc1f2924458773a3e1c Mon Sep 17 00:00:00 2001 From: tiffanysun1 Date: Thu, 12 Mar 2026 02:26:52 -0700 Subject: [PATCH 2/4] Stop version lookup at root --- CLI/cmux.swift | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 6ddd7437731c..2ad2a8b9a8e9 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -8172,7 +8172,7 @@ struct CMUXCLI { } let fileManager = FileManager.default - var current = executableURL.deletingLastPathComponent() + var current = executableURL.deletingLastPathComponent().standardizedFileURL while true { let projectFile = current.appendingPathComponent("GhosttyTabs.xcodeproj/project.pbxproj") @@ -8193,8 +8193,7 @@ struct CMUXCLI { } } - let parent = current.deletingLastPathComponent() - if parent.path == current.path { + guard let parent = parentSearchURL(for: current) else { break } current = parent @@ -8263,6 +8262,22 @@ struct CMUXCLI { return String(normalized.prefix(12)) } + // Foundation can walk past "/" into "/.." when repeatedly deleting path + // components, so stop once the canonical root is reached. + private func parentSearchURL(for url: URL) -> URL? { + let standardized = url.standardizedFileURL + let path = standardized.path + guard !path.isEmpty, path != "/" else { + return nil + } + + let parent = standardized.deletingLastPathComponent().standardizedFileURL + guard parent.path != path else { + return nil + } + return parent + } + private func candidateInfoPlistURLs() -> [URL] { guard let executableURL = resolvedExecutableURL() else { return [] @@ -8280,7 +8295,7 @@ struct CMUXCLI { candidates.append(url) } - var current = executableURL.deletingLastPathComponent() + var current = executableURL.deletingLastPathComponent().standardizedFileURL while true { if current.pathExtension == "app" { appendIfExisting(current.appendingPathComponent("Contents/Info.plist")) @@ -8299,8 +8314,7 @@ struct CMUXCLI { break } - let parent = current.deletingLastPathComponent() - if parent.path == current.path { + guard let parent = parentSearchURL(for: current) else { break } current = parent From 9bd22d5d98afdf020a0c75d0075d558123fd7a54 Mon Sep 17 00:00:00 2001 From: tiffanysun1 Date: Thu, 12 Mar 2026 02:34:04 -0700 Subject: [PATCH 3/4] Pick newest CLI binary in CI --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbaee7cb1c16..22933f4814c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,12 @@ jobs: run: | set -euo pipefail - CLI_BIN="$(find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -print -quit)" + CLI_BIN="$( + find "$HOME/Library/Developer/Xcode/DerivedData" -path "*/Build/Products/Debug/cmux" -exec stat -f '%m %N' {} \; \ + | sort -nr \ + | head -1 \ + | cut -d' ' -f2- + )" if [ -z "${CLI_BIN:-}" ] || [ ! -x "$CLI_BIN" ]; then echo "cmux CLI binary not found in DerivedData" >&2 exit 1 From 85f9ad6ee99431454d22e656feea5d83c62b2d3a Mon Sep 17 00:00:00 2001 From: tiffanysun1 Date: Thu, 12 Mar 2026 02:35:23 -0700 Subject: [PATCH 4/4] Polish version guard follow-ups --- CLI/cmux.swift | 4 ++-- tests/test_cli_version_memory_guard.py | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 2ad2a8b9a8e9..cc3ee89eaf10 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -8327,8 +8327,8 @@ struct CMUXCLI { } let searchRoots = [ - executableURL.deletingLastPathComponent(), - executableURL.deletingLastPathComponent().deletingLastPathComponent() + executableURL.deletingLastPathComponent().standardizedFileURL, + executableURL.deletingLastPathComponent().deletingLastPathComponent().standardizedFileURL ] for root in searchRoots { guard let entries = fileManager.enumerator( diff --git a/tests/test_cli_version_memory_guard.py b/tests/test_cli_version_memory_guard.py index 0a1c5bd16fce..6252ea5e4370 100644 --- a/tests/test_cli_version_memory_guard.py +++ b/tests/test_cli_version_memory_guard.py @@ -83,6 +83,8 @@ def build_fixture(root: str, cli_path: str) -> str: with open(os.path.join(contents_path, "Info.plist"), "wb") as handle: plistlib.dump(info, handle) + # Regular files are enough here because the fallback scan keys off the + # ".app" suffix before it ever tries to inspect bundle contents. for index in range(JUNK_APP_COUNT): open(os.path.join(resources_path, f"junk-{index:05d}.app"), "wb").close()