Skip to content

ci: run iOS TestFlight beta lane every ~2h (prompt internal builds) - #6489

Merged
lawrencecchen merged 1 commit into
mainfrom
feat-ios-testflight-on-push
Jun 20, 2026
Merged

lawrencecchen merged 1 commit into
mainfrom
feat-ios-testflight-on-push

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Why

An iOS change waited up to ~24h to reach internal TestFlight (the lane ran on a nightly cron) — and if that one nightly failed, another day. You wanted iOS changes to reach internal builds promptly.

What

One-line cadence change: run the existing ios-testflight.yml lane every ~2h (17 */2 * * *) instead of nightly. The decide job already SHA-compares HEAD to the last uploaded commit, so each run uploads the latest main only when it has advanced (green skip otherwise).

Why not a push trigger / per-commit (autoreview drove this)

A per-push, build-per-commit lane looked simpler but isn't, and three autoreview rounds proved it:

  • A shared concurrency group cancels superseded pending runs into red checks on intermediate commits (the original reason there was no push trigger).
  • Per-SHA concurrency avoids that but removes the serialization that keeps parallel archives from racing on the timestamp CFBundleVersion (same-second collisions; or an older commit uploading after a newer one with a lower build number).
  • The success-based dedupe can also strand a commit (a run can succeed without uploading).

Running the single, already-serialized per-ref lane more often sidesteps all of that. ~2h spacing exceeds one archive's duration (~30-60m), so runs never overlap and uploads never collide. If faster turnaround is needed later, tighten the interval (kept > one archive's duration) rather than adding a push trigger.

For an immediate beta, workflow_dispatch still works.

🤖 Generated with Claude Code


Note

Low Risk
CI schedule and documentation only; no changes to signing, upload scripts, or dedupe logic.

Overview
Shortens internal TestFlight latency by changing the ios-testflight.yml schedule from once daily (10 9 * * *) to every ~2 hours at minute 17 (17 */2 * * *).

The upload path is unchanged: the decide job still SHA-compares main HEAD to the last successful run and skips when nothing new needs shipping. Expanded workflow comments document why ~2h beats a push trigger (concurrency/cancelled checks, CFBundleVersion races) and why the interval stays longer than a typical archive+upload (~30–60m) so the single serialized per-ref lane does not overlap.

workflow_dispatch remains for on-demand uploads.

Reviewed by Cursor Bugbot for commit aa0de96. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Chores
    • Updated iOS build automation to generate TestFlight releases more frequently (every 2 hours instead of nightly).

@vercel

vercel Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jun 20, 2026 8:10am
cmux-staging Building Building Preview, Comment Jun 20, 2026 8:10am

@coderabbitai

coderabbitai Bot commented Jun 20, 2026 •

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: eff37355-d120-45fb-af1d-d393e8075f86

📥 Commits

Reviewing files that changed from the base of the PR and between 44bf7f6 and aa0de96.

📒 Files selected for processing (1)
  • .github/workflows/ios-testflight.yml

📝 Walkthrough

Walkthrough

The iOS TestFlight workflow's schedule trigger changes from a single nightly cron to every ~2 hours. The associated comments are updated to document how the more frequent schedule enables the existing SHA-based decision logic to retry commits that were previously missed or failed to upload.

Changes

iOS TestFlight schedule cadence and retry documentation

Layer / File(s) Summary
Schedule cadence to every ~2 hours and retry documentation
.github/workflows/ios-testflight.yml
Changes the schedule trigger from nightly (10 9 * * *) to every ~2 hours at :17 (17 */2 * * *). Updates inline comments to explain how the more frequent cadence works with the SHA-based decision logic to automatically retry unuploaded or missed commits.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Possibly related PRs

  • manaflow-ai/cmux#6214: Both PRs modify the same .github/workflows/ios-testflight.yml trigger configuration for the iOS TestFlight CI workflow.
  • manaflow-ai/cmux#5448: Directly relates to the same TestFlight CI workflow and SHA-based decide job logic referenced in this PR's documentation updates.

Poem

🐰 The schedule ticks more often now,
Every two hours, not just a single bow.
The SHA checks catch what slipped away,
Retrying commits without delay.
Twice as frequent, twice as keen! 🚀


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (4 errors)

