From 285f06cad3710943567931e25665ffa9d51df618 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 28 Apr 2026 02:33:13 -0700 Subject: [PATCH 1/3] ci: add Swift warning budget --- .github/swift-warning-budget.tsv | 108 +++++++++++++++ .github/workflows/ci.yml | 8 +- scripts/swift_warning_budget.py | 190 ++++++++++++++++++++++++++ tests/test_ci_swift_warning_budget.sh | 75 ++++++++++ 4 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 .github/swift-warning-budget.tsv create mode 100755 scripts/swift_warning_budget.py create mode 100755 tests/test_ci_swift_warning_budget.sh diff --git a/.github/swift-warning-budget.tsv b/.github/swift-warning-budget.tsv new file mode 100644 index 000000000000..3f1ab771eedc --- /dev/null +++ b/.github/swift-warning-budget.tsv @@ -0,0 +1,108 @@ +# cmux-owned Swift warning budget. +# Format: countrelative pathwarning message +# Reduce counts when warnings are fixed. CI fails if new warnings exceed this budget. +1 CLI/cmux.swift literal value is already handled by previous pattern; consider removing it +2 CLI/cmux.swift variable 'groups' was never mutated; consider changing to 'let' constant +1 Sources/App/TerminalDirectoryOpenSupport.swift 'fullPath(forApplication:)' was deprecated in macOS 11.0: Use -[NSWorkspace URLForApplicationWithBundleIdentifier:] instead. +1 Sources/AppDelegate.swift 'accessibilityAttributeValue' was deprecated in macOS 10.10: Use the NSAccessibility protocol methods instead (see NSAccessibilityProtocols.h) +5 Sources/AppDelegate.swift 'activateIgnoringOtherApps' was deprecated in macOS 14.0: ignoringOtherApps is deprecated in macOS 14 and will have no effect. +1 Sources/AppDelegate.swift add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'Dispatch' +1 Sources/AppDelegate.swift add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'ObjectiveC' +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'beginSuppressWebViewFocusForAddressBar()' in a synchronous nonisolated context +3 Sources/AppDelegate.swift call to main actor-isolated instance method 'browserPanel(for:)' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'browserPanelOwning' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'clearConfiguredShortcutChordState()' in a synchronous nonisolated context +3 Sources/AppDelegate.swift call to main actor-isolated instance method 'endSuppressWebViewFocusForAddressBar()' in a synchronous nonisolated context +2 Sources/AppDelegate.swift call to main actor-isolated instance method 'recordGotoSplitUITestActiveElement(panelId:keyPrefix:)' in a synchronous nonisolated context +2 Sources/AppDelegate.swift call to main actor-isolated instance method 'recordGotoSplitUITestWebViewFocus(panelId:key:)' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'recordJumpUnreadFocusIfExpected(tabId:surfaceId:)' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'refreshConfiguredShortcutChordActions()' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'refreshGhosttyGotoSplitShortcuts()' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'scheduleSplitButtonTooltipRefreshAcrossWorkspaces()' in a synchronous nonisolated context +2 Sources/AppDelegate.swift call to main actor-isolated instance method 'shouldPreserveBrowserAddressBarTracking(for:)' in a synchronous nonisolated context +4 Sources/AppDelegate.swift call to main actor-isolated instance method 'stopBrowserOmnibarSelectionRepeat()' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated instance method 'unregisterMainWindow' in a synchronous nonisolated context +4 Sources/AppDelegate.swift call to main actor-isolated local function 'attemptFocus()' in a synchronous nonisolated context +2 Sources/AppDelegate.swift call to main actor-isolated local function 'attemptResolve()' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated local function 'finishIfReady()' in a synchronous nonisolated context +1 Sources/AppDelegate.swift call to main actor-isolated local function 'publishCurrentState(isTimedOut:)' in a synchronous nonisolated context +2 Sources/AppDelegate.swift call to main actor-isolated local function 'recordFocusedState()' in a synchronous nonisolated context +1 Sources/AppDelegate.swift capture of 'observer' with non-sendable type '(any NSObjectProtocol)?' in a '@Sendable' closure +1 Sources/AppDelegate.swift capture of 'timeoutWorkItem' with non-sendable type 'DispatchWorkItem?' in a '@Sendable' closure +1 Sources/AppDelegate.swift conformance of 'AppDelegate' to protocol 'UNUserNotificationCenterDelegate' crosses into main actor-isolated code and can cause data races +2 Sources/AppDelegate.swift converting non-sendable function value to '@MainActor @Sendable @convention(block) () -> Void' may introduce data races +1 Sources/AppDelegate.swift converting non-sendable function value to '@Sendable @convention(block) () -> Void' may introduce data races +1 Sources/AppDelegate.swift main actor-isolated instance method 'userNotificationCenter(_:didReceive:withCompletionHandler:)' cannot be used to satisfy nonisolated requirement from protocol 'UNUserNotificationCenterDelegate' +1 Sources/AppDelegate.swift main actor-isolated instance method 'userNotificationCenter(_:willPresent:withCompletionHandler:)' cannot be used to satisfy nonisolated requirement from protocol 'UNUserNotificationCenterDelegate' +4 Sources/AppDelegate.swift main actor-isolated property 'browserAddressBarFocusedPanelId' can not be mutated from a Sendable closure +3 Sources/AppDelegate.swift main actor-isolated property 'browserAddressBarFocusedPanelId' can not be referenced from a Sendable closure +1 Sources/AppDelegate.swift main actor-isolated property 'webView' can not be referenced from a Sendable closure +1 Sources/AppDelegate.swift value 'browserPanel' was defined but never used; consider replacing with boolean test +2 Sources/AppDelegate.swift value 'self' was defined but never used; consider replacing with boolean test +1 Sources/Auth/AuthManager.swift main actor-isolated static property 'logTimestampFormatter' can not be referenced from a nonisolated context +24 Sources/ContentView.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +1 Sources/ContentView.swift treating a forced downcast to 'CGColor' as optional will never produce 'nil' +1 Sources/ContentView.swift use of protocol 'Panel' as a type must be written 'any Panel'; this will be an error in a future Swift language mode +1 Sources/Feed/FeedPanelView.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +1 Sources/FileExplorerView.swift 'icon(forFileType:)' was deprecated in macOS 12.0: Use -[NSWorkspace iconForContentType:] instead. +1 Sources/GhosttyTerminalView.swift 'unsafeBitCast' from '@convention(c) (UnsafeMutableRawPointer?, ghostty_clipboard_e, UnsafeMutableRawPointer?) -> Bool' to 'ghostty_runtime_read_clipboard_cb' (aka '@convention(c) (Optional, ghostty_clipboard_e, Optional) -> Bool') is unnecessary and can be removed +1 Sources/GhosttyTerminalView.swift immutable value 'surface' was never used; consider replacing with '_' or removing it +1 Sources/GhosttyTerminalView.swift left side of nil coalescing operator '??' has non-optional type '[NSPasteboard.PasteboardType]', so the right side is never used +1 Sources/GhosttyTerminalView.swift left side of nil coalescing operator '??' has non-optional type 'ghostty_input_mods_e', so the right side is never used +1 Sources/KeyboardShortcutSettings.swift 'activateIgnoringOtherApps' was deprecated in macOS 14.0: ignoringOtherApps is deprecated in macOS 14 and will have no effect. +1 Sources/NotificationsPage.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +1 Sources/Panels/BrowserPanel.swift '@preconcurrency' on conformance to 'NSWindowDelegate' has no effect +3 Sources/Panels/BrowserPanel.swift 'WKProcessPool' was deprecated in macOS 12.0: Creating and using multiple instances of WKProcessPool no longer has any effect. +2 Sources/Panels/BrowserPanel.swift 'processPool' was deprecated in macOS 12.0: Creating and using multiple instances of WKProcessPool no longer has any effect. +2 Sources/Panels/BrowserPanel.swift converting non-sendable function value to '@MainActor @Sendable @convention(block) () -> Void' may introduce data races +2 Sources/Panels/BrowserPanel.swift main actor-isolated property 'effectiveAppearance' can not be referenced from a nonisolated context +1 Sources/Panels/BrowserPanel.swift main actor-isolated static property 'shared' can not be referenced from a nonisolated context +1 Sources/Panels/BrowserPanel.swift main actor-isolated static property 'sharedProcessPool' can not be referenced from a nonisolated context +2 Sources/Panels/BrowserPanel.swift main actor-isolated var 'NSApp' can not be referenced from a nonisolated context +9 Sources/Panels/BrowserPanelView.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +1 Sources/Panels/MarkdownPanelView.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +1 Sources/Panels/ReactGrab.swift consider using asynchronous alternative function +5 Sources/SessionIndexStore.swift expression is 'async' but is not marked with 'await' +1 Sources/SessionIndexStore.swift instance method 'makeIterator' is unavailable from asynchronous contexts +1 Sources/SessionIndexStore.swift main actor-isolated static property 'headByteCap' can not be referenced from a nonisolated context +2 Sources/SessionIndexStore.swift main actor-isolated static property 'headByteCap' cannot be accessed from outside of the actor +2 Sources/SessionIndexStore.swift main actor-isolated static property 'searchMaxFiles' cannot be accessed from outside of the actor +1 Sources/SessionIndexStore.swift main actor-isolated static property 'tailByteCap' cannot be accessed from outside of the actor +1 Sources/SessionIndexStore.swift reference to captured var 'self' in concurrently-executing code +2 Sources/SessionIndexView.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +1 Sources/Settings/ConfigSettingsView.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. +17 Sources/TabManager.swift result of call to 'closePanel(_:force:)' is unused +1 Sources/TerminalController.swift 'CGWindowListCreateImage' was deprecated in macOS 14.0: Please use ScreenCaptureKit instead. +2 Sources/TerminalController.swift 'activateIgnoringOtherApps' was deprecated in macOS 14.0: ignoringOtherApps is deprecated in macOS 14 and will have no effect. +1 Sources/TerminalController.swift 'observer' mutated after capture by sendable closure +1 Sources/TerminalController.swift add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'ObjectiveC' +1 Sources/TerminalController.swift call to main actor-isolated instance method 'handleClient(_:peerPid:)' in a synchronous nonisolated context +1 Sources/TerminalController.swift capture of 'observer' with non-sendable type '(any NSObjectProtocol)?' in a '@Sendable' closure +1 Sources/TerminalController.swift capture of 'terminalSurface' with non-sendable type 'TerminalSurface' in a '@Sendable' closure +2 Sources/TerminalController.swift conditional cast from 'NSResponder' to 'NSResponder' always succeeds +2 Sources/TerminalController.swift result of call to 'closePanel(_:force:)' is unused +1 Sources/TerminalNotificationStore.swift add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'UserNotifications' +2 Sources/TerminalNotificationStore.swift call to main actor-isolated instance method 'logAuthorization' in a synchronous nonisolated context +1 Sources/TerminalNotificationStore.swift call to main actor-isolated instance method 'refreshDockBadge()' in a synchronous nonisolated context +2 Sources/TerminalNotificationStore.swift capture of 'content' with non-sendable type 'UNMutableNotificationContent' in a '@Sendable' closure +1 Sources/Update/UpdateDriver.swift add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'Sparkle' +1 Sources/Update/UpdateDriver.swift sendability of function types in instance method 'show(_:reply:)' does not match requirement in protocol 'SPUUserDriver' +1 Sources/Update/UpdateDriver.swift sendability of function types in instance method 'showReady(toInstallAndRelaunch:)' does not match requirement in protocol 'SPUUserDriver' +1 Sources/Update/UpdateDriver.swift sendability of function types in instance method 'showUpdateFound(with:state:reply:)' does not match requirement in protocol 'SPUUserDriver' +2 Sources/Update/UpdateLogStore.swift result of 'try?' is unused +1 Sources/Update/UpdateTestSupport.swift 'init(dictionary:)' is deprecated: Properties that depend on the system or application version are not supported when used with this initializer. The designated initializer is available in SUAppcastItem+Private.h. Please first explore other APIs or contact us to describe your use case. +1 Sources/Workspace.swift conformance of 'Workspace' to protocol 'BonsplitDelegate' crosses into main actor-isolated code and can cause data races +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didChangeGeometry:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didClosePane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didCloseTab:fromPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didFocusPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didMoveTab:fromPane:toPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didRequestCustomAction:inPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didRequestNewTab:inPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didRequestTabContextAction:for:inPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didSelectTab:inPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:didSplitPane:newPane:orientation:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:shouldClosePane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift main actor-isolated instance method 'splitTabBar(_:shouldCloseTab:inPane:)' cannot be used to satisfy nonisolated requirement from protocol 'BonsplitDelegate' +1 Sources/Workspace.swift switch must be exhaustive +22 Sources/cmuxApp.swift 'onChange(of:perform:)' was deprecated in macOS 14.0: Use `onChange` with a two or zero parameter action closure instead. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf3108c04e66..bfd58ee9b7bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,6 +38,9 @@ jobs: - name: Validate current GhosttyKit checksum pin run: ./tests/test_ci_ghosttykit_checksum_present.sh + - name: Validate Swift warning budget guard + run: ./tests/test_ci_swift_warning_budget.sh + remote-daemon-tests: runs-on: ubuntu-latest steps: @@ -405,7 +408,10 @@ jobs: xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux -configuration Debug \ -clonedSourcePackagesDirPath "$SOURCE_PACKAGES_DIR" \ -disableAutomaticPackageResolution \ - -destination "platform=macOS" build + -destination "platform=macOS" build 2>&1 | tee /tmp/cmux-build-output.txt + + - name: Validate Swift warning budget + run: python3 scripts/swift_warning_budget.py --log /tmp/cmux-build-output.txt - name: Create virtual display run: | diff --git a/scripts/swift_warning_budget.py b/scripts/swift_warning_budget.py new file mode 100755 index 000000000000..470955d2fcd9 --- /dev/null +++ b/scripts/swift_warning_budget.py @@ -0,0 +1,190 @@ +#!/usr/bin/env python3 +"""Check cmux-owned Swift warnings against a checked-in budget.""" + +from __future__ import annotations + +import argparse +import collections +import pathlib +import re +import sys + + +OWNED_ROOTS = ("Sources", "CLI", "Packages", "cmuxTests", "cmuxUITests") +IGNORED_PATH_PARTS = ("/vendor/", "/ghostty/", "/homebrew-cmux/", "/SourcePackages/") +WARNING_RE = re.compile( + r"^(?P.*?(?:Sources|CLI|Packages|cmuxTests|cmuxUITests)/[^:]+):" + r"(?P\d+):(?P\d+): warning: (?P.*)$" +) +ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") +GITHUB_LOG_PREFIX_RE = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z\s+") +SWIFT6_ERROR_SUFFIX = "; this is an error in the Swift 6 language mode" + + +WarningBudget = collections.Counter[tuple[str, str]] + + +def relative_owned_path(raw_path: str) -> str | None: + path = raw_path.strip() + normalized = "/" + path.lstrip("/") + if any(part in normalized for part in IGNORED_PATH_PARTS): + return None + + for root in OWNED_ROOTS: + marker = f"{root}/" + index = path.find(marker) + if index >= 0: + return path[index:] + return None + + +def normalize_message(message: str) -> str: + normalized = " ".join(message.strip().split()) + while normalized.endswith(SWIFT6_ERROR_SUFFIX): + normalized = normalized[: -len(SWIFT6_ERROR_SUFFIX)].rstrip() + normalized = normalized.replace("non-Sendable", "non-sendable") + normalized = normalized.replace("Non-Sendable", "non-sendable") + normalized = normalized.replace( + "@preconcurrency attribute on conformance", + "'@preconcurrency' on conformance", + ) + return normalized + + +def collect_warnings(log_path: pathlib.Path) -> WarningBudget: + exact_seen: set[tuple[str, str, str, str]] = set() + budget: WarningBudget = collections.Counter() + + with log_path.open("r", encoding="utf-8", errors="replace") as handle: + for raw_line in handle: + line = ANSI_RE.sub("", raw_line.rstrip("\n")) + line = GITHUB_LOG_PREFIX_RE.sub("", line) + match = WARNING_RE.match(line) + if not match: + continue + + rel_path = relative_owned_path(match.group("path")) + if rel_path is None: + continue + + message = normalize_message(match.group("message")) + exact_key = (rel_path, match.group("line"), match.group("column"), message) + if exact_key in exact_seen: + continue + exact_seen.add(exact_key) + budget[(rel_path, message)] += 1 + + return budget + + +def load_budget(path: pathlib.Path) -> WarningBudget: + budget: WarningBudget = collections.Counter() + with path.open("r", encoding="utf-8") as handle: + for line_number, raw_line in enumerate(handle, start=1): + line = raw_line.rstrip("\n") + if not line or line.startswith("#"): + continue + + parts = line.split("\t", 2) + if len(parts) != 3: + raise ValueError(f"{path}:{line_number}: expected countpathmessage") + + count_text, rel_path, message = parts + try: + count = int(count_text) + except ValueError as exc: + raise ValueError(f"{path}:{line_number}: invalid count {count_text!r}") from exc + + if count < 0: + raise ValueError(f"{path}:{line_number}: count must be non-negative") + budget[(rel_path, normalize_message(message))] += count + return budget + + +def write_budget(path: pathlib.Path, budget: WarningBudget) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + handle.write("# cmux-owned Swift warning budget.\n") + handle.write("# Format: countrelative pathwarning message\n") + handle.write("# Reduce counts when warnings are fixed. CI fails if new warnings exceed this budget.\n") + for (rel_path, message), count in sorted(budget.items()): + handle.write(f"{count}\t{rel_path}\t{message}\n") + + +def print_budget_summary(label: str, budget: WarningBudget) -> None: + total = sum(budget.values()) + print(f"{label}: {total} warning(s) across {len(budget)} bucket(s)") + + +def compare_budget(actual: WarningBudget, allowed: WarningBudget) -> int: + failures: list[tuple[str, str, int, int]] = [] + reductions: list[tuple[str, str, int, int]] = [] + + for key in sorted(set(actual) | set(allowed)): + actual_count = actual.get(key, 0) + allowed_count = allowed.get(key, 0) + rel_path, message = key + if actual_count > allowed_count: + failures.append((rel_path, message, actual_count, allowed_count)) + elif actual_count < allowed_count: + reductions.append((rel_path, message, actual_count, allowed_count)) + + if failures: + print("Swift warning budget exceeded.") + print("") + for rel_path, message, actual_count, allowed_count in failures: + delta = actual_count - allowed_count + print(f"+{delta} {rel_path}: {message}") + print(f" actual={actual_count} budget={allowed_count}") + print("") + print("Fix the new warnings or refresh the budget only when accepting known debt.") + return 1 + + print("Swift warning budget respected.") + if reductions: + print("") + print("Budget can be reduced:") + for rel_path, message, actual_count, allowed_count in reductions[:20]: + delta = allowed_count - actual_count + print(f"-{delta} {rel_path}: {message}") + print(f" actual={actual_count} budget={allowed_count}") + if len(reductions) > 20: + print(f"... {len(reductions) - 20} more reduction(s)") + return 0 + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--log", required=True, type=pathlib.Path, help="xcodebuild output log") + parser.add_argument( + "--budget", + default=pathlib.Path(".github/swift-warning-budget.tsv"), + type=pathlib.Path, + help="checked-in warning budget file", + ) + parser.add_argument( + "--write-budget", + action="store_true", + help="write the current warnings as the budget instead of checking", + ) + args = parser.parse_args(argv) + + actual = collect_warnings(args.log) + print_budget_summary("Actual cmux-owned Swift warnings", actual) + + if args.write_budget: + write_budget(args.budget, actual) + print(f"Wrote {args.budget}") + return 0 + + if not args.budget.exists(): + print(f"Missing warning budget: {args.budget}", file=sys.stderr) + return 2 + + allowed = load_budget(args.budget) + print_budget_summary("Allowed warning budget", allowed) + return compare_budget(actual, allowed) + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tests/test_ci_swift_warning_budget.sh b/tests/test_ci_swift_warning_budget.sh new file mode 100755 index 000000000000..9d26f83aa78a --- /dev/null +++ b/tests/test_ci_swift_warning_budget.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +set -euo pipefail + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT_DIR" + +LOG="$TMP_DIR/build.log" +BUDGET="$TMP_DIR/budget.tsv" +CI_FILE="$ROOT_DIR/.github/workflows/ci.yml" + +REQUIRED_PATTERNS=( + "Validate Swift warning budget guard" + "tee /tmp/cmux-build-output.txt" + "scripts/swift_warning_budget.py --log /tmp/cmux-build-output.txt" +) + +for pattern in "${REQUIRED_PATTERNS[@]}"; do + if ! grep -Fq "$pattern" "$CI_FILE"; then + echo "missing Swift warning budget CI pattern: $pattern" >&2 + exit 1 + fi +done + +cat >"$LOG" <<'LOG' +/Users/example/cmux/Sources/AppDelegate.swift:10:1: warning: add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'ObjectiveC' +/Users/example/cmux/Sources/AppDelegate.swift:10:1: warning: add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'ObjectiveC' +/Users/example/cmux/Sources/AppDelegate.swift:42:9: warning: result of call to 'closePanel(_:force:)' is unused +2026-04-28T09:40:13.8874600Z /Users/example/cmux/Sources/AppDelegate.swift:44:9: warning: capture of 'observer' with non-Sendable type '(any NSObjectProtocol)?' in a '@Sendable' closure; this is an error in the Swift 6 language mode +2026-04-28T09:40:13.8874610Z /Users/example/cmux/Sources/AppDelegate.swift:44:9: warning: capture of 'observer' with non-sendable type '(any NSObjectProtocol)?' in a '@Sendable' closure +/Users/example/cmux/vendor/bonsplit/Sources/Bonsplit/Public/BonsplitView.swift:1:1: warning: ignored vendor warning +/tmp/cmux/SourcePackages/checkouts/posthog-ios/PostHog/PostHogSDK.swift:1:1: warning: ignored package warning +warning: Run script build phase 'Run Script' will be run during every build +LOG + +python3 scripts/swift_warning_budget.py --log "$LOG" --budget "$BUDGET" --write-budget + +if ! grep -q $'1\tSources/AppDelegate.swift\tadd' "$BUDGET"; then + echo "expected AppDelegate preconcurrency warning budget entry" >&2 + exit 1 +fi + +if ! grep -Fq $'1\tSources/AppDelegate.swift\tcapture of '\''observer'\'' with non-sendable type '\''(any NSObjectProtocol)?'\'' in a '\''@Sendable'\'' closure' "$BUDGET"; then + echo "expected normalized Sendable warning budget entry" >&2 + exit 1 +fi + +if grep -q 'vendor/bonsplit' "$BUDGET"; then + echo "vendor warning should not be included" >&2 + exit 1 +fi + +if grep -q 'SourcePackages' "$BUDGET"; then + echo "package warning should not be included" >&2 + exit 1 +fi + +python3 scripts/swift_warning_budget.py --log "$LOG" --budget "$BUDGET" + +cat >>"$LOG" <<'LOG' +/Users/example/cmux/Sources/AppDelegate.swift:43:9: warning: result of call to 'closePanel(_:force:)' is unused +LOG + +if python3 scripts/swift_warning_budget.py --log "$LOG" --budget "$BUDGET" >"$TMP_DIR/fail.out" 2>&1; then + echo "expected warning budget failure" >&2 + exit 1 +fi + +if ! grep -q 'Swift warning budget exceeded' "$TMP_DIR/fail.out"; then + echo "expected budget failure output" >&2 + cat "$TMP_DIR/fail.out" >&2 + exit 1 +fi From 039e6453c30636619637df5c7a837cfb8ce9cfd8 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 28 Apr 2026 03:02:58 -0700 Subject: [PATCH 2/3] fix: harden Swift warning budget parser --- scripts/swift_warning_budget.py | 61 ++++++++++++++++++++++----- tests/test_ci_swift_warning_budget.sh | 45 ++++++++++++++------ 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/scripts/swift_warning_budget.py b/scripts/swift_warning_budget.py index 470955d2fcd9..944b94fa0d16 100755 --- a/scripts/swift_warning_budget.py +++ b/scripts/swift_warning_budget.py @@ -11,9 +11,15 @@ OWNED_ROOTS = ("Sources", "CLI", "Packages", "cmuxTests", "cmuxUITests") -IGNORED_PATH_PARTS = ("/vendor/", "/ghostty/", "/homebrew-cmux/", "/SourcePackages/") +IGNORED_PATH_PARTS = ( + "/vendor/", + "/ghostty/", + "/homebrew-cmux/", + "/SourcePackages/", + "/.ci-source-packages/", +) WARNING_RE = re.compile( - r"^(?P.*?(?:Sources|CLI|Packages|cmuxTests|cmuxUITests)/[^:]+):" + r"^(?P.+):" r"(?P\d+):(?P\d+): warning: (?P.*)$" ) ANSI_RE = re.compile(r"\x1b\[[0-9;]*m") @@ -24,17 +30,52 @@ WarningBudget = collections.Counter[tuple[str, str]] -def relative_owned_path(raw_path: str) -> str | None: - path = raw_path.strip() +def is_ignored_path(path: str) -> bool: normalized = "/" + path.lstrip("/") - if any(part in normalized for part in IGNORED_PATH_PARTS): + return any(part in normalized for part in IGNORED_PATH_PARTS) + + +def is_owned_relative_path(path: str) -> bool: + return any(path == root or path.startswith(f"{root}/") for root in OWNED_ROOTS) + + +def fallback_owned_path(path: str) -> str | None: + parts = pathlib.PurePosixPath(path).parts + candidates = ["/".join(parts[index:]) for index, part in enumerate(parts) if part in OWNED_ROOTS] + packages_candidates = [candidate for candidate in candidates if candidate.startswith("Packages/")] + if packages_candidates: + return packages_candidates[-1] + if candidates: + return candidates[-1] + return None + + +def relative_owned_path(raw_path: str, repo_root: pathlib.Path | None = None) -> str | None: + path = raw_path.strip() + if is_ignored_path(path): return None - for root in OWNED_ROOTS: - marker = f"{root}/" - index = path.find(marker) - if index >= 0: - return path[index:] + repo_root = repo_root or pathlib.Path.cwd() + candidate_path = pathlib.Path(path) + if candidate_path.is_absolute(): + try: + rel_path = candidate_path.resolve(strict=False).relative_to(repo_root.resolve(strict=False)) + except ValueError: + pass + else: + rel_text = rel_path.as_posix() + if is_ignored_path(rel_text): + return None + if is_owned_relative_path(rel_text): + return rel_text + return None + + if is_owned_relative_path(path): + return path + + fallback = fallback_owned_path(path) + if fallback is not None and not is_ignored_path(fallback): + return fallback return None diff --git a/tests/test_ci_swift_warning_budget.sh b/tests/test_ci_swift_warning_budget.sh index 9d26f83aa78a..b993cc7b2996 100755 --- a/tests/test_ci_swift_warning_budget.sh +++ b/tests/test_ci_swift_warning_budget.sh @@ -11,18 +11,24 @@ LOG="$TMP_DIR/build.log" BUDGET="$TMP_DIR/budget.tsv" CI_FILE="$ROOT_DIR/.github/workflows/ci.yml" -REQUIRED_PATTERNS=( - "Validate Swift warning budget guard" - "tee /tmp/cmux-build-output.txt" - "scripts/swift_warning_budget.py --log /tmp/cmux-build-output.txt" -) - -for pattern in "${REQUIRED_PATTERNS[@]}"; do - if ! grep -Fq "$pattern" "$CI_FILE"; then - echo "missing Swift warning budget CI pattern: $pattern" >&2 - exit 1 - fi -done +python3 - "$CI_FILE" <<'PY' +import pathlib +import sys + +ci_text = pathlib.Path(sys.argv[1]).read_text(encoding="utf-8") +required_tokens = { + "workflow guard step": "Validate Swift warning budget guard", + "guard test script": "./tests/test_ci_swift_warning_budget.sh", + "build log tee": "tee", + "build log path": "cmux-build-output.txt", + "budget script": "scripts/swift_warning_budget.py", + "budget log argument": "--log", +} + +missing = [label for label, token in required_tokens.items() if token not in ci_text] +if missing: + raise SystemExit(f"missing Swift warning budget CI wiring: {', '.join(missing)}") +PY cat >"$LOG" <<'LOG' /Users/example/cmux/Sources/AppDelegate.swift:10:1: warning: add '@preconcurrency' to suppress 'Sendable'-related warnings from module 'ObjectiveC' @@ -35,6 +41,11 @@ cat >"$LOG" <<'LOG' warning: Run script build phase 'Run Script' will be run during every build LOG +cat >>"$LOG" <&2 + exit 1 +fi + if grep -q 'vendor/bonsplit' "$BUDGET"; then echo "vendor warning should not be included" >&2 exit 1 @@ -57,6 +73,11 @@ if grep -q 'SourcePackages' "$BUDGET"; then exit 1 fi +if grep -q '.ci-source-packages' "$BUDGET"; then + echo "cloned package warning should not be included" >&2 + exit 1 +fi + python3 scripts/swift_warning_budget.py --log "$LOG" --budget "$BUDGET" cat >>"$LOG" <<'LOG' From e553f6ec86972ab8b17e8e6540e2b5af74fe6c02 Mon Sep 17 00:00:00 2001 From: Lawrence Chen Date: Tue, 28 Apr 2026 03:08:53 -0700 Subject: [PATCH 3/3] fix: clean up warning budget edge cases --- scripts/swift_warning_budget.py | 6 +++++- tests/test_ci_swift_warning_budget.sh | 20 +++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/scripts/swift_warning_budget.py b/scripts/swift_warning_budget.py index 944b94fa0d16..247305cb8b33 100755 --- a/scripts/swift_warning_budget.py +++ b/scripts/swift_warning_budget.py @@ -222,7 +222,11 @@ def main(argv: list[str]) -> int: print(f"Missing warning budget: {args.budget}", file=sys.stderr) return 2 - allowed = load_budget(args.budget) + try: + allowed = load_budget(args.budget) + except ValueError as exc: + print(f"Error reading warning budget: {exc}", file=sys.stderr) + return 2 print_budget_summary("Allowed warning budget", allowed) return compare_budget(actual, allowed) diff --git a/tests/test_ci_swift_warning_budget.sh b/tests/test_ci_swift_warning_budget.sh index b993cc7b2996..719930c774ef 100755 --- a/tests/test_ci_swift_warning_budget.sh +++ b/tests/test_ci_swift_warning_budget.sh @@ -73,7 +73,7 @@ if grep -q 'SourcePackages' "$BUDGET"; then exit 1 fi -if grep -q '.ci-source-packages' "$BUDGET"; then +if grep -Fq '.ci-source-packages' "$BUDGET"; then echo "cloned package warning should not be included" >&2 exit 1 fi @@ -94,3 +94,21 @@ if ! grep -q 'Swift warning budget exceeded' "$TMP_DIR/fail.out"; then cat "$TMP_DIR/fail.out" >&2 exit 1 fi + +printf 'not-a-valid-budget-line\n' >"$TMP_DIR/bad-budget.tsv" +if python3 scripts/swift_warning_budget.py --log "$LOG" --budget "$TMP_DIR/bad-budget.tsv" >"$TMP_DIR/bad.out" 2>&1; then + echo "expected malformed budget failure" >&2 + exit 1 +fi + +if ! grep -q 'Error reading warning budget' "$TMP_DIR/bad.out"; then + echo "expected malformed budget error output" >&2 + cat "$TMP_DIR/bad.out" >&2 + exit 1 +fi + +if grep -q 'Traceback' "$TMP_DIR/bad.out"; then + echo "malformed budget should not print a traceback" >&2 + cat "$TMP_DIR/bad.out" >&2 + exit 1 +fi