Skip to content

Fix iOS framework MinimumOSVersion packaging - #8131

Closed
azooz2003-bit wants to merge 2 commits into
mainfrom
task-ios-iroh-minos
Closed

azooz2003-bit wants to merge 2 commits into
mainfrom
task-ios-iroh-minos

Conversation

@azooz2003-bit

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

Copy link
Copy Markdown
Collaborator

Summary

  • stamp missing MinimumOSVersion on embedded iOS frameworks during the manual TestFlight/App Store re-sign path
  • re-sign nested frameworks before the top-level app bundle after metadata repair
  • add a post-package gate so missing framework MinimumOSVersion fails before App Store Connect
  • add regression coverage with a fake Iroh.framework embedded in the IPA

Failure Evidence

Testing

  • bash -n ios/scripts/upload-testflight.sh
  • git diff --check origin/main...HEAD
  • python3 tests/test_ios_appstore_lane_identity.py

No runtime app reload: this only changes CI/CD packaging scripts and tests.


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


Summary by cubic

Fixes App Store/TestFlight rejection by stamping MinimumOSVersion on embedded iOS frameworks and re-signing nested frameworks before the app in the manual signing path. Adds a pre-upload check to fail CI early; packaging scripts and tests only, no runtime impact.

  • Bug Fixes
    • Stamp MinimumOSVersion on embedded frameworks (from app Info.plist or IPHONEOS_DEPLOYMENT_TARGET in Shared.xcconfig).
    • Re-sign nested frameworks first, then the top-level app, so repaired metadata is enclosed by the final signature.
    • Add IPA verification gate that fails if any embedded framework lacks MinimumOSVersion; add regression test with a fake Iroh.framework to assert the stamped value.

Written for commit a7e8212. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes

    • Improved iOS release uploads by ensuring embedded frameworks include the required minimum OS version metadata.
    • Added validation to prevent uploads when framework metadata is missing or invalid.
    • Updated manually signed builds to apply and preserve the required framework metadata.
  • Tests

    • Expanded release validation to confirm embedded frameworks contain the expected minimum OS version.

@vercel

vercel Bot commented Jul 15, 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 15, 2026 11:23am
cmux-staging Building Building Preview, Comment Jul 15, 2026 11:23am

@coderabbitai

coderabbitai Bot commented Jul 15, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The iOS TestFlight upload script now resolves, stamps, signs, and validates embedded frameworks’ MinimumOSVersion metadata. The App Store lane identity tests create an embedded Iroh.framework and verify its stamped value against Shared.xcconfig.

Changes

iOS framework metadata handling

Layer / File(s) Summary
Framework metadata repair and test contract
ios/scripts/upload-testflight.sh, tests/test_ios_appstore_lane_identity.py
The script resolves the deployment target, stamps missing framework metadata, and the lane identity test verifies the resulting Iroh.framework value.
Manual re-signing and upload validation
ios/scripts/upload-testflight.sh
Manual signing stamps and re-signs nested frameworks, and both signing paths block upload when embedded framework metadata is incomplete.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: lawrencecchen

Sequence Diagram(s)

sequenceDiagram
  participant ExportedApp
  participant UploadScript
  participant NestedFramework
  participant Codesign
  participant AppStoreConnect
  UploadScript->>ExportedApp: resolve MinimumOSVersion
  UploadScript->>NestedFramework: stamp missing MinimumOSVersion
  UploadScript->>Codesign: re-sign nested framework
  UploadScript->>UploadScript: verify IPA framework metadata
  UploadScript->>AppStoreConnect: upload validated IPA
Loading

Important

Pre-merge checks failed

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

❌ Failed checks (1 error)