Check name Status Explanation Resolution
Cmux No Hacky Sleeps ❌ Error PR introduces scripts/ensure-ghosttykit.sh with a polling loop containing sleep 1 to synchronize access to a cache lock, violating the no-hacky-sleeps rule for shared-state races. Replace the polling loop in ensure-ghosttykit.sh with a proper file locking mechanism (flock) or real synchronization primitive instead of sleep 1 polling.
Cmux Swiftpm Lockfiles ❌ Error 36 cmux-owned packages with Package.swift dependency changes lack corresponding Package.resolved lockfiles, violating the policy requiring dependency pin visibility in PR diffs. Include Package.resolved files for all 47 cmux packages with Package.swift files: Packages/Shared/{CMUXAuthCore,CMUXMobileCore,CmuxAgentChat,CmuxSyncStore}, Packages/iOS/{CmuxAgentChatUI,CmuxMobileAnalytics,CmuxMobileBrowser,CmuxMobileCa...
Cmux Source Artifacts ❌ Error The PR adds 26 new files to .claude/ and .agents/ scratch directories (including .claude/scheduled_tasks.lock, .claude/commands/*.md, .claude/skills/*, and .agents/skills), which violat... Remove all .claude/ and .agents/ files from this commit, or add .claude/ and .agents/ patterns to .gitignore if these are intentional workspace files.
Cmux No Test Or Debug Seam In Production Source ❌ Error Production source files added test/debug seams: MobileCoreRPCClient.swift has #if DEBUG extension with debugWithRequestTimeout hook; MobileShellComposite.swift has ForTesting and debugRun... Move test scaffolding to test target via @testable import after widening state from private to internal; remove test-only accessors from production source per PR #6452 reference fix pattern.
✅ Passed checks (19 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a push trigger to the iOS TestFlight workflow on main with path filtering.
Description check ✅ Passed The description covers Why and What sections clearly, explaining the motivation and implementation details, though Testing section and some checklist items are absent.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed No production Swift changes in this PR—only .github/workflows/ios-testflight.yml (YAML) modified. Swift actor isolation check doesn't apply to workflow files.
Cmux Swift Blocking Runtime ✅ Passed PR modifies only .github/workflows/ios-testflight.yml (a YAML workflow file), not Swift production code; blocking runtime check is inapplicable.
Cmux Expensive Synchronous Load ✅ Passed PR modifies only GitHub Actions workflow YAML (.github/workflows/ios-testflight.yml); contains zero Swift production code changes. The custom check for expensive synchronous Swift loaders is not ap...
Cmux Cache Substitution Correctness ✅ Passed Check not applicable: custom check applies to production Swift/TypeScript/JavaScript changes, but PR only modifies .github/workflows/ios-testflight.yml (a YAML workflow file).
Cmux Algorithmic Complexity ✅ Passed The workflow's JavaScript code fetches only 1 API result (per_page: 1, not in a loop) and shell operations work on bounded system directories. No scalable user-data collections are processed, satis...
Cmux Swift Concurrency ✅ Passed Check does not apply: PR modifies only .github/workflows/ios-testflight.yml (GitHub Actions YAML), not cmux-owned Swift code. Check targets Swift concurrency patterns per .github/review-bot-rules/s...
Cmux Swift @Concurrent ✅ Passed The custom check is for Swift code changes, but this PR only modifies a GitHub Actions workflow YAML file (.github/workflows/ios-testflight.yml) with no Swift code changes.
Cmux Swift File And Package Boundaries ✅ Passed PR modifies only .github/workflows/ios-testflight.yml (a YAML workflow file), not any Swift files. The custom check for Swift file and package boundaries is not applicable.
Cmux Swift Logging ✅ Passed PR modifies only .github/workflows/ios-testflight.yml (GitHub Actions YAML), not Swift code. Custom check applies only to "production Swift changes," making it inapplicable here.
Cmux User-Facing Error Privacy ✅ Passed GitHub Actions workflow files are operational runbooks for CI/CD automation. Error messages in workflow logs are developer-only and not shown to end users, which are explicitly allowed by the user-...
Cmux Full Internationalization ✅ Passed The PR modifies only .github/workflows/ios-testflight.yml, a GitHub Actions workflow file that is operational CI/CD infrastructure code not shown to end users, falling under allowed exceptions as "...
Cmux Swiftui State Layout ✅ Passed PR modifies only .github/workflows/ios-testflight.yml (GitHub Actions workflow); no SwiftUI code changes present, so SwiftUI state layout check does not apply.
Cmux Architecture Rethink ✅ Passed PR modifies only .github/workflows/ios-testflight.yml (GitHub Actions YAML), not Swift code. Swift architecture check is not applicable to workflow configuration changes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies only .github/workflows/ios-testflight.yml (GitHub Actions workflow), not Swift code. The check requires Swift window/panel code changes; no Swift files were modified.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-testflight-on-push

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit b3540cc. Configure here.

Comment thread .github/workflows/ios-testflight.yml Outdated
@greptile-apps

greptile-apps Bot commented Jun 20, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

Tightens the iOS TestFlight beta cadence from a once-nightly cron to every two hours (17 */2 * * *) so iOS changes reach internal testers within ~2 hours of landing on main rather than up to 24+ hours. The existing decide job's SHA-based deduplication and the cancel-in-progress: false concurrency setting are unchanged and remain correct for this schedule.

  • Cron only: The sole code change is the schedule expression and its surrounding comments; no logic in the decide or upload jobs was modified.
  • Dedup still works: The 2-hour interval keeps runs well-separated from the ~30–60 minute build+upload window, so the serialized per-ref lane never overlaps and build-number collisions remain impossible.
  • Stale inline comment: The JavaScript // a failed or missed nightly comment inside the decide step was not updated to match the new cadence (see inline comment).

Confidence Score: 5/5

Safe to merge — the change is a single cron expression swap; all deduplication, concurrency, and upload guard logic is untouched.

The only modification is the schedule from nightly to every 2 hours. The decide job's SHA-comparison, the cancel-in-progress: false concurrency queue, and the refs/heads/main guard on the upload job are all unchanged and remain correct for the new cadence. A 2-hour interval keeps scheduled runs comfortably separated from the ~30–60 minute build window, so no collision risk is introduced.

No files require special attention beyond the minor stale inline comment in the decide step's JavaScript.

Important Files Changed

Filename Overview
.github/workflows/ios-testflight.yml Cron schedule changed from nightly (10 9 * * *) to every 2 hours (17 */2 * * *) with updated top-level comments; one inline JS comment still says "nightly" instead of "run".

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant CRON as Cron (17 */2 * * *)
    participant DECIDE as decide job (Linux)
    participant GH as GitHub Actions API
    participant UPLOAD as upload job (macOS)
    participant ASC as App Store Connect

    CRON->>DECIDE: trigger (every ~2h)
    DECIDE->>GH: "listWorkflowRuns(status=success, per_page=1)"
    GH-->>DECIDE: lastUploadedSha
    alt "lastUploadedSha == HEAD SHA"
        DECIDE-->>DECIDE: "should_build = false (skip, green)"
    else HEAD has advanced
        DECIDE-->>UPLOAD: "should_build = true"
        UPLOAD->>UPLOAD: archive + export (~30-60m)
        UPLOAD->>ASC: upload IPA
        ASC-->>UPLOAD: accepted
    end
Loading
%%{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"}}}%%
sequenceDiagram
    participant CRON as Cron (17 */2 * * *)
    participant DECIDE as decide job (Linux)
    participant GH as GitHub Actions API
    participant UPLOAD as upload job (macOS)
    participant ASC as App Store Connect

    CRON->>DECIDE: trigger (every ~2h)
    DECIDE->>GH: "listWorkflowRuns(status=success, per_page=1)"
    GH-->>DECIDE: lastUploadedSha
    alt "lastUploadedSha == HEAD SHA"
        DECIDE-->>DECIDE: "should_build = false (skip, green)"
    else HEAD has advanced
        DECIDE-->>UPLOAD: "should_build = true"
        UPLOAD->>UPLOAD: archive + export (~30-60m)
        UPLOAD->>ASC: upload IPA
        ASC-->>UPLOAD: accepted
    end
Loading

Reviews (4): Last reviewed commit: "ci(ios-testflight): run the beta lane ev..." | Re-trigger Greptile

Comment on lines 108 to 115
const runs = await github.rest.actions.listWorkflowRuns({
owner,
repo,
workflow_id: 'ios-testflight.yml',
status: 'success',
per_page: 1,
});
lastUploadedSha = runs.data.workflow_runs[0]?.head_sha ?? null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 listWorkflowRuns missing branch: 'main' filter

The call queries all successful runs of this workflow regardless of branch. If workflow_dispatch is triggered on a non-main branch (the upload job is guarded by github.ref == 'refs/heads/main', so it skips, but the run still completes as "success"), that run's head_sha becomes the next auto-run's lastUploadedSha. In practice, feature-branch commit SHAs won't collide with main SHAs, so no incorrect skip would occur — but adding branch: 'main' tightens the intent and ensures "last uploaded sha" actually refers to a main commit, not an incidental dispatch SHA from another branch.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread .github/workflows/ios-testflight.yml Outdated
Comment on lines +115 to +118
lastUploadedSha = runs.data.workflow_runs[0]?.head_sha ?? null;
needsBuild = lastUploadedSha !== context.sha;
const alreadyUploaded = lastUploadedSha === context.sha;

needsBuild = isTip && !alreadyUploaded;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 alreadyUploaded conflates "run succeeded" with "upload actually happened"

A run where decide completes with should_build = false (upload skipped) still resolves as status: success, so lastUploadedSha can point to a commit that was never uploaded. The variable and surrounding comment call it "last uploaded sha," but it is really "most recent successful run's sha." In normal forward-only main history this causes no problem — a skipped-upload run only occurs when isTip = false (a newer commit was already tip), and that older commit can't re-appear as context.sha without a force-push. The mismatch only becomes a real skip if main is ever force-pushed backwards. Renaming to lastSuccessfulRunSha and adding a code comment would clarify the intentional limitation without changing behavior.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 283-314: The pinned SHA for the actions/github-script action does
not match the declared v9.0.0 version tag. Update the SHA hash in the Re-verify
HEAD is still the main tip before uploading step (the one using
actions/github-script@) from the current incorrect SHA to the correct SHA that
resolves to v9.0.0, which is d746ffe35508b1917358783b479e04febd2b8f71, to ensure
the action version and its pin are consistent.
🪄 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: 8d121d70-2c27-4c0c-9f81-a640bd1bedce

📥 Commits

Reviewing files that changed from the base of the PR and between b3540cc and e1543b6.

📒 Files selected for processing (1)
  • .github/workflows/ios-testflight.yml

Comment thread .github/workflows/ios-testflight.yml Outdated
An iOS change waited up to ~24h for the nightly TestFlight upload (and stranded
another day if that nightly failed). Run the existing, proven, serialized lane
every ~2h instead, so an iOS-affecting merge reaches internal TestFlight within
~2h. This reuses the per-ref-serialized decide+upload path unchanged (no push
trigger): a per-push lane would either cancel superseded pending runs into red
checks or, with per-SHA concurrency, let parallel archives race on the timestamp
build number. ~2h spacing exceeds one archive's duration (~30-60m) so runs do not
overlap and uploads never collide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lawrencecchen lawrencecchen changed the title ci: internal TestFlight build on every iOS-affecting push to main ci: run iOS TestFlight beta lane every ~2h (prompt internal builds) Jun 20, 2026
@lawrencecchen
lawrencecchen force-pushed the feat-ios-testflight-on-push branch from 44bf7f6 to aa0de96 Compare June 20, 2026 07:54
@lawrencecchen
lawrencecchen merged commit ae0c71e into main Jun 20, 2026
6 of 9 checks passed
@lawrencecchen
lawrencecchen deleted the feat-ios-testflight-on-push branch June 20, 2026 07:56
@lawrencecchen
lawrencecchen restored the feat-ios-testflight-on-push branch July 18, 2026 10:18

This branch was successfully deployed

1 active deployment
Preview – cmux — aa0de96d Deployed Jun 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant