Skip to content

Auto-submit external TestFlight beta reviews - #7464

Merged
azooz2003-bit merged 10 commits into
mainfrom
feat-ios-external-beta-submit
Jul 6, 2026
Merged

azooz2003-bit merged 10 commits into
mainfrom
feat-ios-external-beta-submit

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 6, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • extend external TestFlight distribution to submit builds for Beta App Review when Apple reports READY_FOR_BETA_SUBMISSION
  • keep the external distribution helper idempotent for already-submitted and already-approved builds
  • document the new main-tracking behavior and cover the submission logic in workflow helper tests

Testing

  • python3 tests/test_ios_testflight_external_distribution.py
  • bash -n ios/scripts/upload-testflight.sh
  • python3.9 -m py_compile ios/scripts/asc_assign_external_testflight_group.py

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Auto-submits external TestFlight builds for Beta App Review when Apple marks a new MARKETING_VERSION as READY_FOR_BETA_SUBMISSION, removing a manual step and speeding up external releases. The flow is idempotent, resilient to ASC delays, handles sibling-review races, and continues to auto-assign builds to the external group.

  • New Features
    • asc_assign_external_testflight_group.py now polls ASC and creates/reuses the submission for READY_FOR_BETA_SUBMISSION; it treats APPROVED/IN_REVIEW/WAITING_FOR_REVIEW as success, no-ops when the build is already approved/testable, waits if a same-version sibling build is in review, retries transient lookup/submit errors, and fails on unexpected states; writes an assign state to ${CMUX_TESTFLIGHT_ASSIGN_STATE_OUT_FILE}.
    • The workflow uploads ios-testflight-assignment-state-{complete|pending} artifacts and the schedule uses them to retry assignment-only when sibling reviews block; if artifacts have expired (30d), missing state is treated as complete to avoid permanent red.
    • Adds a one-time marketing_version_override path to upload latest main under an approved MARKETING_VERSION (skips notes; disallows build_number); publishes ios-testflight-build-metadata-override and excludes override runs from canonical “last uploaded” detection.
    • Raises external distribution timeout to 40m; updates ios/README.md, workflow summaries, and upload-testflight.sh to reflect automatic submission; expands tests to cover polling/retry, sibling-review handling, submission races, and delayed metadata.

Written for commit 85a04aa. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an optional manual marketing-version override for re-uploading the current iOS build without triggering a new Beta App Review cycle.
  • Documentation
    • Updated TestFlight automation docs and command help to clearly describe automatic Beta App Review submission when builds are ready, plus required API key inputs.
  • Bug Fixes
    • Improved external distribution reliability by better detecting when Beta App Review submissions already exist, reusing/creating them as appropriate, and refining upload/assignment behavior.
  • Tests
    • Expanded and stabilized coverage for external review submission paths, retries, idempotency, and race/error handling.

@vercel

vercel Bot commented Jul 6, 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 Jul 6, 2026 10:12pm
cmux-staging Building Building Preview, Comment Jul 6, 2026 10:12pm

@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a marketing-version override for iOS TestFlight uploads, routes external uploads through automatic beta app review submission handling, persists assignment state through the workflow, expands tests for the new control flow, and updates related docs and help text.

Changes

Beta App Review Auto-Submission