Check name Status Explanation Resolution
Cmux User-Facing Error Privacy ❌ Error New upload-testflight.sh output exposes App Store Connect and env/config names (IPHONEOS_DEPLOYMENT_TARGET, SHARED_XCCONFIG) in errors. Rewrite user-visible messages to say only that framework metadata is incomplete and failed validation; keep vendor names and config keys out of stdout/stderr.
✅ Passed checks (24 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing iOS framework MinimumOSVersion packaging.
Description check ✅ Passed The description covers the required summary and testing sections and provides sufficient implementation and validation detail.
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 PR only changes a shell script and a Python test; no production Swift files were modified, so no new actor-isolation issues were introduced.
Cmux Swift Blocking Runtime ✅ Passed Diff touches only a shell script and Python tests; no Swift production files or new blocking/timing primitives were introduced.
Cmux Browser Automation Off-Main ✅ Passed PR only changes iOS upload/test scripts; it doesn't touch browser automation routing, WebKit/AppKit command paths, or policy tests.
Cmux Expensive Synchronous Load ✅ Passed No production Swift files changed; the diff only updates a shell upload script and a Python test, so the expensive-sync-load rule is not implicated.
Cmux Cache Substitution Correctness ✅ Passed PR only changes a Bash packaging script and a Python test; the new logic reads MinimumOSVersion fresh from Info.plist/xcconfig and has no cache/history/snapshot substitution.
Cmux No Hacky Sleeps ✅ Passed The PR only adds metadata stamping/verification and regression coverage; no sleeps, timers, polling loops, or fixed waits appear in the changed files.
Cmux Algorithmic Complexity ✅ Passed New scans are one-pass over the small, fixed IPA/framework set in a packaging script and test harness, not a scalable user-data path.
Cmux Swift Concurrency ✅ Passed Diff only changes a shell script and a Python test; no Swift files or concurrency patterns are introduced, so the check is not applicable.
Cmux Swift @Concurrent ✅ Passed PR changes only shell and Python test files; no Swift code or async annotations were touched, so the Swift @concurrent rule doesn't apply.
Cmux Swift Package Boundaries ✅ Passed No production Swift files changed; the PR only updates packaging script and a Python test, so the package-boundary rule is not violated.
Cmux Swiftpm Lockfiles ✅ Passed Only ios script and a test changed; no Package.swift, Xcode project, .gitignore, workflow, or dependency-resolution files were touched.
Cmux Swift Logging ✅ Passed The diff touches only a shell script and a Python test; it adds no production Swift logging or other disallowed logging changes.
Cmux Full Internationalization ✅ Passed PR only changes an iOS packaging shell script and a test fixture; no Swift/UI/web/message-catalog or locale files were added or edited.
Cmux Swiftui State Layout ✅ Passed No SwiftUI files changed; the PR only touches a shell script and Python tests, so the SwiftUI state-layout rule is not applicable.
Cmux Architecture Rethink ✅ Passed No Swift/UI architecture changes are present; only packaging script and test updates, which are outside the rule’s scope.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed No Swift/UI window files changed; the PR only touches a shell script and a Python test, so the auxiliary-window-close-shortcuts rule is not implicated.
Cmux Source Artifacts ✅ Passed Only source script/test files changed; no logs, caches, build outputs, DerivedData, or other source-control artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed The diff only touches a shell script and a test file; no production Swift files under Sources/ were changed, so the seam rule isn’t implicated.
Cmux No Ambient Global State ✅ Passed Diff only changes a shell script and a test; no production Swift code or new ambient global state was introduced.
✨ 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 task-ios-iroh-minos

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 15, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes an Apple rejection caused by embedded iOS frameworks (Iroh.framework) missing MinimumOSVersion in their Info.plist during the manual re-sign path. It adds pre-upload repair and a post-sign gate to prevent the class of regressions from reaching App Store Connect.

  • stamp_ios_framework_minimum_os_versions resolves the version from the app's own Info.plist or IPHONEOS_DEPLOYMENT_TARGET in Shared.xcconfig, then patches any framework plist that lacks it — invoked on the manual signing path before nested frameworks and the top-level app are re-signed, preserving the correct code-signing order.
  • verify_ipa_framework_minimum_os_versions runs after both signing paths and fails the upload if any *.framework/Info.plist still lacks a non-empty MinimumOSVersion, acting as a safety net on the automatic path where in-place stamping isn't possible.
  • The regression test extends the existing beta-lane fixture with a fake Iroh.framework (plist intentionally missing the key) and asserts the final resigned IPA carries the correct stamped value.

Confidence Score: 4/5

Safe to merge; the stamp-then-resign ordering is correct, the post-zip gate runs on both signing paths, and the test exercises the full manual-resign flow end-to-end.

The logic for resolving, stamping, re-signing, and verifying MinimumOSVersion is sound and the function ordering (stamp → sign nested → sign app → verify → re-zip → gate) is correct. The only gap is in verify_ipa_framework_minimum_os_versions: with set -euo pipefail active, a non-zero exit from the inline python3 call terminates the function before the rm -rf cleanup, leaving extracted IPA contents on disk. Every other verification function in the file guards cleanup explicitly. This is not a correctness issue for the business logic but is an inconsistency worth fixing.

The verify_ipa_framework_minimum_os_versions function in ios/scripts/upload-testflight.sh warrants a close look at its cleanup path when python3 exits non-zero.

Important Files Changed

Filename Overview
ios/scripts/upload-testflight.sh Adds three new functions (resolve, stamp, verify) plus nested-framework re-signing before the top-level app sign; minor temp dir cleanup gap in verify_ipa_framework_minimum_os_versions when Python exits non-zero
tests/test_ios_appstore_lane_identity.py Adds a fake Iroh.framework (no MinimumOSVersion) to the export fixture and asserts the beta-lane manual-resign path stamps the correct value into the final IPA

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Export IPA via xcodebuild -exportArchive] --> B{SIGNING mode?}
    B -->|manual| C[Extract IPA to resign dir]
    C --> D[resolve_ios_minimum_os_version\nfrom app Info.plist or Shared.xcconfig]
    D --> E[stamp_ios_framework_minimum_os_versions\npatch missing MinimumOSVersion in each *.framework/Info.plist]
    E --> F[Merge entitlements\nbaseline + profile + Release]
    F --> G[Re-sign nested frameworks\ncodesign --force --sign RESIGN_IDENTITY]
    G --> H[Re-sign top-level .app\ncodesign --force --sign + entitlements]
    H --> I[verify codesign --verify --strict]
    I --> J[Re-zip into resigned IPA]
    J --> K
    B -->|automatic| K[verify_ipa_framework_minimum_os_versions\nunzip IPA, check all *.framework/Info.plist for MinimumOSVersion]
    K -->|missing key| L[exit 1 — refuse upload]
    K -->|all present| M[verify_ipa_bundle_identity\nupload to App Store Connect]
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"}}}%%
flowchart TD
    A[Export IPA via xcodebuild -exportArchive] --> B{SIGNING mode?}
    B -->|manual| C[Extract IPA to resign dir]
    C --> D[resolve_ios_minimum_os_version\nfrom app Info.plist or Shared.xcconfig]
    D --> E[stamp_ios_framework_minimum_os_versions\npatch missing MinimumOSVersion in each *.framework/Info.plist]
    E --> F[Merge entitlements\nbaseline + profile + Release]
    F --> G[Re-sign nested frameworks\ncodesign --force --sign RESIGN_IDENTITY]
    G --> H[Re-sign top-level .app\ncodesign --force --sign + entitlements]
    H --> I[verify codesign --verify --strict]
    I --> J[Re-zip into resigned IPA]
    J --> K
    B -->|automatic| K[verify_ipa_framework_minimum_os_versions\nunzip IPA, check all *.framework/Info.plist for MinimumOSVersion]
    K -->|missing key| L[exit 1 — refuse upload]
    K -->|all present| M[verify_ipa_bundle_identity\nupload to App Store Connect]
Loading

Reviews (1): Last reviewed commit: "Fix iOS framework minimum OS packaging" | Re-trigger Greptile

Comment on lines +201 to +232
python3 - "$app" <<'PY'
import plistlib
import sys
from pathlib import Path

app = Path(sys.argv[1])
frameworks = app / "Frameworks"
bad = []

if frameworks.is_dir():
for plist_path in sorted(frameworks.glob("*.framework/Info.plist")):
try:
info = plistlib.loads(plist_path.read_bytes())
except Exception as exc:
bad.append(f"{plist_path.relative_to(app)}: unreadable Info.plist ({exc})")
continue
value = info.get("MinimumOSVersion")
if not isinstance(value, str) or not value.strip():
bad.append(f"{plist_path.relative_to(app)}: MinimumOSVersion is missing or empty")

if bad:
print(
"error: iOS framework metadata is incomplete; App Store Connect rejects embedded frameworks without MinimumOSVersion",
file=sys.stderr,
)
for item in bad:
print(f"error: {item}", file=sys.stderr)
raise SystemExit(1)
PY
status=$?
rm -rf "$workdir"
return "$status"

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 Temp dir leaked when Python MinimumOSVersion check fails

With set -euo pipefail active, if python3 exits 1 (missing MinimumOSVersion detected), Bash's set -e terminates the function immediately. The status=$? and rm -rf "$workdir" lines are never reached, leaving the extracted IPA contents in a temp dir. Every other verification function in this file (e.g. verify_ipa_aps_environment_production, verify_ipa_bundle_identity) explicitly guards cleanup in every error branch rather than relying on the status=$? pattern after a bare command that can fail. When the gate fires on a bad IPA, the leaked dir can hold tens of MB of decompressed app payload on the runner's disk.

