Repository navigation
iOS TestFlight CI/CD + fix Release-archive build (DEBUG-gating from #5079) - #5448
Conversation
New .github/workflows/ios-testflight.yml archives, exports, and uploads the cmux-ios beta lane (bundle id dev.cmux.app.beta) to TestFlight. Triggers: workflow_dispatch (optional build_number/force) and a nightly cron that skips when main has had no new commits in the last 24h. Auth uses the App Store Connect API key from the ASC_API_KEY_ID / ASC_API_ISSUER_ID / ASC_API_KEY_P8_BASE64 secrets, materialized to ~/.appstoreconnect/private_keys. Signing is cloud-managed (automatic) via -allowProvisioningUpdates, so the runner needs no iOS distribution cert/profile in its keychain. upload-testflight.sh gains a --signing manual|automatic flag (default manual; automatic drops the manual signingCertificate/provisioningProfiles and sets signingStyle=automatic). Validated end-to-end: real TestFlight upload succeeded (altool "UPLOAD SUCCEEDED with no errors", Delivery UUID 39be114b-d1df-4392-8dcb-32f9708f3f16). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`visibleTerminalSnapshot()` (Copy Debug Logs) is non-DEBUG and public (called from WorkspaceDetailView), but `surfaceText` was inside `#if DEBUG`, so the iOS app compiled in Debug (simulator CI, reload) yet failed the Release archive with "cannot find 'surfaceText' in scope". A latent main regression from #5079 that only a Release archive exercises. Move `surfaceText` (pure libghostty read) out of the DEBUG block; the DEBUG-only accessibility helpers still call it. Keeps Copy Debug Logs working in TestFlight betas. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…chive
TerminalLayoutPreviewView is a DEBUG-only screenshot harness (mounted only when
CMUX_UITEST_TERMINAL_PREVIEW=1) that calls the DEBUG-only test seams
debugSetKeyboardHeightForLayoutPreview / debugShowZoomControlOverlayForPreview,
but it was gated only by `#if canImport(UIKit)`, so it shipped in Release and
failed the archive ("no member ...ForLayoutPreview"). Gate the file and its
CMUXMobileRootView mount points (shouldShowTerminalLayoutPreview /
terminalLayoutPreview) with `&& DEBUG`; Release already had the `#else`
false/EmptyView fallbacks. Debug behavior unchanged. With the surfaceText
un-gate, cmux-ios now compiles in Release (verified BUILD SUCCEEDED + a real
TestFlight upload).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a scheduled/manual TestFlight upload workflow, extends the upload script with manual/automatic signing support, and makes terminal preview UI compile only in DEBUG while keeping the release snapshot helper available. ChangesiOS TestFlight & Release Build Pipeline
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 18 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (18 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 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 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 458d1001bc
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (!forceBuild && context.eventName === 'schedule') { | ||
| const since = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); | ||
| const commits = await github.rest.repos.listCommits({ | ||
| owner, | ||
| repo, | ||
| sha: 'main', | ||
| since, | ||
| per_page: 1, |
There was a problem hiding this comment.
Widen the scheduled commit window
For scheduled runs, this checks only commits newer than exactly 24 hours before the job actually starts. GitHub documents that schedule workflows can be delayed or even dropped under load, so a commit that lands just after yesterday's 09:10 run can be older than 24 hours by the time today's delayed run executes, causing the nightly TestFlight upload to be skipped even though that commit was never uploaded. Consider using a wider overlap window or comparing against the last successful upload/run instead of Date.now() - 24h.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR adds an automated TestFlight delivery pipeline for the cmux iOS beta and fixes two
Confidence Score: 5/5Safe to merge. The Swift compilation fixes are correct and narrowly scoped; the new workflow has been validated end-to-end per the PR description. The Swift changes are compile-gating only with no runtime behavior change in Release beyond fixing the broken archive. The workflow is dispatch + nightly only, not PR-triggered, so merging carries no risk of accidental TestFlight submissions. .github/workflows/ios-testflight.yml — the decide job listWorkflowRuns call would benefit from a branch filter to avoid spurious nightly uploads after manual feature-branch dispatches. Important Files Changed
Reviews (3): Last reviewed commit: "ci: add human-readable job names to the ..." | Re-trigger Greptile |
| const commits = await github.rest.repos.listCommits({ | ||
| owner, | ||
| repo, | ||
| sha: 'main', | ||
| since, | ||
| per_page: 1, | ||
| }); | ||
| recentCount = commits.data.length; | ||
| hasRecentCommits = recentCount > 0; |
There was a problem hiding this comment.
Commit check hardcodes
sha: 'main'
listCommits is called with sha: 'main' unconditionally. If the repository's default branch is ever renamed, the nightly skip logic silently breaks — listCommits for an unknown branch returns an empty array, so hasRecentCommits is always false and the nightly build never fires. Using context.payload.repository.default_branch (available in github-script) keeps this resilient to a branch rename.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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-testflight.yml:
- Around line 78-92: Add a human-readable name field to the upload job by
inserting a "name: Upload" (or a more descriptive title like "Upload to
TestFlight") directly under the upload job key so the GitHub Actions UI shows a
clear label for the job; target the upload job block (the job named upload) and
add the name property at the top of that block alongside the existing
needs/if/runs-on entries.
- Around line 32-77: The job with id "decide" is missing a human-readable name
for GitHub Actions UI; add a name field to that job (for example "Decide whether
a TestFlight upload is needed") directly under the "decide:" job header so the
workflow shows a descriptive title in the Actions UI—update the job definition
where "decide" is declared (the job that runs actions/github-script@... and sets
output should_build) to include the name property.
🪄 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: ac1213ee-1d36-44ab-82df-69bada82e58c
📒 Files selected for processing (5)
.github/workflows/ios-testflight.ymlPackages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/CMUXMobileRootView.swiftPackages/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalLayoutPreviewView.swiftPackages/CmuxMobileTerminal/Sources/CmuxMobileTerminal/GhosttySurfaceView.swiftios/scripts/upload-testflight.sh
The scheduled gate compared commit dates against a moving 24h wall-clock window, so a failed or missed nightly could permanently strand the latest commit: the next run would see zero commits in the window and skip a SHA that was never uploaded. Gate instead on whether the current main HEAD already matches the head_sha of the most recent successful run of this workflow. A successful run either uploaded HEAD or correctly skipped an already-uploaded HEAD, so its head_sha is always an uploaded commit; a failed run is not "success", so the last-success SHA stays older and the next run retries. Adds actions: read so decide can query run history. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes 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 e44b49d. Configure here.
| per_page: 1, | ||
| }); | ||
| lastUploadedSha = runs.data.workflow_runs[0]?.head_sha ?? null; | ||
| needsBuild = lastUploadedSha !== context.sha; |
There was a problem hiding this comment.
Skip logic ignores branch
Medium Severity
The scheduled skip check compares main’s HEAD to the latest successful run’s head_sha across all branches. After main is uploaded, a later successful manual run from another branch makes nightly jobs think main is new and re-upload the same commit, contradicting the “skip when HEAD already uploaded” behavior and wasting CI and build numbers.
Reviewed by Cursor Bugbot for commit e44b49d. Configure here.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e44b49de6a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| concurrency: | ||
| group: ios-testflight-${{ github.ref_name }} | ||
| # Queue concurrent runs instead of canceling them so no upload is lost. | ||
| cancel-in-progress: false |
There was a problem hiding this comment.
Queue all pending TestFlight uploads
In this workflow, cancel-in-progress: false does not actually queue every requested upload: GitHub Actions concurrency groups still keep only one pending run by default, and a third run for the same ref cancels the older pending run unless the new queue setting is enabled. If a TestFlight upload is running and two manual dispatches or delayed scheduled runs for main arrive, the middle requested upload can be dropped despite the comment's "no upload is lost" guarantee; add an explicit multi-run queue (for example queue: max) or remove the workflow-level concurrency if every dispatch must upload.
Useful? React with 👍 / 👎.
Addresses CodeRabbit: label the decide/upload jobs so the Actions UI shows descriptive titles instead of the raw job ids. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>


Adds GitHub Actions CI/CD that ships the cmux iOS app to TestFlight, and fixes the iOS Release-archive build regression that building it in Release exposed.
TestFlight workflow (
.github/workflows/ios-testflight.yml)workflow_dispatch(optionalbuild_number/force) + a nightly cron that skips whenmainhas had no new commits in the last 24h (a cheap ubuntudecidejob), so it doesn't burn build numbers on unchanged code.macos-26, Xcode select +submodules: recursive+install-zig-ci.sh+ensure-ghosttykit.sh(mirrorstest-ios.yml), thenios/scripts/upload-testflight.sh --lane beta --signing automatic(bundle iddev.cmux.app.beta, internal TestFlight).-allowProvisioningUpdates+ the App Store Connect API key, so the runner needs no iOS distribution cert/profile in its keychain — only the three ASC secrets (ASC_API_KEY_ID,ASC_API_ISSUER_ID,ASC_API_KEY_P8_BASE64).upload-testflight.shgains a--signing manual|automaticflag (defaultmanual, preserving local-keychain exports).Validated end-to-end on the branch (temp push trigger, since removed): real archive → cloud-signed export →
altoolUPLOAD SUCCEEDED with no errors, Delivery UUID39be114b-d1df-4392-8dcb-32f9708f3f16, ~3.5 min.iOS Release-archive fix (⚠️ this was also breaking main's nightly/release iOS)
test-ios.ymlonly builds Debug/simulator, so two DEBUG-only-used-from-non-DEBUG leaks from the #5079 iOS-mobile merge went unnoticed and broke the Release archive:GhosttySurfaceView.surfaceTextwas#if DEBUGbut the non-DEBUG publicvisibleTerminalSnapshot()(Copy Debug Logs, ships in betas) calls it → un-gated the helper.TerminalLayoutPreviewView(UITest screenshot scaffolding, mounted only whenCMUX_UITEST_TERMINAL_PREVIEW=1) was gated only by#if canImport(UIKit)and called DEBUG-onlydebug*ForLayoutPreviewseams → DEBUG-gated the file + its CMUXMobileRootView mount points.cmux-iosnow compiles in Release (verifiedBUILD SUCCEEDED+ the real upload above). Debug behavior is unchanged.Notes
ios-testflightworkflow doesn't run on PRs (dispatch + nightly only); it's validated above.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Touches release signing/export and CI secrets for App Store Connect; Swift changes are compile-gating only with no intended runtime behavior change in Release beyond fixing the broken archive.
Overview
Adds automated TestFlight delivery for the iOS beta (
dev.cmux.app.beta) via a new GitHub Actions workflow: manual dispatch or a nightly cron, with a lightweightdecidejob that skips scheduled uploads whenmainhas no commits in the last 24h. The macOS job provisions GhosttyKit, materializes App Store Connect API keys, and runsupload-testflight.shwith--signing automaticso CI can export without a distribution cert in the keychain.upload-testflight.shgains--signing manual|automatic(default manual): automatic export omits manual cert/profile plist entries and relies on cloud-managed signing with-allowProvisioningUpdates.Release archive fixes exposed by shipping Release builds:
GhosttySurfaceView.surfaceTextis no longer#if DEBUG-only so release Copy Debug Logs /visibleTerminalSnapshot()compiles; UITest terminal layout preview (TerminalLayoutPreviewViewand its mount inCMUXMobileRootView) is restricted to DEBUG so Release does not pull DEBUG-only preview seams.Reviewed by Cursor Bugbot for commit 458d100. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Automates TestFlight uploads for the cmux iOS beta and fixes Release-archive failures from DEBUG-only code. Manual and nightly uploads use cloud-managed signing; nightly skips only when the current
mainHEAD was already uploaded. Release/TestFlight builds compile again.New Features
iOS TestFlight (beta)workflow at.github/workflows/ios-testflight.ymlto archive, export, and upload thebetalane (dev.cmux.app.beta).workflow_dispatchwith optionalbuild_number) and nightly; gate uses the last successful run’shead_shaso missed/failed runs retry. Addsactions: readto query run history.ASC_API_KEY_ID,ASC_API_ISSUER_ID,ASC_API_KEY_P8_BASE64. No distribution cert/profile on the runner.ios/scripts/upload-testflight.shsupports--signing automatic(CI) in addition tomanual(default).Bug Fixes
GhosttySurfaceView.surfaceTextsovisibleTerminalSnapshot()works in Release; fixes Release/TestFlight archive failures.TerminalLayoutPreviewViewand its mounts inCMUXMobileRootViewto avoid DEBUG-only seams in Release.cmux-iosnow compiles in Release; nightly/release iOS and TestFlight builds succeed.Written for commit 1711183. Summary will update on new commits.
Summary by CodeRabbit
New Features
Improvements