Layer / File(s) Summary
Workflow override and external upload routing
.github/workflows/ios-testflight.yml
Adds marketing_version_override, switches upload behavior based on it, updates metadata and assignment-state handling, and adjusts retry/timeout logic.
Submission helpers and main flow
ios/scripts/asc_assign_external_testflight_group.py
Adds ASC lookup and submission helpers, then invokes beta review submission checks from the assignment flow and writes ensured state output.
Tests for submission and race handling
tests/test_ios_testflight_external_distribution.py
Expands coverage for submission creation, reuse, retries, sibling-build races, and unexpected external states.
Documentation and help text
ios/README.md, ios/scripts/upload-testflight.sh
Updates README and script help/warnings to describe automatic beta app review submission and required ASC API inputs.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Workflow as ios-testflight.yml
  participant AssignScript as asc_assign_external_testflight_group.py
  participant ASC as App Store Connect API

  Workflow->>AssignScript: assign uploaded build
  AssignScript->>ASC: read build beta detail
  alt READY_FOR_BETA_SUBMISSION
    AssignScript->>ASC: create or reuse beta app review submission
    AssignScript-->>Workflow: assignment_state = pending or complete
  else already active
    AssignScript-->>Workflow: assignment_state = complete
  else unexpected state
    AssignScript-->>Workflow: raise RuntimeError
  end
Loading

Possibly related PRs

  • manaflow-ai/cmux#7419: Introduces the external TestFlight assignment flow that this PR extends with beta app review submission handling.

Suggested reviewers: lawrencecchen


Important

Pre-merge checks failed

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

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error The retry timeout now embeds raw recovery exceptions (recovery lookup failed: {recovery_exc}) in user-facing RuntimeErrors, exposing upstream/implementation details. Remove raw recovery exception text from timeout/error messages; surface a generic retry timeout and log the detailed ASC failure only in internal telemetry.
Description check ⚠️ Warning The description covers summary and testing, but omits the Demo Video, Review Trigger, and Checklist sections from the template. Add the missing template sections: Demo Video, Review Trigger block, and Checklist, and complete the checklist items.
✅ Passed checks (23 passed)
Check name Status Explanation
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 Diff only touches workflow/Python/tests; no Swift files changed in the commit, so no actor-isolation issues were introduced.
Cmux Swift Blocking Runtime ✅ Passed No Swift files changed in the PR, so the blocking-runtime rule for production Swift code is not applicable.
Cmux Browser Automation Off-Main ✅ Passed PR only changes iOS TestFlight workflow/scripts/tests; no browser.*, WebKit, socketWorkerMethods, or mainActor routing changes found.
Cmux Expensive Synchronous Load ✅ Passed The PR only changes workflow, Python, tests, and docs; no Swift production code or main-actor interactive loader paths were modified.
Cmux Cache Substitution Correctness ✅ Passed No production Swift/TS/JS files changed; the new workflow/script state path handles missing artifacts and 30-day expiry, so no stale-cache substitution issue.
Cmux No Hacky Sleeps ✅ Passed The only new waits are bounded poll/retry loops in a dedicated ASC helper with tests; workflow YAML is out of scope, and no ad hoc shell/runtime sleeps were added.
Cmux Algorithmic Complexity ✅ Passed PASS: The PR only adds bounded artifact/state lookups in CI scripts; no new nested scans or per-target rescans over scalable user-owned collections.
Cmux Swift Concurrency ✅ Passed The PR changes only workflow/docs/scripts/tests; no Swift source files were touched, so the Swift concurrency rule is not applicable.
Cmux Swift @Concurrent ✅ Passed No Swift files are changed in the PR diff, so the Swift @concurrent rule is not applicable.
Cmux Swift File And Package Boundaries ✅ Passed PASS — PR only touches workflow, docs, Python, and shell scripts; no production Swift files or package-boundary risks are introduced.
Cmux Swiftpm Lockfiles ✅ Passed Diff only touches workflow/Python/tests; no Package.resolved, .gitignore, or Xcode package-reference files changed, so the SwiftPM lockfile rule isn’t triggered.
Cmux Swift Logging ✅ Passed PASS: The diff only changes workflow, Python, shell, and tests; no production Swift files or new logging calls are present, so the Swift-logging rule isn’t implicated.
Cmux Full Internationalization ✅ Passed Changes touch workflow, scripts, tests, and internal iOS docs only; no Swift string catalogs, web locale files, or user-facing app/web copy were changed.
Cmux Swiftui State Layout ✅ Passed The diff only touches workflow, Python, and test files; no SwiftUI view/state code or layout boundaries were changed.
Cmux Architecture Rethink ✅ Passed No Swift source changes are present; the diff only touches workflow, Python, Bash, docs, and tests, so the Swift architectural rethink rule is not applicable.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No Swift files or window-controller code are changed in this PR; the diff only touches workflow, Python, shell, and tests.
Cmux Source Artifacts ✅ Passed All changed paths are intentional workflow/source/test files; no temp dirs, logs, caches, build output, or other source-control artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No Swift production Sources/ files changed; the PR only touches workflow, docs, Python/shell scripts, and tests, so the seam rule doesn’t apply.
Cmux No Ambient Global State ✅ Passed No production Swift files were changed; the commit only modifies workflow, Python, tests, and README, so the rule doesn’t apply.
Title check ✅ Passed The title clearly summarizes the main change: automatic external TestFlight beta review submission.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-ios-external-beta-submit

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.

@greptile-apps

greptile-apps Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR automates the Apple Beta App Review submission step for external TestFlight distribution, removing the manual "Ready to Submit" action that previously blocked founders from receiving a new MARKETING_VERSION. It also adds a marketing_version_override escape hatch for uploading current main under an already-approved version without triggering a new review cycle.

  • asc_assign_external_testflight_group.py: New _ensure_external_review_submission function polls ASC for externalBuildState, creates the beta review submission when Apple reports READY_FOR_BETA_SUBMISSION, treats IN_REVIEW/WAITING_FOR_REVIEW/APPROVED as idempotent success, and handles sibling-review races by returning a sibling_review_pending state that lets CI retry without turning the run red.
  • .github/workflows/ios-testflight.yml: Tracks assignment outcome via new ios-testflight-assignment-state-{complete|pending} artifacts; the decide job reads these to retry assignment-only when a sibling review blocks, with a 30-day expiry fallback to prevent permanent retry loops on aged-out artifacts. Override runs write a separate ios-testflight-build-metadata-override artifact so they are excluded from canonical SHA detection.
  • Tests: Expanded to cover the full polling/retry state machine including transient failures, sibling-review races, and double-transient recovery.

Confidence Score: 5/5

Safe to merge. The new submission logic is well-guarded, idempotent, and covered by tests; the workflow state-tracking changes have sound backward-compatibility handling for pre-migration runs and aged-out artifacts.

The core state machine in _ensure_external_review_submission correctly handles all active build states, existing submissions, sibling races, transient API failures, and deadline expiry. Deadline resets are intentional and fit within the raised 40-minute job timeout. The artifact-based state detection in the decide job has solid backward-compatibility logic. The only notable gap is the artifact listing capped at 100 entries without pagination, which is a theoretical concern for runs with an unusually large artifact count but not a real risk in this workflow.

.github/workflows/ios-testflight.yml — the listWorkflowRunArtifacts call at the artifact-name detection block uses per_page: 100 without a pagination loop; all other files are clean.

Important Files Changed

Filename Overview
.github/workflows/ios-testflight.yml Adds marketing_version_override dispatch input, override-run exclusion from canonical SHA detection, artifact-based assignment-state tracking (pending/complete) with 30-day expiry fallback, and the assignment-state artifact upload step. The logic is well-reasoned with good inline documentation; the artifact fetch for state detection caps at 100 without pagination, which is a theoretical correctness gap.
ios/scripts/asc_assign_external_testflight_group.py New _ensure_external_review_submission function adds deadline-driven polling to create/reuse the Beta App Review submission. State machine and retry logic are thorough; handles sibling-review races, transient lookup failures, and recovery after failed submits. Minor PEP 8 blank-line nit between two top-level function definitions.
tests/test_ios_testflight_external_distribution.py Expanded test coverage exercises the key branches of _ensure_external_review_submission: happy path, idempotent re-entry, transient lookup/submit failures, sibling-review races, and approved/rejected terminal states. Module-level function monkeypatching pattern is consistent with pre-existing test harness style.
ios/scripts/upload-testflight.sh Documentation and warning message updates only to reflect that Beta App Review submission is now automatic; no logic changes.
ios/README.md Documentation-only update describing the automatic Beta App Review submission behaviour; no functional changes.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Scheduler
    participant decide_job
    participant upload_job
    participant external_dist_job
    participant ASC

    Scheduler->>decide_job: Trigger (schedule / dispatch)
    decide_job->>decide_job: Scan prior runs for last canonical SHA
    decide_job-->>upload_job: "should_build=true (or should_assign_only=true)"

    opt Normal upload path
        upload_job->>upload_job: Build IPA
        upload_job->>ASC: Upload build
        upload_job->>upload_job: Persist ios-testflight-build-metadata artifact
    end

    upload_job-->>external_dist_job: BUILD_NUMBER

    external_dist_job->>ASC: "Find build / wait for processingState=VALID"
    external_dist_job->>ASC: Assign build to external group
    external_dist_job->>ASC: GET buildBetaDetail (externalBuildState?)

    alt externalBuildState in ACTIVE_EXTERNAL_BUILD_STATES
        external_dist_job->>external_dist_job: "state = current_build_active"
    else submission already exists (APPROVED / IN_REVIEW / WAITING)
        external_dist_job->>external_dist_job: "state = current_build_review_pending"
    else "externalBuildState = READY_FOR_BETA_SUBMISSION"
        external_dist_job->>ASC: Check sibling builds for active review
        alt Sibling in WAITING_FOR_REVIEW or IN_REVIEW
            external_dist_job->>external_dist_job: "state = sibling_review_pending"
        else No blocking sibling
            external_dist_job->>ASC: POST /v1/betaAppReviewSubmissions
            external_dist_job->>external_dist_job: "state = submitted_beta_review"
        end
    end

    external_dist_job->>external_dist_job: Write state artifact (pending or complete)

    opt "state = sibling_review_pending"
        Scheduler->>decide_job: Next schedule tick
        decide_job->>decide_job: "Detect pending artifact → should_assign_only=true"
        decide_job-->>external_dist_job: Re-run assignment only
    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 Scheduler
    participant decide_job
    participant upload_job
    participant external_dist_job
    participant ASC

    Scheduler->>decide_job: Trigger (schedule / dispatch)
    decide_job->>decide_job: Scan prior runs for last canonical SHA
    decide_job-->>upload_job: "should_build=true (or should_assign_only=true)"

    opt Normal upload path
        upload_job->>upload_job: Build IPA
        upload_job->>ASC: Upload build
        upload_job->>upload_job: Persist ios-testflight-build-metadata artifact
    end

    upload_job-->>external_dist_job: BUILD_NUMBER

    external_dist_job->>ASC: "Find build / wait for processingState=VALID"
    external_dist_job->>ASC: Assign build to external group
    external_dist_job->>ASC: GET buildBetaDetail (externalBuildState?)

    alt externalBuildState in ACTIVE_EXTERNAL_BUILD_STATES
        external_dist_job->>external_dist_job: "state = current_build_active"
    else submission already exists (APPROVED / IN_REVIEW / WAITING)
        external_dist_job->>external_dist_job: "state = current_build_review_pending"
    else "externalBuildState = READY_FOR_BETA_SUBMISSION"
        external_dist_job->>ASC: Check sibling builds for active review
        alt Sibling in WAITING_FOR_REVIEW or IN_REVIEW
            external_dist_job->>external_dist_job: "state = sibling_review_pending"
        else No blocking sibling
            external_dist_job->>ASC: POST /v1/betaAppReviewSubmissions
            external_dist_job->>external_dist_job: "state = submitted_beta_review"
        end
    end

    external_dist_job->>external_dist_job: Write state artifact (pending or complete)

    opt "state = sibling_review_pending"
        Scheduler->>decide_job: Next schedule tick
        decide_job->>decide_job: "Detect pending artifact → should_assign_only=true"
        decide_job-->>external_dist_job: Re-run assignment only
    end
Loading

Reviews (3): Last reviewed commit: "Harden external TestFlight review retrie..." | Re-trigger Greptile

Comment on lines 416 to 424
@@ -321,6 +419,8 @@ def main() -> int:
f"asc_assign_external_testflight_group: assigned build {args.build_number} to "
f"{_describe_group(target_group)}"
)
token = _token()
_ensure_external_review_submission(token, build_id, args.build_number)
return 0

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 Redundant _token() call: in the two early-return paths the token is refreshed once and reused for _ensure_external_review_submission, but in the _assign_build path a second token is minted immediately after the first — before _assign_build makes a single fast HTTP call. _assign_build finishes in under a second, so the first token (valid for 600 s) is still fresh when _ensure_external_review_submission starts. The extra _token() invocation unnecessarily spawns a second openssl dgst subprocess and creates two throw-away JWTs.

Suggested change
token = _token()
_assign_build(token, target_group["id"], build_id)
print(
f"asc_assign_external_testflight_group: assigned build {args.build_number} to "
f"{_describe_group(target_group)}"
)
_ensure_external_review_submission(token, build_id, args.build_number)
return 0

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 on lines +327 to +347
if external_state in ("READY_FOR_BETA_TESTING", "IN_BETA_REVIEW", "WAITING_FOR_BETA_REVIEW"):
print(
"asc_assign_external_testflight_group: build "
f"{build_number} external state is {external_state}"
)
return

submission = _beta_review_submission(token, build_id)
if submission is not None:
print(
"asc_assign_external_testflight_group: build "
f"{build_number} external state is {external_state or 'unknown'} with submission "
f"{submission['id']} (state={submission['beta_review_state'] or 'unknown'})"
)
return

raise RuntimeError(
"build "
f"{build_number} is in unexpected externalBuildState={external_state or '<empty>'} "
"with no beta app review submission"
)

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 Missing APPROVED / other intermediate states in the whitelist

_ensure_external_review_submission only explicitly passes through READY_FOR_BETA_TESTING, IN_BETA_REVIEW, and WAITING_FOR_BETA_REVIEW. Any other externalBuildState that doesn't carry an existing submission will raise a fatal RuntimeError. Two edge cases worth covering: (1) EXPIRED — a build older than 90 days has no submission, so a delayed or retried CI run would fail with an "unexpected externalBuildState" error. (2) PROCESSING_EXCEPTION — Apple can set this even after processingState=VALID in transient failure scenarios; the current code would permanently fail that CI run rather than allowing a retry. Both are fail-loud by design, but logging a warning and returning instead of raising for EXPIRED and exception states would avoid permanently poisoning those runs.

@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 `@tests/test_ios_testflight_external_distribution.py`:
- Around line 92-135: Add test coverage for the approved idempotent path in the
external review flow by extending the existing tests around
_ensure_external_review_submission, _build_beta_detail, and
_beta_review_submission. Stub external_build_state as BETA_APPROVED (and
optionally IN_BETA_TESTING) with both no existing submission and an existing
submission, and assert the function neither raises nor calls _submit_beta_review
again. Keep the new checks alongside the current submission, dedup, skip, and
failure cases so the intended already-approved behavior is locked in.
🪄 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: 948fb75b-99c7-418a-b528-850d8d1aadba

📥 Commits

Reviewing files that changed from the base of the PR and between cc20a51 and 7e09960.

📒 Files selected for processing (5)
  • .github/workflows/ios-testflight.yml
  • ios/README.md
  • ios/scripts/asc_assign_external_testflight_group.py
  • ios/scripts/upload-testflight.sh
  • tests/test_ios_testflight_external_distribution.py

Comment thread tests/test_ios_testflight_external_distribution.py
Comment on lines +437 to +453
except RuntimeError as exc:
submission = _beta_review_submission(token, build_id)
if submission is not None:
review_state = submission["beta_review_state"]
if review_state in ACTIVE_BETA_REVIEW_STATES:
print(
"asc_assign_external_testflight_group: build "
f"{build_number} external state is READY_FOR_BETA_SUBMISSION with submission "
f"{submission['id']} (state={review_state or 'unknown'})"
)
return
raise RuntimeError(
"build "
f"{build_number} external state is READY_FOR_BETA_SUBMISSION with beta app review "
f"submission {submission['id']} in unexpected betaReviewState={review_state or '<empty>'}"
)
sibling_submission = _find_active_review_submission_on_sibling_build(token, build_id)

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.

P1 Uncaught RuntimeError breaks the retry loop on double transient failure

Inside the except RuntimeError handler for a failed _submit_beta_review, the recovery calls to _beta_review_submission (line 438) and _find_active_review_submission_on_sibling_build (line 453) are themselves unprotected. If either raises a transient RuntimeError (e.g., ASC returns 5xx during an outage), it escapes the inner except block entirely — bypassing the deadline check, time.sleep, and retry logic — and propagates up to kill the CI job. The outer try/except at the top of the loop only guards _build_beta_detail and the initial _beta_review_submission, not these recovery calls. Wrapping those two calls in their own try/except RuntimeError (treating a failure the same as "nothing found yet" and continue-ing after the deadline check) would close the gap.

@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: 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 `@ios/scripts/asc_assign_external_testflight_group.py`:
- Around line 434-464: The submit-recovery lookups in
asc_assign_external_testflight_group.py can fail transiently and escape the
retry loop; wrap the _beta_review_submission() and
_find_active_review_submission_on_sibling_build() calls inside the existing
try/except retry path in the submission loop so RuntimeError from those helpers
is treated like _submit_beta_review() failures, preserving the sleep, token
refresh, and continue behavior before the deadline.
- Around line 39-47: The state sets for the external TestFlight flow are missing
approved terminal values, so reruns on already-approved builds are not treated
as idempotent success. Update the state handling in
asc_assign_external_testflight_group.py by adding the approved external build
states and beta review state to the existing ACTIVE_EXTERNAL_BUILD_STATES and
ACTIVE_BETA_REVIEW_STATES checks, using the existing assignment/short-circuit
logic in asc_assign_external_testflight_group so those cases return early as
already-complete.
🪄 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: 25e03cc3-f995-4726-8464-36f762b3e8c5

📥 Commits

Reviewing files that changed from the base of the PR and between 7e09960 and 44b10e8.

📒 Files selected for processing (3)
  • .github/workflows/ios-testflight.yml
  • ios/scripts/asc_assign_external_testflight_group.py
  • tests/test_ios_testflight_external_distribution.py

Comment thread ios/scripts/asc_assign_external_testflight_group.py
Comment thread ios/scripts/asc_assign_external_testflight_group.py

@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: 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:
- Line 531: Move CMUX_TESTFLIGHT_ASSIGN_STATE_OUT_FILE out of the
jobs.assign-external-group env block and into the “Assign uploaded build to the
external beta group” step scope so runner.temp resolves correctly. Update the
relevant step-level env for the assign flow, keeping the state file path tied to
runner.temp where it is available, and leave the rest of the workflow env
unchanged.

In `@ios/scripts/asc_assign_external_testflight_group.py`:
- Around line 458-489: The retry loop in asc_assign_external_testflight_group.py
is swallowing the deliberate RuntimeError raised for an unexpected
betaReviewState inside the submission check, causing terminal states to be
retried as if they were transient lookup failures. Update the try/except around
_beta_review_submission and _find_active_review_submission_on_sibling_build so
that only recovery/lookup errors are handled by the inner except RuntimeError as
recovery_exc, and let the unexpected betaReviewState branch propagate
immediately. Keep the existing current_build_review_pending and
sibling_review_pending paths unchanged, but make sure the RuntimeError from the
unexpected betaReviewState in this block is not converted into last_submit_error
or retried.
🪄 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: 8d7dcf46-da62-400d-b08c-390aa2f39888

📥 Commits

Reviewing files that changed from the base of the PR and between 44b10e8 and 85a04aa.

📒 Files selected for processing (3)
  • .github/workflows/ios-testflight.yml
  • ios/scripts/asc_assign_external_testflight_group.py
  • tests/test_ios_testflight_external_distribution.py

SHOULD_ASSIGN_ONLY: ${{ needs.decide.outputs.should_assign_only }}
LAST_UPLOADED_RUN_ID: ${{ needs.decide.outputs.last_uploaded_run_id }}
BUILD_NUMBER: ${{ needs.upload.outputs.final_build_number }}
CMUX_TESTFLIGHT_ASSIGN_STATE_OUT_FILE: ${{ runner.temp }}/ios-testflight-assign-state.txt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the relevant section around the reported line and the artifact upload path.
nl -ba .github/workflows/ios-testflight.yml | sed -n '500,590p'

Repository: manaflow-ai/cmux

Length of output: 194


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check the workflow file around the reported line and surrounding job structure.
nl -ba .github/workflows/ios-testflight.yml | sed -n '500,590p'

Repository: manaflow-ai/cmux

Length of output: 194


🌐 Web query:

GitHub Actions runner context job-level env available runner.temp documentation

💡 Result:

The runner context is not available within the job-level env clause [1]. Because the env block is processed before the job is sent to the runner, you cannot use context expressions like ${{ runner.temp }} at that level [2][3]. Instead, runner-specific information such as RUNNER_TEMP is provided as a default environment variable available directly to the shell [4][5]. You can access it using standard shell syntax (e.g., $RUNNER_TEMP on Linux/macOS or %RUNNER_TEMP% on Windows) within your steps [4][5]. If you need to use the temporary directory path within an environment variable that is accessible to subsequent steps, you should export it to the GITHUB_ENV file during a step [1][4]. Example of the recommended approach: - name: Set isolated directory run: echo "MY_TEMP_DIR=${RUNNER_TEMP}/my-custom-dir" >> "$GITHUB_ENV" Once added to GITHUB_ENV in this way, the variable will be available as an environment variable in all subsequent steps of that job [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/ios-testflight.yml')
lines = p.read_text().splitlines()
for start,end in [(520,580)]:
    for i in range(start, min(end, len(lines)) + 1):
        print(f"{i:4d}: {lines[i-1]}")
PY

Repository: manaflow-ai/cmux

Length of output: 3095


Move CMUX_TESTFLIGHT_ASSIGN_STATE_OUT_FILE to step scope
runner.temp doesn’t resolve in jobs.assign-external-group.env, so this becomes /ios-testflight-assign-state.txt and the assign step can’t write/read the state file. Put it on the Assign uploaded build to the external beta group step, where runner.temp is available.

🧰 Tools
🪛 actionlint (1.7.12)

[error] 531-531: context "runner" is not allowed here. available contexts are "github", "inputs", "matrix", "needs", "secrets", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)

🤖 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 @.github/workflows/ios-testflight.yml at line 531, Move
CMUX_TESTFLIGHT_ASSIGN_STATE_OUT_FILE out of the jobs.assign-external-group env
block and into the “Assign uploaded build to the external beta group” step scope
so runner.temp resolves correctly. Update the relevant step-level env for the
assign flow, keeping the state file path tied to runner.temp where it is
available, and leave the rest of the workflow env unchanged.

Source: Linters/SAST tools