@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 `@ios/scripts/upload-testflight.sh`:
- Line 179: Update the parameter expansion in the nested framework logging
statement to quote the app-path variable used for prefix removal, ensuring
pattern characters in its value are treated literally while preserving the
existing displayed relative path.
🪄 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: fd483dbe-4327-45a4-b64b-c7a753631124

📥 Commits

Reviewing files that changed from the base of the PR and between 1ff14b8 and a7e8212.

📒 Files selected for processing (2)
  • ios/scripts/upload-testflight.sh
  • tests/test_ios_appstore_lane_identity.py

PY
)"
if [[ "$changed" == "1" ]]; then
echo "stamped MinimumOSVersion=$minimum_os on nested framework: ${framework_info#$app/}"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Quote the variable inside the parameter expansion.

Expansions inside ${...} should be quoted so they are treated as literal strings rather than glob patterns. If the $app path happens to contain pattern characters like * or ?, leaving it unquoted can result in incorrect string truncation.

💻 Proposed fix
-      echo "stamped MinimumOSVersion=$minimum_os on nested framework: ${framework_info#$app/}"
+      echo "stamped MinimumOSVersion=$minimum_os on nested framework: ${framework_info#"$app/"}"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
echo "stamped MinimumOSVersion=$minimum_os on nested framework: ${framework_info#$app/}"
echo "stamped MinimumOSVersion=$minimum_os on nested framework: ${framework_info#"$app/"}"
🧰 Tools
🪛 Shellcheck (0.11.0)

[info] 179-179: Expansions inside ${..} need to be quoted separately, otherwise they match as patterns.

(SC2295)

🤖 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` at line 179, Update the parameter expansion
in the nested framework logging statement to quote the app-path variable used
for prefix removal, ensuring pattern characters in its value are treated
literally while preserving the existing displayed relative path.

Source: Linters/SAST tools

@azooz2003-bit

Copy link
Copy Markdown
Collaborator Author

Closing because the implementation is architecturally superseded by current main at 22f9e5e; merging this old head would restore obsolete transport code, and no unique required capability remains.

This branch was successfully deployed

1 active deployment
Preview – cmux — a7e82129 Deployed Jul 15, 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