iOS: public App Store lane (com.cmux.app), privacy manifest, fastlane screenshots - #6697
Conversation
… screenshots Prep cmux iOS for a public App Store release alongside the existing dev.cmux.app.beta dogfood channel. - PrivacyInfo.xcprivacy wired into the app target: NSPrivacyTracking=false, UserDefaults (CA92.1) + file-timestamp (DDA9.1) reasons, product-interaction analytics label. No Sentry/IDFA in iOS. - upload-testflight.sh + cloud-testflight.sh: new appstore lane (com.cmux.app, on-device name "cmux", cmux Distribution profile), sharing the existing release entitlements and cmux-ios URL scheme. - web/services/apns/routePolicy.ts: route com.cmux.app to production APNs (+ test). - MobileBuildType: document/test com.cmux.app as a prod bundle id. - ios/fastlane: snapshot config (en-US + ja; iPhone 6.9" + iPad 13") driving the CMUX_UITEST_MOCK_DATA DEBUG state via a SnapshotUITests case. - .github/workflows/ios-screenshots.yml: capture screenshots in CI on a DEBUG build (no signing), resolving the required iPhone/iPad classes at runtime; optional upload to App Store Connect on workflow_dispatch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds end-to-end App Store screenshot automation via a new GitHub Actions workflow, fastlane ChangesiOS App Store Readiness
Sequence Diagram(s)sequenceDiagram
participant GHActions as GitHub Actions
participant Fastlane
participant Simulator as iOS Simulator
participant SnapshotUITests
participant Snapshot
participant ASC as App Store Connect
GHActions->>GHActions: resolve SNAPSHOT_DEVICES via simctl
GHActions->>Fastlane: fastlane screenshots (SNAPSHOT_LANGUAGES, SNAPSHOT_DEVICES)
Fastlane->>Simulator: launch cmux-ios scheme DEBUG build
Simulator->>SnapshotUITests: run testCaptureAppStoreScreenshots
SnapshotUITests->>Snapshot: setupSnapshot(app) — inject language/locale
SnapshotUITests->>Snapshot: snapshot("01-Workspaces")
Snapshot->>Simulator: XCUIScreen.main.screenshot()
Snapshot->>Snapshot: write PNG to fastlane/screenshots/
SnapshotUITests->>Snapshot: snapshot("02-Terminal")
SnapshotUITests->>Snapshot: snapshot("03-Terminal-Keyboard")
Fastlane-->>GHActions: screenshots artifact uploaded
GHActions->>GHActions: decode ASC_API_KEY secret (if upload=true)
GHActions->>Fastlane: fastlane upload_screenshots
Fastlane->>ASC: deliver screenshots (skip binary/metadata)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 24 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (24 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryPrepares cmux iOS for a public App Store release under
Confidence Score: 4/5Safe to merge with the screenshot-fixture placement question resolved; the production distribution, APNs routing, and privacy manifest changes are straightforward and well-tested. The production-path changes (APNs routing, upload scripts, privacy manifest) are minimal, correct, and covered by tests. The screenshot pipeline works as described. Three new types — ScreenshotKeyboardView, ScreenshotNotificationBanner, and TerminalPreviewTranscripts — and additional env-var reading in TerminalLayoutPreviewView are placed in production Sources/CmuxMobileShellUI/ wrapped entirely in #if DEBUG with no production callers. They exist solely to serve the snapshot test harness in ios/cmuxUITests/, which is where they belong per the repo's established rule. Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/ScreenshotKeyboardView.swift, ScreenshotNotificationBanner.swift, TerminalPreviewTranscripts.swift, and the new additions to TerminalLayoutPreviewView.swift — all debug-only screenshot scaffolding currently in production Sources. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[ios/scripts/upload-testflight.sh] -->|--lane beta| B[dev.cmux.app.beta\ncmux Beta Distribution]
A -->|--lane appstore| C[com.cmux.app\ncmux Distribution]
D[ios/scripts/cloud-testflight.sh] -->|--lane beta| B
D -->|--lane appstore| C
C --> E[TestFlight\ncom.cmux.app record]
B --> F[TestFlight\nbeta record]
C --> G[web/services/apns/routePolicy.ts\nPROD_BUNDLE_IDS]
B --> G
G -->|production| H[APNs production host]
G -->|sandbox| I[APNs sandbox host]
J[.github/workflows/ios-screenshots.yml] -->|fastlane screenshots| K[DEBUG build\nCMUX_UITEST_MOCK_DATA]
K --> L[SnapshotUITests\niPhone 6.9 + iPad 13]
L -->|workflow_dispatch + upload=true| M[App Store Connect\ncom.cmux.app screenshots]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[ios/scripts/upload-testflight.sh] -->|--lane beta| B[dev.cmux.app.beta\ncmux Beta Distribution]
A -->|--lane appstore| C[com.cmux.app\ncmux Distribution]
D[ios/scripts/cloud-testflight.sh] -->|--lane beta| B
D -->|--lane appstore| C
C --> E[TestFlight\ncom.cmux.app record]
B --> F[TestFlight\nbeta record]
C --> G[web/services/apns/routePolicy.ts\nPROD_BUNDLE_IDS]
B --> G
G -->|production| H[APNs production host]
G -->|sandbox| I[APNs sandbox host]
J[.github/workflows/ios-screenshots.yml] -->|fastlane screenshots| K[DEBUG build\nCMUX_UITEST_MOCK_DATA]
K --> L[SnapshotUITests\niPhone 6.9 + iPad 13]
L -->|workflow_dispatch + upload=true| M[App Store Connect\ncom.cmux.app screenshots]
Reviews (12): Last reviewed commit: "ios-screenshots: fix MainActor setupSnap..." | Re-trigger Greptile |
| run: | | ||
| set -euo pipefail | ||
| # Use the Homebrew fastlane (ships its own Ruby) instead of bundler: | ||
| # macOS system Ruby is 2.6 but fastlane needs >= 3.0, so `bundle install` |
There was a problem hiding this comment.
$SNAPSHOT_DEVICES is always empty at this echo
The Python heredoc writes SNAPSHOT_DEVICES=… to $GITHUB_ENV, which only takes effect in subsequent steps — not in the current step's shell. The echo "Resolved SNAPSHOT_DEVICES=$SNAPSHOT_DEVICES" line therefore always prints "Resolved SNAPSHOT_DEVICES=" and provides no useful diagnostic. The fastlane screenshots step in the next step correctly receives the variable, so there is no functional breakage, but the misleading output could confuse anyone debugging a CI capture failure.
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ios/scripts/upload-testflight.sh (2)
43-50: 🩺 Stability & Availability | 🟡 MinorThe
rc=$?capture is unreachable underset -euo pipefailwhen PlistBuddy fails.When the
PlistBuddycommand returns non-zero (e.g., key absent), the assignmentaps="$(..."exits the function beforerc=$?runs. This prevents the diagnostic on lines 45–46 from printing. Both call sites already use theif !exempt pattern, so the function is invoked correctly, but the function's internal logic fails to capture the exit code as intended.Use
aps="$(/usr/libexec/PlistBuddy -c 'Print :aps-environment' "$ent" 2>/dev/null)" || rc=$?to capture the failure.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/scripts/upload-testflight.sh` around lines 43 - 50, The exit code capture pattern in the PlistBuddy command block uses an unreachable `rc=$?` statement that never executes under `set -euo pipefail`. When PlistBuddy fails and returns non-zero, the command substitution in the `aps="$(..."` assignment causes the function to exit before the subsequent `rc=$?` line can run, preventing the error diagnostics from being printed. Fix this by modifying the assignment to use the OR operator pattern: change `aps="$(/usr/libexec/PlistBuddy ...)"` followed by `rc=$?` on the next line to instead combine them as `aps="$(/usr/libexec/PlistBuddy ...)" || rc=$?` so the exit code is properly captured when the command fails.
18-23: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
unzipruns inside$workdir, so a relative IPA path won't be found.
( cd "$workdir" && unzip -q "$ipa" )resolves$iparelative to the temp dir, not the caller's CWD. Ifverify_ipa_aps_environment_productionis ever passed a relative path, the unzip fails and you get a misleading "could not unzip IPA" error rather than verifying entitlements. Drop thecdand letunzipwrite to the temp dir via-d:🛠️ Proposed fix
- workdir="$(mktemp -d)" - if ! ( cd "$workdir" && unzip -q "$ipa" ); then + workdir="$(mktemp -d)" + if ! unzip -q "$ipa" -d "$workdir"; thenConfirm whether existing callers always pass an absolute path:
#!/bin/bash rg -nP '\bverify_ipa_aps_environment_production\b' ios/scripts/upload-testflight.sh # Inspect how the argument (IPA path) is constructed before the call rg -nP '\b(IPA|ipa|EXPORT_IPA|exported_ipa)\w*=' ios/scripts/upload-testflight.sh🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/scripts/upload-testflight.sh` around lines 18 - 23, In the verify_ipa_aps_environment_production function, the unzip command currently runs inside a subshell that changes directory to $workdir, which causes relative IPA paths to be resolved incorrectly. Remove the cd "$workdir" && part from the unzip line and instead use the -d flag to specify the destination directory: use unzip -q -d "$workdir" "$ipa" so that $ipa is resolved relative to the caller's current working directory instead of relative to the temp directory.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ios-screenshots.yml:
- Around line 90-117: The issue is that the Python script output within the
heredoc is redirected to $GITHUB_ENV, but this does not populate the
SNAPSHOT_DEVICES variable in the current shell environment. When the echo
command on line 116 tries to expand $SNAPSHOT_DEVICES, it fails under set -u
because the variable is undefined in the current shell. You need to capture the
Python script's output into a shell variable first, export or declare it in the
current shell environment, and then write it to $GITHUB_ENV so the variable is
available both immediately and in subsequent workflow steps. This ensures that
the echo command referencing $SNAPSHOT_DEVICES will have access to the resolved
device names.
- Around line 13-40: The ios-screenshots workflow lacks a concurrency group
configuration, which allows multiple simultaneous runs to overlap and compete
for macOS runners while also creating race conditions during screenshot uploads.
Add a concurrency block after the permissions section and before the jobs
section in the .github/workflows/ios-screenshots.yml file that defines a
concurrency group to ensure only one workflow run executes at a time, using a
group identifier that prevents overlapping executions and sets
cancel-in-progress to true to terminate outdated runs when new ones are
triggered.
In `@ios/cmuxUITests/SnapshotUITests.swift`:
- Around line 51-53: The composeButton lookup is using an incorrect primary
accessibility identifier terminal.inputAccessory.composeButton when the correct
app-side identifier is terminal.inputAccessory.composer. Update the ternary
expression that assigns to composeButton to check for
terminal.inputAccessory.composer first as the primary identifier, then fall back
to the NSPredicate matching for identifiers containing "compose" as a secondary
option. This ensures the correct control is found reliably.
- Around line 33-48: The test currently passes even when required UI elements
are missing because the conditions checking workspaceList.waitForExistence and
terminalSurface.waitForExistence silently continue if they return false instead
of failing the test. Replace the if statements that guard the snapshot calls for
workspaceList and terminalSurface with assertions or test failures that
explicitly require these UI elements to exist within their timeouts. This
ensures the test will fail when required screenshots cannot be captured,
preventing the CI from passing with empty or incomplete snapshot artifacts.
In `@ios/scripts/upload-testflight.sh`:
- Around line 296-299: The empty DISPLAY_NAME_ARGS array expansion breaks under
set -u strict mode on macOS bash 3.2 when PRODUCT_DISPLAY_NAME_OVERRIDE is not
set. Find the two archive call sites that expand DISPLAY_NAME_ARGS (referenced
at lines 549 and 572) where "${DISPLAY_NAME_ARGS[@]}" is used, and apply the
conditional expansion guard by changing the expansion to use
"${DISPLAY_NAME_ARGS[@]:-}" to safely handle the empty array case across bash
versions.
---
Outside diff comments:
In `@ios/scripts/upload-testflight.sh`:
- Around line 43-50: The exit code capture pattern in the PlistBuddy command
block uses an unreachable `rc=$?` statement that never executes under `set -euo
pipefail`. When PlistBuddy fails and returns non-zero, the command substitution
in the `aps="$(..."` assignment causes the function to exit before the
subsequent `rc=$?` line can run, preventing the error diagnostics from being
printed. Fix this by modifying the assignment to use the OR operator pattern:
change `aps="$(/usr/libexec/PlistBuddy ...)"` followed by `rc=$?` on the next
line to instead combine them as `aps="$(/usr/libexec/PlistBuddy ...)" || rc=$?`
so the exit code is properly captured when the command fails.
- Around line 18-23: In the verify_ipa_aps_environment_production function, the
unzip command currently runs inside a subshell that changes directory to
$workdir, which causes relative IPA paths to be resolved incorrectly. Remove the
cd "$workdir" && part from the unzip line and instead use the -d flag to specify
the destination directory: use unzip -q -d "$workdir" "$ipa" so that $ipa is
resolved relative to the caller's current working directory instead of relative
to the temp directory.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 9d0b216d-c6df-4374-8b50-c4a6442e810b
📒 Files selected for processing (14)
.github/workflows/ios-screenshots.ymlPackages/iOS/CmuxMobileShellModel/Sources/CmuxMobileShellModel/MobileBuildType.swiftPackages/iOS/CmuxMobileShellModel/Tests/CmuxMobileShellModelTests/MobileFeedbackRouteTests.swiftios/cmux-ios.xcodeproj/project.pbxprojios/cmux/PrivacyInfo.xcprivacyios/cmuxUITests/SnapshotHelper.swiftios/cmuxUITests/SnapshotUITests.swiftios/fastlane/Appfileios/fastlane/Fastfileios/fastlane/Snapfileios/scripts/cloud-testflight.shios/scripts/upload-testflight.shweb/services/apns/routePolicy.tsweb/tests/apns.test.ts
| DISPLAY_NAME_ARGS=() | ||
| if [[ -n "$PRODUCT_DISPLAY_NAME_OVERRIDE" ]]; then | ||
| DISPLAY_NAME_ARGS=( "PRODUCT_DISPLAY_NAME=$PRODUCT_DISPLAY_NAME_OVERRIDE" ) | ||
| fi |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
sed -n '1,12p' ios/scripts/upload-testflight.sh # shebang + set flags
rg -nP 'set -[a-z]*u|set -o nounset' ios/scripts/upload-testflight.sh
rg -nP '"\$\{DISPLAY_NAME_ARGS\[@\]\}"' ios/scripts/upload-testflight.sh
bash --version | head -1 # sandbox bash, for reference onlyRepository: manaflow-ai/cmux
Length of output: 1044
🏁 Script executed:
#!/bin/bash
# Inspect the array initialization section
sed -n '269,300p' ios/scripts/upload-testflight.sh
# Inspect both archive call sites with context
sed -n '540,560p' ios/scripts/upload-testflight.sh
sed -n '560,580p' ios/scripts/upload-testflight.sh
# Check if PRODUCT_DISPLAY_NAME_OVERRIDE has a default value
rg -B2 -A2 'PRODUCT_DISPLAY_NAME_OVERRIDE' ios/scripts/upload-testflight.sh | head -40Repository: manaflow-ai/cmux
Length of output: 3760
Empty DISPLAY_NAME_ARGS array expansion breaks under set -u on macOS system bash.
For the beta lane (the default), PRODUCT_DISPLAY_NAME_OVERRIDE remains empty, leaving DISPLAY_NAME_ARGS as an empty array. On bash 3.2 (macOS system /bin/bash), expanding "${DISPLAY_NAME_ARGS[@]}" under set -u raises unbound variable and breaks the most common path. Use the conditional-expansion guard to ensure empty array safety across bash versions.
Apply at both archive call sites (lines 549 & 572):
Fix
- "${DISPLAY_NAME_ARGS[@]}" \
+ ${DISPLAY_NAME_ARGS[@]+"${DISPLAY_NAME_ARGS[@]}"} \🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ios/scripts/upload-testflight.sh` around lines 296 - 299, The empty
DISPLAY_NAME_ARGS array expansion breaks under set -u strict mode on macOS bash
3.2 when PRODUCT_DISPLAY_NAME_OVERRIDE is not set. Find the two archive call
sites that expand DISPLAY_NAME_ARGS (referenced at lines 549 and 572) where
"${DISPLAY_NAME_ARGS[@]}" is used, and apply the conditional expansion guard by
changing the expansion to use "${DISPLAY_NAME_ARGS[@]:-}" to safely handle the
empty array case across bash versions.
The resolve step exported SNAPSHOT_DEVICES to $GITHUB_ENV (for later steps) but then echoed $SNAPSHOT_DEVICES, which is unset in the current shell, so set -u aborted the step. Confirm by grepping the env file instead. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…iants Resolver picked the device TYPE 'iPad Pro 13-inch (M5) (16GB)', which has no pre-created simulator, so fastlane errored 'not in list of available simulators'. Resolve against available simulator DEVICES and exclude RAM-variant (GB) names; prefer iPhone NN Pro Max + iPad Pro/Air 13-inch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
The Snapfile's devices([...]) overrode the action's devices param, so CI's runtime-resolved simulators were ignored and fastlane looked for the stale 'iPhone 16 Pro Max'. Move devices+languages to the Fastfile (env-overridable, SNAPSHOT_DEVICES/SNAPSHOT_LANGUAGES) as the single source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CMUX_UITEST_MOCK_DATA alone lands on the add-device screen, so snapshots were empty (0 images). Use the standalone preview hooks that render real UI with no sign-in/pairing: WORKSPACE_LIST_PREVIEW + TERMINAL_PREVIEW (+ fake keyboard), settling on window/foreground instead of identifiers the preview views do not expose. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the captured screenshots presentable for the App Store: - TerminalLayoutPreviewView feeds a sample ANSI agent-session transcript when CMUX_UITEST_TERMINAL_PREVIEW_CONTENT=1, so the terminal shot shows real content instead of a blank surface (blank layout preview unchanged). - SnapshotUITests enables that flag, drops the debug zoom overlay, and swipes away the one-time 'Ready for Apple Intelligence' notification banner. - Fastfile capture uses override_status_bar for a clean 9:41 status bar. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
ios/cmuxUITests/SnapshotUITests.swift (1)
30-65: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winScreenshot test cannot fail, so empty/blank captures pass CI silently.
settle()only does best-effort waits (_ = app.wait(...),_ = ...waitForExistence(...)) and the snapshot calls are unconditional. Combined withcontinueAfterFailure = trueand noXCTAssert*, a launch that never renders the intended preview (e.g. preview env flag regressed, fixture broke) still produces a green run with a blank/launch-screen artifact. Anchor at least one stable element per screen and assert it before capturing.Same underlying gap previously raised on the anchor checks; re-flagging since the refactor dropped the identifier waits without adding assertions.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/cmuxUITests/SnapshotUITests.swift` around lines 30 - 65, The snapshot tests in the `settle()` method and subsequent test flow use best-effort waits that discard results and unconditional snapshot calls with no assertions, allowing blank/unrendered screenshots to pass silently if preview environment flags regress or fixtures break. Replace the best-effort waits in `settle()` with assertions, or add explicit XCTAssert calls before each snapshot call to anchor and validate at least one stable UI element exists on each screen before capturing. Add assertions targeting a stable element for the workspace list before snapshot("01-Workspaces"), a stable element for the terminal surface before snapshot("02-Terminal"), and a stable element for the keyboard view before snapshot("03-Terminal-Keyboard").
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@ios/cmuxUITests/SnapshotUITests.swift`:
- Around line 30-65: The snapshot tests in the `settle()` method and subsequent
test flow use best-effort waits that discard results and unconditional snapshot
calls with no assertions, allowing blank/unrendered screenshots to pass silently
if preview environment flags regress or fixtures break. Replace the best-effort
waits in `settle()` with assertions, or add explicit XCTAssert calls before each
snapshot call to anchor and validate at least one stable UI element exists on
each screen before capturing. Add assertions targeting a stable element for the
workspace list before snapshot("01-Workspaces"), a stable element for the
terminal surface before snapshot("02-Terminal"), and a stable element for the
keyboard view before snapshot("03-Terminal-Keyboard").
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5cdc41c4-9c8e-4f25-a98b-9c4dcc0f1ab6
📒 Files selected for processing (5)
.github/workflows/ios-screenshots.ymlPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLayoutPreviewView.swiftios/cmuxUITests/SnapshotUITests.swiftios/fastlane/Fastfileios/fastlane/Snapfile
updateUIView never re-ran with a non-zero size, so the sample transcript was never fed and the terminal shot came out blank. Feed it from the surface's first didResize (grid sized = can render). Also trigger the screenshots workflow on preview-view changes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLayoutPreviewView.swift (1)
66-90: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftLocalize the screenshot transcript copy instead of hardcoding English.
Line 66 introduces user-visible terminal text as bare English literals, so Japanese screenshot runs will still render English content. Route these lines through localization keys and provide
en/jacatalog entries.Suggested direction
- static let sampleTranscript: Data = { + static func sampleTranscript(locale: Locale = .current) -> Data { let esc = "\u{1B}" let reset = "\(esc)[0m" @@ - let lines = [ - "\(dim)~/projects/app\(reset) \(cyan)main\(reset)", - "\(prompt) claude \(dim)\"add a dark mode toggle\"\(reset)", + let commandText = String( + localized: "terminalPreview.sample.command", + defaultValue: "add a dark mode toggle" + ) + let statusText = String( + localized: "terminalPreview.sample.status", + defaultValue: "I'll add a dark mode toggle to Settings." + ) + let lines = [ + "\(dim)~/projects/app\(reset) \(cyan)main\(reset)", + "\(prompt) claude \(dim)\"\(commandText)\"\(reset)", @@ - "\(magenta)●\(reset) I'll add a dark mode toggle to Settings.", + "\(magenta)●\(reset) \(statusText)", @@ - return Data(lines.joined(separator: "\r\n").utf8) - }() + return Data(lines.joined(separator: "\r\n").utf8) + } @@ - surfaceView.processOutput(TerminalLayoutPreviewSurface.sampleTranscript) + surfaceView.processOutput(TerminalLayoutPreviewSurface.sampleTranscript())As per coding guidelines, “All user-facing strings must be localized … currently English and Japanese,” and as per path instructions, full internationalization must be enforced for production user-facing text changes.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLayoutPreviewView.swift` around lines 66 - 90, The sampleTranscript static variable in TerminalLayoutPreviewView contains hardcoded English user-visible strings that must be localized per coding guidelines. Extract all user-facing text from the lines array in the sampleTranscript computed property (such as "Reading SettingsView.swift", "I'll add a dark mode toggle", "Build succeeded", etc.) and replace them with NSLocalizedString references using appropriate localization keys. Then create corresponding localization catalog entries providing both English and Japanese translations for each extracted string.Sources: Coding guidelines, Path instructions
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLayoutPreviewView.swift`:
- Around line 66-90: The sampleTranscript static variable in
TerminalLayoutPreviewView contains hardcoded English user-visible strings that
must be localized per coding guidelines. Extract all user-facing text from the
lines array in the sampleTranscript computed property (such as "Reading
SettingsView.swift", "I'll add a dark mode toggle", "Build succeeded", etc.) and
replace them with NSLocalizedString references using appropriate localization
keys. Then create corresponding localization catalog entries providing both
English and Japanese translations for each extracted string.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: bf9d5464-64a6-4141-9426-dad2cc0d33da
📒 Files selected for processing (2)
.github/workflows/ios-screenshots.ymlPackages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLayoutPreviewView.swift
Add zh-Hans, zh-Hant, ko, de, fr, es, pt-BR, it, ru, nl, tr, pl to all iOS xcstrings (app, agent chat UI, InfoPlist permission strings) and the project knownRegions, alongside the existing en + ja. Translations are a machine-translation first pass (placeholders/format specifiers preserved, brand/tech terms kept) and should get native review before public release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Store the App Store listing copy (description, keywords, promotional text, URLs; en-US also name/subtitle) for en-US + ja, zh-Hans, zh-Hant, ko, de-DE, fr-FR, es-ES, pt-BR, it, ru, nl-NL, tr, pl. Applied to App Store Connect and kept here so the listing is reproducible via fastlane deliver and the machine-translated copy is reviewable before public release. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ios/fastlane/metadata/it/description.txt`:
- Line 3: Replace the English loanword "input" with native Italian terminology
in the iOS App Store metadata description. On line 3, change "quando gli agenti
richiedono input" to use either "immissione" or "ingresso" as appropriate. On
line 7, apply the same replacement pattern where "invia input" appears, using
either "immissione" or alternatively "istruzioni" for the sending context.
Ensure consistency in terminology choice across both occurrences for proper
localization.
In `@ios/fastlane/metadata/pl/description.txt`:
- Around line 1-11: In the Polish description text, the phrase "pierścienie
powiadomień gdy agent czeka na dane wejściowe" is missing a required comma
before the conjunction "gdy". Add a comma between "powiadomień" and "gdy" so it
reads "pierścienie powiadomień, gdy agent czeka na dane wejściowe" to comply
with Polish grammar rules.
In `@ios/fastlane/metadata/pt-BR/description.txt`:
- Line 9: In the Portuguese description text for the bullet point about secure
pairing, add a comma before the word "ou" to improve grammatical clarity and
follow Portuguese style conventions. Locate the line containing "Pareamento
seguro: conecte ao seu próprio Mac pela rede local ou por relay" and insert a
comma after "local" so it reads "rede local, ou por relay". This follows
standard Portuguese grammar when joining alternatives with "ou".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f66234ab-df93-466c-a1c1-ed7850e0f983
📒 Files selected for processing (72)
ios/fastlane/metadata/de-DE/description.txtios/fastlane/metadata/de-DE/keywords.txtios/fastlane/metadata/de-DE/marketing_url.txtios/fastlane/metadata/de-DE/promotional_text.txtios/fastlane/metadata/de-DE/support_url.txtios/fastlane/metadata/en-US/description.txtios/fastlane/metadata/en-US/keywords.txtios/fastlane/metadata/en-US/marketing_url.txtios/fastlane/metadata/en-US/name.txtios/fastlane/metadata/en-US/promotional_text.txtios/fastlane/metadata/en-US/subtitle.txtios/fastlane/metadata/en-US/support_url.txtios/fastlane/metadata/es-ES/description.txtios/fastlane/metadata/es-ES/keywords.txtios/fastlane/metadata/es-ES/marketing_url.txtios/fastlane/metadata/es-ES/promotional_text.txtios/fastlane/metadata/es-ES/support_url.txtios/fastlane/metadata/fr-FR/description.txtios/fastlane/metadata/fr-FR/keywords.txtios/fastlane/metadata/fr-FR/marketing_url.txtios/fastlane/metadata/fr-FR/promotional_text.txtios/fastlane/metadata/fr-FR/support_url.txtios/fastlane/metadata/it/description.txtios/fastlane/metadata/it/keywords.txtios/fastlane/metadata/it/marketing_url.txtios/fastlane/metadata/it/promotional_text.txtios/fastlane/metadata/it/support_url.txtios/fastlane/metadata/ja/description.txtios/fastlane/metadata/ja/keywords.txtios/fastlane/metadata/ja/marketing_url.txtios/fastlane/metadata/ja/promotional_text.txtios/fastlane/metadata/ja/support_url.txtios/fastlane/metadata/ko/description.txtios/fastlane/metadata/ko/keywords.txtios/fastlane/metadata/ko/marketing_url.txtios/fastlane/metadata/ko/promotional_text.txtios/fastlane/metadata/ko/support_url.txtios/fastlane/metadata/nl-NL/description.txtios/fastlane/metadata/nl-NL/keywords.txtios/fastlane/metadata/nl-NL/marketing_url.txtios/fastlane/metadata/nl-NL/promotional_text.txtios/fastlane/metadata/nl-NL/support_url.txtios/fastlane/metadata/pl/description.txtios/fastlane/metadata/pl/keywords.txtios/fastlane/metadata/pl/marketing_url.txtios/fastlane/metadata/pl/promotional_text.txtios/fastlane/metadata/pl/support_url.txtios/fastlane/metadata/pt-BR/description.txtios/fastlane/metadata/pt-BR/keywords.txtios/fastlane/metadata/pt-BR/marketing_url.txtios/fastlane/metadata/pt-BR/promotional_text.txtios/fastlane/metadata/pt-BR/support_url.txtios/fastlane/metadata/ru/description.txtios/fastlane/metadata/ru/keywords.txtios/fastlane/metadata/ru/marketing_url.txtios/fastlane/metadata/ru/promotional_text.txtios/fastlane/metadata/ru/support_url.txtios/fastlane/metadata/tr/description.txtios/fastlane/metadata/tr/keywords.txtios/fastlane/metadata/tr/marketing_url.txtios/fastlane/metadata/tr/promotional_text.txtios/fastlane/metadata/tr/support_url.txtios/fastlane/metadata/zh-Hans/description.txtios/fastlane/metadata/zh-Hans/keywords.txtios/fastlane/metadata/zh-Hans/marketing_url.txtios/fastlane/metadata/zh-Hans/promotional_text.txtios/fastlane/metadata/zh-Hans/support_url.txtios/fastlane/metadata/zh-Hant/description.txtios/fastlane/metadata/zh-Hant/keywords.txtios/fastlane/metadata/zh-Hant/marketing_url.txtios/fastlane/metadata/zh-Hant/promotional_text.txtios/fastlane/metadata/zh-Hant/support_url.txt
| @@ -0,0 +1,11 @@ | |||
| cmux porta i tuoi agenti di programmazione sul telefono. Collega il tuo Mac con il terminale cmux e guarda i tuoi agenti AI lavorare in tempo reale, ricevi una notifica push nel momento in cui un agente ha bisogno della tua attenzione o completa un'attività, e rispondi o esegui comandi da qualsiasi luogo. | |||
|
|
|||
| cmux è il terminale costruito per gli agenti di programmazione: schede verticali, anelli di notifica quando gli agenti richiedono input, pannelli suddivisi, un browser integrato e una CLI programmabile sul desktop. L'app iOS è il tuo telecomando per tutto questo. | |||
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use native Italian term instead of English loanword "input".
Lines 3 and 7 use the English loanword "input" in Italian App Store metadata. For formal App Store listings and localization completeness, replace with a native Italian term: "ingresso" (entry/input) or "immissione" (input/entry).
Suggested replacements:
- Line 3:
...quando gli agenti richiedono input...→...quando gli agenti richiedono immissione...or...ingresso... - Line 7:
...invia input, esegui comandi...→...invia immissioni, esegui comandi...or...invia istruzioni...
Also applies to: 7-7
🧰 Tools
🪛 LanguageTool
[uncategorized] ~3-~3: "ingresso" "entrata" "immissione"
Context: ...i notifica quando gli agenti richiedono input, pannelli suddivisi, un browser integra...
(ST_01_005)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ios/fastlane/metadata/it/description.txt` at line 3, Replace the English
loanword "input" with native Italian terminology in the iOS App Store metadata
description. On line 3, change "quando gli agenti richiedono input" to use
either "immissione" or "ingresso" as appropriate. On line 7, apply the same
replacement pattern where "invia input" appears, using either "immissione" or
alternatively "istruzioni" for the sending context. Ensure consistency in
terminology choice across both occurrences for proper localization.
Source: Linters/SAST tools
…ons, frameit) Make App Store screenshots realistic and on-message: - TerminalPreviewTranscripts: Claude Code / Codex / OpenCode / pi sample sessions, selected via CMUX_UITEST_TERMINAL_TRANSCRIPT. - ScreenshotKeyboardView: drawn dark iOS keyboard overlaid in the reserved keyboard region (CMUX_UITEST_SCREENSHOT_KEYBOARD=1); the simulator won't render the system keyboard in CI. Device-aware height (iPhone vs iPad). - ScreenshotNotificationBanner: iOS push banner over the workspace list (CMUX_UITEST_NOTIFICATION_BANNER=1) to show agent notifications. - SnapshotUITests: 7 screens (workspaces, notifications, 4 agents w/ keyboard, full Ghostty terminal). - frameit pipeline: tranquil gradient background, Framefile.json, localized title.strings (prepare_frames.py from titles.*.json), framed in the lane after capture. Workflow installs imagemagick; deliver uploads the framed images. Dynamic island comes from the frameit device frame. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-lane # Conflicts: # ios/cmux/Resources/Localizable.xcstrings
05-Opencode said 'OpenCode, pi, any agent' but pi is the very next screenshot (06-Pi). Reworded to 'OpenCode and any agent' (and each locale's equivalent) so the two shots don't overlap. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…d-lane # Conflicts: # Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Resources/Localizable.xcstrings # Packages/iOS/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttyRuntime.swift # ios/scripts/upload-testflight.sh
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 3ed49c8. Configure here.
…d-lane # Conflicts: # Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Resources/Localizable.xcstrings
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
…issions guard, screenshot decoupling, notarization hardening) (#12157) * ci: guard reusable-workflow permission grants (red on main's shape) GitHub validates a reusable workflow's permissions against the calling job when it parses the caller. A callee that requests a scope the caller does not grant fails the whole caller run at startup, before any job runs. That is what blocks the stable release today: release.yml calls ios-screenshots.yml, which requests `actions: write` while release.yml grants none (#12149). Add scripts/ci/check_reusable_workflow_permissions.py (python3 stdlib only) that walks every local `uses: ./.github/workflows/*.yml` call, computes the calling job's grant (job block, else workflow block, else the repository default) and the callee's request (max over its workflow block and every job block, gated jobs included, mirroring 4b9720d), follows nested calls with the intermediate grant, and fails on any scope that asks for more. tests/test_ci_reusable_workflow_permissions.py covers the rule on fixture trees (the exact #12149 shape, shorthands, job-level overrides, repository defaults, nesting, missing callees) and then runs the checker on the real tree, which fails until the next commit fixes the workflows. Wired into the workflow-guard-tests job in ci.yml. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: stop ios-screenshots.yml requesting actions: write (fixes release startup) The screenshot workflow declared `actions: write` since #6697, but no step ever used it: checkout runs with persist-credentials disabled, the two artifact uploads use the runner's artifact token, the capture is a DEBUG simulator build, and the App Store Connect upload path authenticates with an API key. When #11342 made release.yml call this workflow, GitHub compared the callee's block with the caller's grant (contents/attestations/id-token only) and refused the release workflow at parse time: startup_failure, no job run, for tag pushes and dispatches alike (#12149). Reduce the callee to `contents: read`, the minimum its steps use. Widening release.yml instead would have handed a UI-test job the ability to cancel or dispatch runs for no benefit. The guard added in the previous commit now passes on the tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: do not gate build-sign-notarize on iOS screenshot capture #11342 made build-sign-notarize need generate-ios-screenshots ("gates build-sign-notarize on screenshot success"). The DMG never consumes those artifacts: nothing in build-sign-notarize downloads them, and the App Store tooling (ios/scripts/appstore-shots.sh capture) dispatches its own ios-screenshots.yml run rather than reading a release run. What the gate did do was make every stable macOS release wait for, and fail with, a 300-minute simulator capture across nine locales on shared macOS runners, a lane that had "not compiled on main for days" before #11342 healed it. Keep the capture in release.yml as a sibling job, so every tag still gets screenshots at the exact release ref and a failed capture still turns the run red, but drop it from build-sign-notarize.needs. Trade-off: a green macOS release no longer implies the screenshot capture succeeded; the run conclusion still does. tests/test_ci_release_ios_screenshots_decoupled.sh pins the policy (fails on main's needs list, passes here) and runs in workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: give the screenshot capture job only contents: read and no secrets The screenshot job runs a DEBUG simulator UI test after `brew install` of fastlane and imagemagick. Capture-only needs to read the repository and nothing else: checkout runs with persist-credentials disabled, artifact uploads use the runner's artifact token, and the App Store Connect upload path in ios-screenshots.yml is gated to workflow_dispatch from main, so it is unreachable from a release run whatever `upload` says. Set job-level `permissions: contents: read` on the calling job (the pattern the cmux-tui callers already use) instead of passing the workflow's contents/attestations/id-token write grant through, and drop `secrets: inherit`, which handed every repository secret (Developer ID certificate and password, notarization credentials, Sparkle private key, R2 keys, Sentry token, ASC key) to that job for no benefit. Trade-off: if the release lane ever wants the ASC upload, it must add `secrets: inherit` back together with `upload: true` and relax the callee's dispatch-only guard. That should be a deliberate change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: let the Sparkle monotonic guard warn on non-tag dry runs release.yml runs tests/test_ci_sparkle_build_monotonic.sh at the top of build-sign-notarize. On plain main it fails (CURRENT_PROJECT_VERSION 102 equals the published 0.64.22 build), which is correct for a tag push about to publish but wrong for the workflow's built-in dry run: a non-tag workflow_dispatch publishes nothing and, by design, runs from a branch that has not been bumped yet. The dry run was therefore impossible without a throwaway bump commit. Chosen fix: a CMUX_SPARKLE_MONOTONIC_MODE switch on the guard, `enforce` by default (tag pushes, scripts/release-pretag-guard.sh) and `warn` when release.yml runs from anything but refs/tags/*. Rejected alternative: running the dry run from a throwaway branch with a temporary bump, which would validate a commit that never merges and leave the pipeline un-dry-runnable for everyone else. tests/test_sparkle_build_monotonic_modes.sh drives the guard against fixture project files and a local appcast (stale fails in enforce and by default, warns in warn mode, bumped passes in both, unreachable appcast soft-passes, unknown mode fails) and pins the ref-based selection in release.yml. Wired into workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: give Gatekeeper twenty minutes to see a fresh notarization ticket scripts/ci/notarize-computer-use-helper.sh polls `spctl` on the standalone Computer Use helper after stapling because Apple's CDN publishes the ticket some time after notarytool reports Accepted. The budget was 20 x 15s. Nightly run 34208928547 (2026-09-08) exhausted it: Accepted at 09:51:28, still "Unnotarized Developer ID" at 09:56:18, exit 3, whole universal lane failed, while the arm64 and x86_64 lanes passed in the same window. Raise the default to 80 x 15s (twenty minutes) and announce the budget on the first rejection so a log reader can tell propagation from a hang. Trade-off: a genuinely rejected helper now takes up to twenty minutes to fail instead of five, which only delays an already-lost release; a short budget failed good releases, each costing a full rebuild and a human retry. Both knobs remain env-configurable (CMUX_GATEKEEPER_ASSESS_ATTEMPTS/_DELAY_SECONDS). nightly's signing job has an 80-minute timeout with a 7-10 minute typical duration, so the budget fits there; release.yml's timeout is raised in the next commit. tests/test_notarize_computer_use_helper.sh now pins the defaults (at least 1200s, polled at least every 30s, env-configurable literals) and the budget announcement, alongside the existing override and give-up coverage. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: raise build-sign-notarize timeout to 90 minutes v0.64.22's build-sign-notarize took 39.8 minutes on 2026-08-03. Since then the job gained the Cloud tunnel system extension and its Go engine build (#11789), the universal diff sidecar and cmux-tui client install (#12006), two extra smoke launches, and a Gatekeeper propagation wait that can now run twenty minutes on its own. A 60-minute ceiling leaves no room for a slow notarytool day, and a timeout mid-notarization wastes the whole build. 90 minutes covers the measured baseline plus the known variable waits with headroom while still bounding a hung job on a shared self-hosted runner. To be re-checked against the dry-run duration for this branch: the timeout must stay at least 25 percent above it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: name the tunnel extension by its bundle identifier, not its App ID The first release dry run that could start after the permission fix (run 34222835589) failed 30 minutes in, at "Verify binary architectures": error: system extension identifier is 'com.cmuxterm.app.tunnel', expected '7WLXT3NR37.com.cmuxterm.app.tunnel' #11789 passed the team-prefixed App ID to scripts/normalize-system-extension-bundle.sh and looked for the tunnel binary under 7WLXT3NR37.com.cmuxterm.app.tunnel.systemextension. The Release build's PRODUCT_BUNDLE_IDENTIFIER for the extension is com.cmuxterm.app.tunnel; only NEMachServiceName ($(CMUX_TEAM_ID_PREFIX)$(PRODUCT_BUNDLE_IDENTIFIER)) and the provisioning profile's com.apple.application-identifier carry the team prefix, and the app activates whatever CFBundleIdentifier the bundled extension declares. nightly.yml already does it this way and ships com.cmuxterm.app.nightly.tunnel.systemextension with a profile for 7WLXT3NR37.com.cmuxterm.app.nightly.tunnel (run 34220568401). The mistake was invisible until now because release.yml could not start at all. Use the bundle identifier for the normalize call and the directory the verify step inspects; keep the App ID for the profile check. tests/test_ci_release_tunnel_identifiers.sh derives all three from cmux.xcodeproj/project.pbxproj (fails on main's release.yml, passes here) and runs in workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * Fix main's package-test compile error and Swift warning-budget violations main is red for every branch that routes the macOS lane (#12161, #12165), which keeps ci-status from ever reporting green on this release-pipeline PR. Fix both at the root rather than refreshing the budget: - swift-package-tests: FakeTerminalEngine.swift gained a `UUID` parameter in #10564 but imports only GhosttyKit. Add `import Foundation`. - tests-build-and-lag (scripts/swift_warning_budget.py, actual > budget): * AppDelegate+PaneMemoryGuardrail.swift: parenthesize the two `compactMap` closures inside the `guard` condition ("trailing closure in this context is confusable with the body of the statement"). * SessionIndexTableController.swift: the bounds-change observer block is typed @sendable in the current SDK, so referencing `isApplyingRows` and `reconcilePresentation(in:)` warned. The block is delivered on `queue: .main`, so run it under `MainActor.assumeIsolated`, the same pattern SidebarWorkspaceRowCellView uses; no async hop, same timing. * CmuxTuiSnapshotParser.swift: `switch resourceID.kind` already covers every SurfaceResourceKind case (terminal, display, browser), so the `default: continue` could never run. Remove it; a new case now fails to compile here instead of being silently skipped. * SurfaceCatalogModel.swift: `if let rowID,` rebound a value the body never read; test `rowID != nil` instead. * TerminalController.swift: `payload` in the `.delivered` branch is never mutated; make it `let`. Every change is behavior-preserving. Verified with `swiftc -parse` on each file locally (no app build on the shared machine); the routed CI lane proves the build and the budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * Normalize project.pbxproj (main bypassed the pre-commit hook in #12145) scripts/check-pbxproj.sh fails on main since 567ba48 (#12145): the three StackAccountAvatarViewTests.swift entries were added out of the normalizer's sorted order, so every PR's workflow-guard-tests job goes red at "Validate pbxproj objectVersion pin and normalization" and linux-preflight, tests and ci-status cascade from it. This is the output of scripts/normalize-pbxproj.py: three lines reordered, no identifier or setting changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * tests: drive sparkle_generate_appcast.sh through the no-delta release path (red) Release dry run 34227505375 (2026-09-08) reported "Generate Sparkle appcast: success" and uploaded a cmux-release-dry-run artifact containing only the DMG. The job log shows why: ./scripts/sparkle_generate_appcast.sh: line 93: delta_args[@]: unbound variable A tag push would have published a GitHub Release without appcast.xml, so no Sparkle client would ever be offered the update, and the R2 stable appcast upload would then fail after the release already existed. tests/test_sparkle_generate_appcast_no_deltas.sh runs the real script with fake git/xcodebuild/generate_appcast/sign_update tools under every bash on the machine (/bin/bash 3.2 on macOS reproduces the bug; bash 5 never did) and requires a signed appcast at the requested output path with no delta arguments when there are no previous archives, and with --maximum-deltas when there are. It also requires release.yml to verify the feed after generation instead of trusting the exit status. Fails on main's script and workflow; the next commit fixes both. Wired into workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: generate the appcast when there are no previous archives (bash 3.2) #11788 added `delta_args=()` and passed "${delta_args[@]}" to generate_appcast. In bash 4.4+ an empty array expands to nothing; in bash 3.2 (macOS /bin/bash, which `#!/usr/bin/env bash` resolves to on the release runner) it is an "unbound variable" error under `set -u`. Worse, with the script's EXIT trap bash 3.2 then exits 0, so the step passed and no appcast was written. Nightly always has previous archives (delta_args non-empty) and was never affected; the stable release lane never has them and has been broken since 2026-09-03, unnoticed because release.yml could not start at all (#12149). Expand the array as ${delta_args[@]+"${delta_args[@]}"}, which is empty when the array is empty in every bash. In release.yml, verify after generation that appcast.xml exists, carries sparkle:edSignature and references cmux-macos.dmg before anything uploads it: the exit status alone is not a reliable signal on bash 3.2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: fail the Sparkle monotonic guard closed when the appcast is unreachable CodeRabbit on #12157: enforce mode (tag pushes, release-pretag-guard.sh) soft-passed when the published appcast could not be fetched, so a tag push could publish a stale CURRENT_PROJECT_VERSION on a network blip or on a latest release that lacks appcast.xml, the exact state that leaves Sparkle clients without updates. A missing signal must fail closed when the run is about to publish. enforce mode now fails with an explanation when the published build is unknown; warn mode (non-tag dry runs) keeps the soft pass because it publishes nothing. curl retries transient failures (3 x 2s by default, overridable so the tests exercise the unreachable path without waiting). tests/test_sparkle_build_monotonic_modes.sh covers enforce, default and warn against an unreachable appcast. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * tests: assert the journal-carried pane clear that #11976 replaced clear_notifications with #11976 removed the v1 `clear_notifications --tab --panel` send from the Claude prompt-submit and pre-tool-use hooks; the pane-scoped clear now rides on the `agent.turn.started` / `agent.state.changed` journal events, which the app reconciles into `clearNotifications(forTabId:surfaceId:)`. It updated the Python hook tests to the new wire contract but not ClaudeHookLifecycleCleanupTests, whose two moved-pane tests still expected the removed command. They fail on main in the strict app-host agent-notification step (shard 6), unnoticed because #11976's PR CI never routed the macOS lane. Assert the new contract instead: the journal event for the hook names the re-homed workspace and the live pane (via the existing AgentJournalAppendCapture parser), and nothing still wipes the whole destination workspace. SessionEnd keeps sending the v1 command, so its tests are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: make app-host hangs fail in minutes instead of the 75-minute job timeout Every macOS lane run since 2026-09-03 has ended with app-host shards "cancelled" at the 75-minute job timeout. Today's logs (run 34236235360, shards 1/2/4, both attempts) show the mechanism, and it is two plumbing defects rather than the tests: 1. scripts/ci/xcodebuild_noninteractive.py resets its 300s idle deadline on every output chunk. Since #11755 (merged 2026-09-03T02:09Z, after the last green lane at 2026-09-02T09:21Z) the app host logs every Cloud API poll, `[CloudVM] GET /api/vm not_signed_in`, every 45 seconds. A test host hung inside a WebKit page load (WebContent XPC: "Could not signal service ... 113") therefore never looks idle, so the wrapper's kill and retry path, which handled the same WebKit failure on the 09-02 green run, never fires. 2. The tolerant batch watchdog in ci.yml (1800s) killed only the console-session launcher and left the lock wrapper, xcodebuild and the app host alive; the app host kept the `| tee` pipe open, so the step sat idle from "timeout after 1800s; terminating" until the job timeout. Fixes: - CMUX_XCODEBUILD_NONINTERACTIVE_IDLE_IGNORE_RE: output lines matching it do not count as progress. run-app-host-xcodebuild.sh defaults it to the Cloud poll line (an empty value restores counting everything; an invalid regex fails closed with exit 2). Real output still resets the clock, so a slow but progressing batch is unaffected. - The ci.yml batch runner writes xcodebuild output to the capture file and streams it with a detached tail, kills the whole process tree (pgrep -P recursion, TERM then KILL) when the batch budget expires, and reads both the streamed and per-batch captures for the SwiftPM retry heuristic. Behavior tests: tests/test_ci_xcodebuild_noninteractive_helper.py drives a child that prints only the keepalive every 50ms (finishes without the pattern, idles out at 0.3s with it, invalid pattern exits 2); tests/test_ci_change_areas.py runs the real step script against a runner that hangs and leaves a grandchild holding stdout, and requires exit 124 within seconds with the grandchild dead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: keep the canonical OUTPUT capture line the SPM-retry guard pins tests/test_ci_unit_test_spm_retry.sh requires `OUTPUT=$(cat "$TEST_OUTPUT")` verbatim in the app-host step; the previous commit folded the per-batch capture files into that line and turned workflow-guard-tests red. Keep the pinned line and append the per-batch captures on the next line instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: keep a watchdog-killed app-host batch terminal; drop the wall-clock assert CodeRabbit on #12157: the expected-failure normalization in run_unit_test_batch greps the capture for the last "Executed ... failures" summary and returns success on "(0 unexpected)". After the watchdog kills a batch (status 124) the capture can still hold an earlier attempt's summary (run-app-host-xcodebuild.sh retries into the same file), so a terminated batch could be reported as passed. Treat 124 as terminal before the normalization. The hung-runner behavior test now prints a decoy "(0 unexpected)" summary before hanging and requires the step to stay at 124 without the "All failures ... are expected" message. Also drop the `elapsed < 60` assertion from that test: the harness's 120s subprocess timeout already bounds a runaway step, and a hard wall-clock ceiling only adds scheduler-delay flakes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * chore: normalize project file after main merge * test: remove timing dependency from terminal lane test * ci: accept completed app-host summaries after launcher timeout * test: make idle watchdog coverage scheduler tolerant * ci: require Swift Testing completion before accepting launcher timeout * ci: fail fast on known broad app-host hangs * test: allowlist virtual retry delay fixtures * ci: preserve app-host lock queue headroom * ci: restore terminal creation CLI regression coverage * ci: retain release guard coverage after main merge * ci: remove obsolete app-host idle override * cmuxTests: run the Coderouter no-socket tests without an unwaited expectation `runCoderouterCLI(waitForSocket: false)` still asked `startMockServer` for a case-bound `expectation(description: "cli mock socket handled")` and then never waited on it. The shared accept loop fulfills that expectation when the listener closes at the end of the helper, so XCTest ended `testCoderouterUnknownVerbStillPassesThroughToTheInstalledCLI` and `testCoderouterClaudeAddOAuthTokenRejectsAPIKeyShapeBeforeTheSocket` with "Failed due to unwaited expectation", which it counts as an *unexpected* failure. Since #12207 the app-host batch classifier fails a batch on any unexpected failure, so this one test turned shard 5 (and the sibling test shard 6) red on main and on every PR: run 34401456032, main run 34342638735 attempts 1 and 2. Serve those two tests from the detached mock server instead, which owns no expectation, and keep the waited path for every other Coderouter test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: rerun a remote tmux mirror suite once after an app-host crash The non-tolerant "Run remote tmux mirror detach and placement regressions" gate on shard 6 fails whenever the app host crashes mid-suite, which #9348 documents as nondeterministic: the crash point moves between tests and the relaunched host passes the rest (run 34401456032 crashed in dedicatedWindowSocketDefaultsToFocusNeutral; the previous run and both main attempts passed the same step). Capture each suite's output and rerun the suite exactly once, only when xcodebuild printed "Restarting after unexpected exit, crash, or test timeout". An assertion failure never earns a rerun and a second crash still fails the shard, so the gate keeps rejecting real regressions. tests/test_ci_change_areas.py drives the real step script against a fake console runner: crash-then-pass is green with three invocations, an assertion failure exits 65 after one invocation, and two crashes exit 65 after two. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(iroh): promote the replacement connection deterministically usableConnectionRetiresOlderConnectionsFromSameEndpointIdentity keyed the markUsable call off `recorder.recordedCount() == 2`, which both handlers evaluate concurrently. When the first connection's handler reached that check after the replacement had already recorded, it promoted `first` instead, superseded the freshly admitted replacement, and the replacement's own markUsable returned false: "Expectation failed: await admission.markUsable()" at CmxIrohEndpointServerTests.swift:353 in CI run 34414741413 (swift-package-tests), while the previous run passed the same code. Admit before recording so the test's `recorder.next()` proves `first` is active before `replacement` is enqueued, and promote only the replacement by identity. The suite passes three consecutive local runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cmuxTests: serialize the stdin pump suite and bound its blocking waits Shard 3 of CI run 34414741413 hung three times at the app-host wrapper's 300s idle timeout in the same batch. The hang sample shows SSHPTYAttachReconnectInputFilterTests.stdinPumpFiltersReadyInputBeforeStopSignal parked in stopFiltering() -> read() on the stop-acknowledgement pipe with every other visible cooperative-pool thread also inside a test body's synchronous wait. The pump under test is a detached task that needs one of those same threads, and the five pump tests each hold a thread for the ~13s reconnect probe deadline while running concurrently, so the suite can leave no thread for any pump (the family issue #12180 tracks). Run the suite serialized so at most one test parks a thread at a time, and bound every wait: stopFiltering now takes a 30s acknowledgement timeout and must succeed, and the EOF/exact reads poll with the same deadline. A pump that never gets scheduled now fails its test inside the batch instead of parking the shard until the idle timeout retries are exhausted. The two assertions in this suite that already fail on main (readUntilEOF == forwardedInput in stdinPumpFiltersReadyInputBeforeStopSignal and stdinPumpKeepsFilteringLateProbeRepliesAfterInitialDrain) are unchanged; the batch classifier tolerates them today. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…rkflow permissions guard, screenshot decoupling, notarization hardening) (manaflow-ai#12157) * ci: guard reusable-workflow permission grants (red on main's shape) GitHub validates a reusable workflow's permissions against the calling job when it parses the caller. A callee that requests a scope the caller does not grant fails the whole caller run at startup, before any job runs. That is what blocks the stable release today: release.yml calls ios-screenshots.yml, which requests `actions: write` while release.yml grants none (manaflow-ai#12149). Add scripts/ci/check_reusable_workflow_permissions.py (python3 stdlib only) that walks every local `uses: ./.github/workflows/*.yml` call, computes the calling job's grant (job block, else workflow block, else the repository default) and the callee's request (max over its workflow block and every job block, gated jobs included, mirroring 4b9720d), follows nested calls with the intermediate grant, and fails on any scope that asks for more. tests/test_ci_reusable_workflow_permissions.py covers the rule on fixture trees (the exact manaflow-ai#12149 shape, shorthands, job-level overrides, repository defaults, nesting, missing callees) and then runs the checker on the real tree, which fails until the next commit fixes the workflows. Wired into the workflow-guard-tests job in ci.yml. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: stop ios-screenshots.yml requesting actions: write (fixes release startup) The screenshot workflow declared `actions: write` since manaflow-ai#6697, but no step ever used it: checkout runs with persist-credentials disabled, the two artifact uploads use the runner's artifact token, the capture is a DEBUG simulator build, and the App Store Connect upload path authenticates with an API key. When manaflow-ai#11342 made release.yml call this workflow, GitHub compared the callee's block with the caller's grant (contents/attestations/id-token only) and refused the release workflow at parse time: startup_failure, no job run, for tag pushes and dispatches alike (manaflow-ai#12149). Reduce the callee to `contents: read`, the minimum its steps use. Widening release.yml instead would have handed a UI-test job the ability to cancel or dispatch runs for no benefit. The guard added in the previous commit now passes on the tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: do not gate build-sign-notarize on iOS screenshot capture manaflow-ai#11342 made build-sign-notarize need generate-ios-screenshots ("gates build-sign-notarize on screenshot success"). The DMG never consumes those artifacts: nothing in build-sign-notarize downloads them, and the App Store tooling (ios/scripts/appstore-shots.sh capture) dispatches its own ios-screenshots.yml run rather than reading a release run. What the gate did do was make every stable macOS release wait for, and fail with, a 300-minute simulator capture across nine locales on shared macOS runners, a lane that had "not compiled on main for days" before manaflow-ai#11342 healed it. Keep the capture in release.yml as a sibling job, so every tag still gets screenshots at the exact release ref and a failed capture still turns the run red, but drop it from build-sign-notarize.needs. Trade-off: a green macOS release no longer implies the screenshot capture succeeded; the run conclusion still does. tests/test_ci_release_ios_screenshots_decoupled.sh pins the policy (fails on main's needs list, passes here) and runs in workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: give the screenshot capture job only contents: read and no secrets The screenshot job runs a DEBUG simulator UI test after `brew install` of fastlane and imagemagick. Capture-only needs to read the repository and nothing else: checkout runs with persist-credentials disabled, artifact uploads use the runner's artifact token, and the App Store Connect upload path in ios-screenshots.yml is gated to workflow_dispatch from main, so it is unreachable from a release run whatever `upload` says. Set job-level `permissions: contents: read` on the calling job (the pattern the cmux-tui callers already use) instead of passing the workflow's contents/attestations/id-token write grant through, and drop `secrets: inherit`, which handed every repository secret (Developer ID certificate and password, notarization credentials, Sparkle private key, R2 keys, Sentry token, ASC key) to that job for no benefit. Trade-off: if the release lane ever wants the ASC upload, it must add `secrets: inherit` back together with `upload: true` and relax the callee's dispatch-only guard. That should be a deliberate change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: let the Sparkle monotonic guard warn on non-tag dry runs release.yml runs tests/test_ci_sparkle_build_monotonic.sh at the top of build-sign-notarize. On plain main it fails (CURRENT_PROJECT_VERSION 102 equals the published 0.64.22 build), which is correct for a tag push about to publish but wrong for the workflow's built-in dry run: a non-tag workflow_dispatch publishes nothing and, by design, runs from a branch that has not been bumped yet. The dry run was therefore impossible without a throwaway bump commit. Chosen fix: a CMUX_SPARKLE_MONOTONIC_MODE switch on the guard, `enforce` by default (tag pushes, scripts/release-pretag-guard.sh) and `warn` when release.yml runs from anything but refs/tags/*. Rejected alternative: running the dry run from a throwaway branch with a temporary bump, which would validate a commit that never merges and leave the pipeline un-dry-runnable for everyone else. tests/test_sparkle_build_monotonic_modes.sh drives the guard against fixture project files and a local appcast (stale fails in enforce and by default, warns in warn mode, bumped passes in both, unreachable appcast soft-passes, unknown mode fails) and pins the ref-based selection in release.yml. Wired into workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: give Gatekeeper twenty minutes to see a fresh notarization ticket scripts/ci/notarize-computer-use-helper.sh polls `spctl` on the standalone Computer Use helper after stapling because Apple's CDN publishes the ticket some time after notarytool reports Accepted. The budget was 20 x 15s. Nightly run 34208928547 (2026-09-08) exhausted it: Accepted at 09:51:28, still "Unnotarized Developer ID" at 09:56:18, exit 3, whole universal lane failed, while the arm64 and x86_64 lanes passed in the same window. Raise the default to 80 x 15s (twenty minutes) and announce the budget on the first rejection so a log reader can tell propagation from a hang. Trade-off: a genuinely rejected helper now takes up to twenty minutes to fail instead of five, which only delays an already-lost release; a short budget failed good releases, each costing a full rebuild and a human retry. Both knobs remain env-configurable (CMUX_GATEKEEPER_ASSESS_ATTEMPTS/_DELAY_SECONDS). nightly's signing job has an 80-minute timeout with a 7-10 minute typical duration, so the budget fits there; release.yml's timeout is raised in the next commit. tests/test_notarize_computer_use_helper.sh now pins the defaults (at least 1200s, polled at least every 30s, env-configurable literals) and the budget announcement, alongside the existing override and give-up coverage. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: raise build-sign-notarize timeout to 90 minutes v0.64.22's build-sign-notarize took 39.8 minutes on 2026-08-03. Since then the job gained the Cloud tunnel system extension and its Go engine build (manaflow-ai#11789), the universal diff sidecar and cmux-tui client install (manaflow-ai#12006), two extra smoke launches, and a Gatekeeper propagation wait that can now run twenty minutes on its own. A 60-minute ceiling leaves no room for a slow notarytool day, and a timeout mid-notarization wastes the whole build. 90 minutes covers the measured baseline plus the known variable waits with headroom while still bounding a hung job on a shared self-hosted runner. To be re-checked against the dry-run duration for this branch: the timeout must stay at least 25 percent above it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: name the tunnel extension by its bundle identifier, not its App ID The first release dry run that could start after the permission fix (run 34222835589) failed 30 minutes in, at "Verify binary architectures": error: system extension identifier is 'com.cmuxterm.app.tunnel', expected '7WLXT3NR37.com.cmuxterm.app.tunnel' manaflow-ai#11789 passed the team-prefixed App ID to scripts/normalize-system-extension-bundle.sh and looked for the tunnel binary under 7WLXT3NR37.com.cmuxterm.app.tunnel.systemextension. The Release build's PRODUCT_BUNDLE_IDENTIFIER for the extension is com.cmuxterm.app.tunnel; only NEMachServiceName ($(CMUX_TEAM_ID_PREFIX)$(PRODUCT_BUNDLE_IDENTIFIER)) and the provisioning profile's com.apple.application-identifier carry the team prefix, and the app activates whatever CFBundleIdentifier the bundled extension declares. nightly.yml already does it this way and ships com.cmuxterm.app.nightly.tunnel.systemextension with a profile for 7WLXT3NR37.com.cmuxterm.app.nightly.tunnel (run 34220568401). The mistake was invisible until now because release.yml could not start at all. Use the bundle identifier for the normalize call and the directory the verify step inspects; keep the App ID for the profile check. tests/test_ci_release_tunnel_identifiers.sh derives all three from cmux.xcodeproj/project.pbxproj (fails on main's release.yml, passes here) and runs in workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * Fix main's package-test compile error and Swift warning-budget violations main is red for every branch that routes the macOS lane (manaflow-ai#12161, manaflow-ai#12165), which keeps ci-status from ever reporting green on this release-pipeline PR. Fix both at the root rather than refreshing the budget: - swift-package-tests: FakeTerminalEngine.swift gained a `UUID` parameter in manaflow-ai#10564 but imports only GhosttyKit. Add `import Foundation`. - tests-build-and-lag (scripts/swift_warning_budget.py, actual > budget): * AppDelegate+PaneMemoryGuardrail.swift: parenthesize the two `compactMap` closures inside the `guard` condition ("trailing closure in this context is confusable with the body of the statement"). * SessionIndexTableController.swift: the bounds-change observer block is typed @sendable in the current SDK, so referencing `isApplyingRows` and `reconcilePresentation(in:)` warned. The block is delivered on `queue: .main`, so run it under `MainActor.assumeIsolated`, the same pattern SidebarWorkspaceRowCellView uses; no async hop, same timing. * CmuxTuiSnapshotParser.swift: `switch resourceID.kind` already covers every SurfaceResourceKind case (terminal, display, browser), so the `default: continue` could never run. Remove it; a new case now fails to compile here instead of being silently skipped. * SurfaceCatalogModel.swift: `if let rowID,` rebound a value the body never read; test `rowID != nil` instead. * TerminalController.swift: `payload` in the `.delivered` branch is never mutated; make it `let`. Every change is behavior-preserving. Verified with `swiftc -parse` on each file locally (no app build on the shared machine); the routed CI lane proves the build and the budget. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * Normalize project.pbxproj (main bypassed the pre-commit hook in manaflow-ai#12145) scripts/check-pbxproj.sh fails on main since 567ba48 (manaflow-ai#12145): the three StackAccountAvatarViewTests.swift entries were added out of the normalizer's sorted order, so every PR's workflow-guard-tests job goes red at "Validate pbxproj objectVersion pin and normalization" and linux-preflight, tests and ci-status cascade from it. This is the output of scripts/normalize-pbxproj.py: three lines reordered, no identifier or setting changed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * tests: drive sparkle_generate_appcast.sh through the no-delta release path (red) Release dry run 34227505375 (2026-09-08) reported "Generate Sparkle appcast: success" and uploaded a cmux-release-dry-run artifact containing only the DMG. The job log shows why: ./scripts/sparkle_generate_appcast.sh: line 93: delta_args[@]: unbound variable A tag push would have published a GitHub Release without appcast.xml, so no Sparkle client would ever be offered the update, and the R2 stable appcast upload would then fail after the release already existed. tests/test_sparkle_generate_appcast_no_deltas.sh runs the real script with fake git/xcodebuild/generate_appcast/sign_update tools under every bash on the machine (/bin/bash 3.2 on macOS reproduces the bug; bash 5 never did) and requires a signed appcast at the requested output path with no delta arguments when there are no previous archives, and with --maximum-deltas when there are. It also requires release.yml to verify the feed after generation instead of trusting the exit status. Fails on main's script and workflow; the next commit fixes both. Wired into workflow-guard-tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: generate the appcast when there are no previous archives (bash 3.2) manaflow-ai#11788 added `delta_args=()` and passed "${delta_args[@]}" to generate_appcast. In bash 4.4+ an empty array expands to nothing; in bash 3.2 (macOS /bin/bash, which `#!/usr/bin/env bash` resolves to on the release runner) it is an "unbound variable" error under `set -u`. Worse, with the script's EXIT trap bash 3.2 then exits 0, so the step passed and no appcast was written. Nightly always has previous archives (delta_args non-empty) and was never affected; the stable release lane never has them and has been broken since 2026-09-03, unnoticed because release.yml could not start at all (manaflow-ai#12149). Expand the array as ${delta_args[@]+"${delta_args[@]}"}, which is empty when the array is empty in every bash. In release.yml, verify after generation that appcast.xml exists, carries sparkle:edSignature and references cmux-macos.dmg before anything uploads it: the exit status alone is not a reliable signal on bash 3.2. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * release: fail the Sparkle monotonic guard closed when the appcast is unreachable CodeRabbit on manaflow-ai#12157: enforce mode (tag pushes, release-pretag-guard.sh) soft-passed when the published appcast could not be fetched, so a tag push could publish a stale CURRENT_PROJECT_VERSION on a network blip or on a latest release that lacks appcast.xml, the exact state that leaves Sparkle clients without updates. A missing signal must fail closed when the run is about to publish. enforce mode now fails with an explanation when the published build is unknown; warn mode (non-tag dry runs) keeps the soft pass because it publishes nothing. curl retries transient failures (3 x 2s by default, overridable so the tests exercise the unreachable path without waiting). tests/test_sparkle_build_monotonic_modes.sh covers enforce, default and warn against an unreachable appcast. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * tests: assert the journal-carried pane clear that manaflow-ai#11976 replaced clear_notifications with manaflow-ai#11976 removed the v1 `clear_notifications --tab --panel` send from the Claude prompt-submit and pre-tool-use hooks; the pane-scoped clear now rides on the `agent.turn.started` / `agent.state.changed` journal events, which the app reconciles into `clearNotifications(forTabId:surfaceId:)`. It updated the Python hook tests to the new wire contract but not ClaudeHookLifecycleCleanupTests, whose two moved-pane tests still expected the removed command. They fail on main in the strict app-host agent-notification step (shard 6), unnoticed because manaflow-ai#11976's PR CI never routed the macOS lane. Assert the new contract instead: the journal event for the hook names the re-homed workspace and the live pane (via the existing AgentJournalAppendCapture parser), and nothing still wipes the whole destination workspace. SessionEnd keeps sending the v1 command, so its tests are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: make app-host hangs fail in minutes instead of the 75-minute job timeout Every macOS lane run since 2026-09-03 has ended with app-host shards "cancelled" at the 75-minute job timeout. Today's logs (run 34236235360, shards 1/2/4, both attempts) show the mechanism, and it is two plumbing defects rather than the tests: 1. scripts/ci/xcodebuild_noninteractive.py resets its 300s idle deadline on every output chunk. Since manaflow-ai#11755 (merged 2026-09-03T02:09Z, after the last green lane at 2026-09-02T09:21Z) the app host logs every Cloud API poll, `[CloudVM] GET /api/vm not_signed_in`, every 45 seconds. A test host hung inside a WebKit page load (WebContent XPC: "Could not signal service ... 113") therefore never looks idle, so the wrapper's kill and retry path, which handled the same WebKit failure on the 09-02 green run, never fires. 2. The tolerant batch watchdog in ci.yml (1800s) killed only the console-session launcher and left the lock wrapper, xcodebuild and the app host alive; the app host kept the `| tee` pipe open, so the step sat idle from "timeout after 1800s; terminating" until the job timeout. Fixes: - CMUX_XCODEBUILD_NONINTERACTIVE_IDLE_IGNORE_RE: output lines matching it do not count as progress. run-app-host-xcodebuild.sh defaults it to the Cloud poll line (an empty value restores counting everything; an invalid regex fails closed with exit 2). Real output still resets the clock, so a slow but progressing batch is unaffected. - The ci.yml batch runner writes xcodebuild output to the capture file and streams it with a detached tail, kills the whole process tree (pgrep -P recursion, TERM then KILL) when the batch budget expires, and reads both the streamed and per-batch captures for the SwiftPM retry heuristic. Behavior tests: tests/test_ci_xcodebuild_noninteractive_helper.py drives a child that prints only the keepalive every 50ms (finishes without the pattern, idles out at 0.3s with it, invalid pattern exits 2); tests/test_ci_change_areas.py runs the real step script against a runner that hangs and leaves a grandchild holding stdout, and requires exit 124 within seconds with the grandchild dead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: keep the canonical OUTPUT capture line the SPM-retry guard pins tests/test_ci_unit_test_spm_retry.sh requires `OUTPUT=$(cat "$TEST_OUTPUT")` verbatim in the app-host step; the previous commit folded the per-batch capture files into that line and turned workflow-guard-tests red. Keep the pinned line and append the per-batch captures on the next line instead. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * ci: keep a watchdog-killed app-host batch terminal; drop the wall-clock assert CodeRabbit on manaflow-ai#12157: the expected-failure normalization in run_unit_test_batch greps the capture for the last "Executed ... failures" summary and returns success on "(0 unexpected)". After the watchdog kills a batch (status 124) the capture can still hold an earlier attempt's summary (run-app-host-xcodebuild.sh retries into the same file), so a terminated batch could be reported as passed. Treat 124 as terminal before the normalization. The hung-runner behavior test now prints a decoy "(0 unexpected)" summary before hanging and requires the step to stay at 124 without the "All failures ... are expected" message. Also drop the `elapsed < 60` assertion from that test: the harness's 120s subprocess timeout already bounds a runaway step, and a hard wall-clock ceiling only adds scheduler-delay flakes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VsJWT2S5Mx2Wv3XGFih8as * chore: normalize project file after main merge * test: remove timing dependency from terminal lane test * ci: accept completed app-host summaries after launcher timeout * test: make idle watchdog coverage scheduler tolerant * ci: require Swift Testing completion before accepting launcher timeout * ci: fail fast on known broad app-host hangs * test: allowlist virtual retry delay fixtures * ci: preserve app-host lock queue headroom * ci: restore terminal creation CLI regression coverage * ci: retain release guard coverage after main merge * ci: remove obsolete app-host idle override * cmuxTests: run the Coderouter no-socket tests without an unwaited expectation `runCoderouterCLI(waitForSocket: false)` still asked `startMockServer` for a case-bound `expectation(description: "cli mock socket handled")` and then never waited on it. The shared accept loop fulfills that expectation when the listener closes at the end of the helper, so XCTest ended `testCoderouterUnknownVerbStillPassesThroughToTheInstalledCLI` and `testCoderouterClaudeAddOAuthTokenRejectsAPIKeyShapeBeforeTheSocket` with "Failed due to unwaited expectation", which it counts as an *unexpected* failure. Since manaflow-ai#12207 the app-host batch classifier fails a batch on any unexpected failure, so this one test turned shard 5 (and the sibling test shard 6) red on main and on every PR: run 34401456032, main run 34342638735 attempts 1 and 2. Serve those two tests from the detached mock server instead, which owns no expectation, and keep the waited path for every other Coderouter test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * ci: rerun a remote tmux mirror suite once after an app-host crash The non-tolerant "Run remote tmux mirror detach and placement regressions" gate on shard 6 fails whenever the app host crashes mid-suite, which manaflow-ai#9348 documents as nondeterministic: the crash point moves between tests and the relaunched host passes the rest (run 34401456032 crashed in dedicatedWindowSocketDefaultsToFocusNeutral; the previous run and both main attempts passed the same step). Capture each suite's output and rerun the suite exactly once, only when xcodebuild printed "Restarting after unexpected exit, crash, or test timeout". An assertion failure never earns a rerun and a second crash still fails the shard, so the gate keeps rejecting real regressions. tests/test_ci_change_areas.py drives the real step script against a fake console runner: crash-then-pass is green with three invocations, an assertion failure exits 65 after one invocation, and two crashes exit 65 after two. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(iroh): promote the replacement connection deterministically usableConnectionRetiresOlderConnectionsFromSameEndpointIdentity keyed the markUsable call off `recorder.recordedCount() == 2`, which both handlers evaluate concurrently. When the first connection's handler reached that check after the replacement had already recorded, it promoted `first` instead, superseded the freshly admitted replacement, and the replacement's own markUsable returned false: "Expectation failed: await admission.markUsable()" at CmxIrohEndpointServerTests.swift:353 in CI run 34414741413 (swift-package-tests), while the previous run passed the same code. Admit before recording so the test's `recorder.next()` proves `first` is active before `replacement` is enqueued, and promote only the replacement by identity. The suite passes three consecutive local runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * cmuxTests: serialize the stdin pump suite and bound its blocking waits Shard 3 of CI run 34414741413 hung three times at the app-host wrapper's 300s idle timeout in the same batch. The hang sample shows SSHPTYAttachReconnectInputFilterTests.stdinPumpFiltersReadyInputBeforeStopSignal parked in stopFiltering() -> read() on the stop-acknowledgement pipe with every other visible cooperative-pool thread also inside a test body's synchronous wait. The pump under test is a detached task that needs one of those same threads, and the five pump tests each hold a thread for the ~13s reconnect probe deadline while running concurrently, so the suite can leave no thread for any pump (the family issue manaflow-ai#12180 tracks). Run the suite serialized so at most one test parks a thread at a time, and bound every wait: stopFiltering now takes a 30s acknowledgement timeout and must succeed, and the EOF/exact reads poll with the same deadline. A pump that never gets scheduled now fails its test inside the batch instead of parking the shard until the idle timeout retries are exhausted. The two assertions in this suite that already fail on main (readUntilEOF == forwardedInput in stdinPumpFiltersReadyInputBeforeStopSignal and stdinPumpKeepsFilteringLateProbeRepliesAfterInitialDrain) are unchanged; the batch classifier tolerates them today. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>

Prepares cmux iOS for a public App Store release under a clean new bundle id, alongside the existing
dev.cmux.app.betadogfood channel. App Store Connect app recordcmux/com.cmux.appand thecmux Distributionprofile are already created; a v1.0 build is uploaded and processing.Changed
ios/cmux/PrivacyInfo.xcprivacywired into the app target.NSPrivacyTracking=false; required-reason APIs UserDefaults (CA92.1) + file-timestamp (DDA9.1); product-interaction analytics label. (No Sentry/IDFA in the iOS app.)appstorelane inios/scripts/upload-testflight.shandios/scripts/cloud-testflight.sh→com.cmux.app, on-device namecmux,cmux Distributionprofile. Reuses the existing release entitlements andcmux-iosURL scheme; beta lane unchanged.com.cmux.appadded toPROD_BUNDLE_IDSinweb/services/apns/routePolicy.ts(+ test) so production pushes route correctly.MobileBuildTypedoc/test note thatcom.cmux.appresolves to.prod(behavior already covered by the else branch).ios/fastlane/(Snapfile/Fastfile/Appfile) capturing en-US + ja on the 6.9" iPhone + 13" iPad classes, driven by the existingCMUX_UITEST_MOCK_DATADEBUG hook viaSnapshotUITests..github/workflows/ios-screenshots.ymlcaptures on a DEBUG build (no signing), resolving the required iPhone/iPad device classes at runtime so it survives Xcode version bumps; optional ASC upload onworkflow_dispatch.Images
ios/fastlane/frame_assets/backgrounds/l/00.jpg
ios/fastlane/frame_assets/backgrounds/l/01.jpg
ios/fastlane/frame_assets/backgrounds/l/02.jpg
ios/fastlane/frame_assets/backgrounds/l/03.jpg
ios/fastlane/frame_assets/backgrounds/l/04.jpg
ios/fastlane/frame_assets/backgrounds/l/05.jpg
ios/fastlane/frame_assets/backgrounds/p/00.jpg
ios/fastlane/frame_assets/backgrounds/p/01.jpg
ios/fastlane/frame_assets/backgrounds/p/02.jpg
ios/fastlane/frame_assets/backgrounds/p/03.jpg
ios/fastlane/frame_assets/backgrounds/p/04.jpg
ios/fastlane/frame_assets/backgrounds/p/05.jpg
ios/fastlane/frame_assets/bg_landscape.jpg
ios/fastlane/frame_assets/bg_portrait.jpg
ios/fastlane/frame_assets/logos/Claude.png
ios/fastlane/frame_assets/logos/Codex.png
ios/fastlane/frame_assets/logos/OpenCode.png
ios/fastlane/frame_assets/logos/Pi.png
Check
web-typecheck/ web tests:normalizeApnsBundle("com.cmux.app")→ production.test-ios: app builds with the privacy manifest bundled.ios-screenshots(this PR triggers capture-only): snapshot test runs and produces iPhone + iPad screenshots in en-US + ja.Docs/metadata note: this is the iOS App Store enablement; no macOS runtime behavior changes.
🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Prod upload and ASC screenshot upload use signing/API secrets with main-only guards; the streamed-validation job is large, long-running, and depends on many CI secrets and seeded fake agent auth.
Overview
Adds three GitHub Actions workflows for public iOS App Store release automation on macOS runners.
ios-appstore-upload.ymlis dispatch-only onmain, with serialized concurrency. It materializes ASC API keys, imports the distribution cert and prod provisioning profile (validatescom.cmux.appandaps-environment=production), then archives and uploads via./ios/scripts/upload-testflight.shwith--lane appstore, manual signing, optional marketing/build numbers, and--skip-notes.ios-screenshots.ymlruns fastlane capture for en-US + ja on runtime-resolved 6.9" iPhone and 13" iPad simulators (DEBUG / mock data, no signing for capture). PRs touching screenshot paths get capture-only; ASC upload is dispatch +upload=true, blocked unlessmainwith no ref override.ios-streamed-validate.ymlis a headless E2E check for the Mac-streamed screenshot path: native Postgres + Next dev, tagged Mac/iOS sim builds, agent CLI seeding, pairing, andcapture-streamed.py, with diagnostic artifacts on failure.Reviewed by Cursor Bugbot for commit d0f3a4f. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Prepares a public iOS App Store build under
com.cmux.appwith a privacy manifest, a parallelized framedfastlanescreenshots pipeline (preserves raw captures and writes an HTML gallery), streamed validation, and a dispatch‑only prod upload. Also fixes the screenshot preview’s terminal keyboard‑height parsing.New Features
--lane appstoreinios/scripts/upload-testflight.shandcloud-testflight.shtargetscom.cmux.appwith thecmux Distributionprofile; APNs routes to production;MobileBuildTyperesolves.prod(test added).ios/cmux/PrivacyInfo.xcprivacy(NSPrivacyTracking=false, required‑reason APIs) and a product‑interaction analytics label.fastlane screenshotscaptures 6.9" iPhone + 13" iPad (dark mode), replays recorded agent sessions with auto‑derived terminal background, shows a real iOS notification banner, stitches a real iPhone frame with an opening mask, renders SF Pro headers with inline agent logos, paints a unified Dynamic Island, varies per‑screen backgrounds, writes an HTML gallery, preserves raw captures alongside framed images, and parallelizes framing for faster CI; workflowsios-screenshots.yml,ios-streamed-validate.yml, andios-appstore-upload.ymladded; prod upload is dispatch‑only onmain; streamed‑validation secrets are restricted tomain.Migration
ios/scripts/upload-testflight.sh --lane appstore(or cloud), or run “iOS App Store (prod upload)” withmarketing_version/build_number.cd ios && fastlane screenshots; to upload, run withupload=trueand ASC API key env; overrides:SNAPSHOT_DEVICES,SNAPSHOT_LANGUAGES,CMUX_UITEST_TERMINAL_TARGET_COLS.Written for commit 8e690b3. Summary will update on new commits.
Summary by CodeRabbit
Release Notes
aps-environment=production.