Comment on lines +458 to +489
try:
submission = _beta_review_submission(token, build_id)
if submission is not None:
review_state = submission["beta_review_state"]
if review_state in ACTIVE_CURRENT_BUILD_BETA_REVIEW_STATES:
print(
"asc_assign_external_testflight_group: build "
f"{build_number} external state is READY_FOR_BETA_SUBMISSION with submission "
f"{submission['id']} (state={review_state or 'unknown'})"
)
return "current_build_review_pending"
raise RuntimeError(
"build "
f"{build_number} external state is READY_FOR_BETA_SUBMISSION with beta app review "
f"submission {submission['id']} in unexpected betaReviewState={review_state or '<empty>'}"
)
sibling_submission = _find_active_review_submission_on_sibling_build(token, build_id)
if sibling_submission is not None:
_report_pending_sibling_review(build_number, sibling_submission)
return "sibling_review_pending"
except RuntimeError as recovery_exc:
last_submit_error = (
f"{str(exc) or 'submit beta app review did not succeed yet'}; "
f"recovery lookup failed: {recovery_exc}"
)
if time.time() >= deadline:
raise RuntimeError(
f"build {build_number} failed to submit beta app review within the timeout window"
)
time.sleep(max(1, poll_seconds))
token = _token()
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

sed -n '400,540p' ios/scripts/asc_assign_external_testflight_group.py

Repository: manaflow-ai/cmux

Length of output: 7005


🏁 Script executed:

rg -n "current_build_review_pending|sibling_review_pending|unexpected betaReviewState|beta_review_state|_find_active_review_submission_on_sibling_build|_report_pending_sibling_review" ios -S

Repository: manaflow-ai/cmux

Length of output: 2533


🏁 Script executed:

sed -n '330,390p' ios/scripts/asc_assign_external_testflight_group.py

Repository: manaflow-ai/cmux

Length of output: 2437


🏁 Script executed:

rg -n "asc_assign_external_testflight_group|beta app review|READY_FOR_BETA_SUBMISSION|REJECTED|unexpected betaReviewState|sibling_review_pending|current_build_review_pending" ios -g '*test*' -S

Repository: manaflow-ai/cmux

Length of output: 4323


Unexpected betaReviewState errors should not be treated as recovery failures. The inner except RuntimeError as recovery_exc also catches the deliberate raise RuntimeError(... unexpected betaReviewState=...), so a terminal review state gets retried until the deadline and then collapses into a generic timeout. Re-raise that branch or narrow the recovery catch to lookup failures only.

🧰 Tools
🪛 Ruff (0.15.20)

[warning] 469-473: Abstract raise to an inner function

(TRY301)


[warning] 469-473: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 484-486: Within an except clause, raise exceptions with raise ... from err or raise ... from None to distinguish them from errors in exception handling

(B904)


[warning] 484-486: Avoid specifying long messages outside the exception class

(TRY003)

🤖 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/asc_assign_external_testflight_group.py` around lines 458 - 489,
The retry loop in asc_assign_external_testflight_group.py is swallowing the
deliberate RuntimeError raised for an unexpected betaReviewState inside the
submission check, causing terminal states to be retried as if they were
transient lookup failures. Update the try/except around _beta_review_submission
and _find_active_review_submission_on_sibling_build so that only recovery/lookup
errors are handled by the inner except RuntimeError as recovery_exc, and let the
unexpected betaReviewState branch propagate immediately. Keep the existing
current_build_review_pending and sibling_review_pending paths unchanged, but
make sure the RuntimeError from the unexpected betaReviewState in this block is
not converted into last_submit_error or retried.

Source: Linters/SAST tools

@azooz2003-bit
azooz2003-bit enabled auto-merge July 6, 2026 22:23
@azooz2003-bit
azooz2003-bit merged commit 7c992ca into main Jul 6, 2026
40 of 42 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — 85a04aae Deployed Jul 6, 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