Repository navigation
iOS changelog + auto TestFlight "What to Test" notes - #6154
Conversation
Testers got an opaque MARKETING_VERSION (timestamp) build with no notes on every install and auto-update. Add a per-build changelog and wire it into the upload so each TestFlight build shows what changed. - ios/CHANGELOG.md: single source of truth, newest version on top. Each entry has an Internal block (terse, dev-facing, per-build) and an External block (curated, user-facing). Seeded with the real 1.0.2 and 1.0.3 shipped history. - ios/scripts/asc_set_testflight_notes.py: dependency-free ASC API helper (ES256 JWT via openssl, mirrors asc_max_build.py). Resolves the app, finds the build by CFBundleVersion (polls while it processes), creates/PATCHes the en-US betaBuildLocalizations.whatsNew. - ios/scripts/set-testflight-notes.sh: extracts the top entry's Internal/External block and calls the helper. --validate-only checks local preconditions with no network; --expect-marketing-version refuses to attach notes for the wrong version. - upload-testflight.sh: preflights the notes (changelog present, version match) BEFORE the archive (fatal, deterministic), then sets the notes after a successful upload (non-fatal; the binary is already up, notes can be re-applied). Internal lane -> Internal block, --external -> External block. --skip-notes opts out. - cloud-testflight.sh: passes --external / --skip-notes through. - Shared.xcconfig: bump MARKETING_VERSION to 1.0.3 so the default lane matches the changelog top entry (the upload guard requires they agree). - docs/ios-release-notes.md: presentation design (version scheme, internal vs external) and the phase-2 in-app What's New sheet spec (deferred). Co-Authored-By: Claude Opus 4.8 (1M context) <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 (7)
📝 WalkthroughWalkthroughAdds end-to-end automation for iOS TestFlight "What to Test" notes. A new ChangesiOS TestFlight Release Notes Automation
Sequence DiagramsequenceDiagram
actor Developer
participant upload_sh as upload-testflight.sh
participant set_notes_sh as set-testflight-notes.sh
participant asc_py as asc_set_testflight_notes.py
participant ASC as App Store Connect API
Developer->>upload_sh: run (--external / --skip-notes)
upload_sh->>set_notes_sh: --validate-only --expect-marketing-version (preflight)
set_notes_sh-->>upload_sh: notes preview or version mismatch error
upload_sh->>upload_sh: xcodebuild archive + export + altool upload
upload_sh->>set_notes_sh: --audience internal|external --build-number
set_notes_sh->>asc_py: python3 asc_set_testflight_notes.py --bundle-id --build-number --notes-file
asc_py->>ASC: GET /apps, GET /builds (poll)
asc_py->>ASC: PATCH or POST betaBuildLocalizations.whatsNew
ASC-->>asc_py: 200/201
asc_py-->>set_notes_sh: exit 0
set_notes_sh-->>upload_sh: notes set (or warning on failure)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (18 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit a057592. Configure here.
| --build-number "$SHIPPED_BUILD_NUMBER" \ | ||
| --audience "$NOTES_AUDIENCE" \ | ||
| --bundle-id "$PRODUCT_BUNDLE_IDENTIFIER" \ | ||
| "${NOTES_VERSION_ARGS[@]}"; then |
There was a problem hiding this comment.
Empty array aborts notes step
Medium Severity
After upload, the script passes optional "${NOTES_VERSION_ARGS[@]}" into set-testflight-notes.sh. When the archived marketing version does not match the semver regex, NOTES_VERSION_ARGS stays empty. With set -u, expanding an empty array that way triggers an unbound-variable error on macOS Bash 3.2, so the post-upload notes step can fail even though TestFlight upload already succeeded.
Reviewed by Cursor Bugbot for commit a057592. Configure here.
| if [[ ! -s "$NOTES_FILE" ]]; then | ||
| TOP_VER="$(grep -m1 '^## \[' "$CHANGELOG" || true)" | ||
| echo "error: no '### ${AUDIENCE^}' bullet items found in the top entry ($TOP_VER) of $CHANGELOG" >&2 | ||
| exit 1 |
There was a problem hiding this comment.
Bash 3.2 breaks audience error
Low Severity
When changelog extraction yields no bullets, the error path uses ${AUDIENCE^} to capitalize the audience name. That parameter expansion requires Bash 4+, but macOS still ships Bash 3.2 for #!/usr/bin/env bash, so validation can fail with a bad substitution instead of the intended message about missing ### Internal or ### External items.
Reviewed by Cursor Bugbot for commit a057592. Configure here.
Greptile SummaryThis PR adds a per-build iOS changelog (
Confidence Score: 4/5Safe to merge after fixing the bash 3.2 incompatibility; all other script logic and guards are sound. The shell scripts and Python helper are well-structured with solid pre/post-upload guards and a non-fatal design for the ASC mutation. The one concrete defect is in the error path of set-testflight-notes.sh: ${AUDIENCE^} is a bash 4+ expansion that dies with 'bad substitution' on macOS's system bash 3.2, turning a clear changelog-validation failure into a confusing one. The upload pipeline's validate-only calls pass through this code path, so it can surface during a normal release cut on a stock macOS or CI machine that relies on the system bash. ios/scripts/set-testflight-notes.sh — line 144 uses ${AUDIENCE^} which requires bash 4+. Important Files Changed
Sequence DiagramsequenceDiagram
participant Dev as Developer
participant upload as upload-testflight.sh
participant setnotes as set-testflight-notes.sh
participant py as asc_set_testflight_notes.py
participant ASC as App Store Connect API
Dev->>upload: ship ios [--external] [--skip-notes]
upload->>setnotes: --validate-only --audience a
setnotes-->>upload: OK / error (fast fail, no network)
upload->>upload: xcodebuild archive
upload->>setnotes: --validate-only --audience a --expect-marketing-version X.Y.Z
setnotes-->>upload: OK / error (version-match guard)
upload->>ASC: altool upload IPA
ASC-->>upload: upload accepted
alt "SKIP_NOTES=0 and ASC API key present"
upload->>setnotes: --build-number N --audience a --expect-marketing-version X.Y.Z
setnotes->>py: --bundle-id ... --build-number N --notes-file /tmp/notes.txt
loop Poll until build visible up to 900s / 20s interval
py->>ASC: "GET /v1/builds?filter[version]=N"
ASC-->>py: build id or empty (still processing)
end
py->>ASC: "GET /v1/builds/{id}/betaBuildLocalizations"
ASC-->>py: existing en-US localization id or none
py->>ASC: PATCH or POST betaBuildLocalizations whatsNew
ASC-->>py: 200/201
py-->>setnotes: exit 0
setnotes-->>upload: notes set
else "SKIP_NOTES=1 or no ASC key"
upload->>upload: warn and skip (non-fatal)
end
Reviews (1): Last reviewed commit: "iOS changelog + auto TestFlight "What to..." | Re-trigger Greptile |
|
|
||
| if [[ ! -s "$NOTES_FILE" ]]; then | ||
| TOP_VER="$(grep -m1 '^## \[' "$CHANGELOG" || true)" | ||
| echo "error: no '### ${AUDIENCE^}' bullet items found in the top entry ($TOP_VER) of $CHANGELOG" >&2 |
There was a problem hiding this comment.
${AUDIENCE^} is a bash 4.0+ case-modification expansion. macOS ships bash 3.2.x as the system default (Apache license reasons), so on any stock macOS machine or macOS CI runner where Homebrew's bash is not at the front of PATH, this error path will die with bad substitution instead of emitting the intended helpful message. The preflight validate-only call made from upload-testflight.sh also triggers this code path on a malformed changelog, turning a clear failure into a confusing one.
| echo "error: no '### ${AUDIENCE^}' bullet items found in the top entry ($TOP_VER) of $CHANGELOG" >&2 | |
| AUDIENCE_CAP="$(printf '%s' "${AUDIENCE:0:1}" | tr '[:lower:]' '[:upper:]')${AUDIENCE:1}" | |
| echo "error: no '### ${AUDIENCE_CAP}' bullet items found in the top entry ($TOP_VER) of $CHANGELOG" >&2 |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a057592466
ℹ️ 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".
| data = body.get("data", []) | ||
| if not data: | ||
| return None | ||
| return data[0]["id"] |
There was a problem hiding this comment.
Wait for processing before returning a build
For a just-uploaded build that App Store Connect has listed but whose processingState is still PROCESSING, this returns the id immediately and the caller tries to create/patch betaBuildLocalizations right away. Those TestFlight metadata updates can still be rejected until processing finishes; because upload-testflight.sh treats any notes failure as non-fatal, the build can ship without the intended What to Test text even though it would have become writable within the timeout. Use the processingState value already requested here, or retry the localization mutation, before considering the build ready.
Useful? React with 👍 / 👎.


Why
iOS TestFlight testers get an opaque
MARKETING_VERSION (timestamp)build with no notes on every install and auto-update, so there's no way to tell what changed. This adds a per-build iOS changelog and wires it into the upload so every TestFlight build shows its "What to Test" notes.The changelog system
ios/CHANGELOG.mdis the single source of truth, newest version on top (mirrors the macOSCHANGELOG.md## [X.Y.Z] - dateshape so it could feed a docs page later). Each version entry has two blocks:ship ios, thecmux betagroup): terse, dev-facing, per-build, includes a dogfood-focus line.ship ios founders): a curated, user-facing summary, no jargon or PR numbers.Seeded with the real shipped 1.0.2 and 1.0.3 history (composer, View as Text, minimal QR, presence-gated + deep-linking notifications with a synced unread badge, offline sign-out, workspace-list polish, the render-grid watchdog fix), attributed from the merged PR list.
The top entry's version must equal the checked-in
MARKETING_VERSIONinios/Config/Shared.xcconfig; this PR bumps it to1.0.3so the two agree.How "What to Test" gets set (the ASC API call + when)
After a successful
altoolupload,upload-testflight.shrunsset-testflight-notes.sh, which awk-extracts the top entry's chosen block and callsasc_set_testflight_notes.py. That helper is dependency-free (ES256 JWT viaopenssl, same pattern asasc_max_build.py): it mints the JWT from the ASC API key (ios/Config/AppStoreConnect.local.plist, key id4WN4S8ANN4), resolves the app by bundle id, finds the build byCFBundleVersion, thenPOST/PATCHes the en-USbetaBuildLocalizations.whatsNew. A just-uploaded build isn't addressable until App Store Connect ingests it, so the build lookup polls (default 900s / 20s).Audience is driven by the lane: internal cut -> Internal block;
--external-> External block.--skip-notes(orCMUX_TESTFLIGHT_SKIP_NOTES=1) opts out;cloud-testflight.shpasses--external/--skip-notesthrough.Fatal vs non-fatal split (from review): the deterministic local preconditions (changelog present, audience block non-empty, top version == the archived
CFBundleShortVersionString) are checked before the archive and again right after it, and fail the upload. Only the App Store Connect mutation itself is non-fatal after upload (the binary is already on TestFlight; transient processing/API errors just warn, and notes can be re-applied withset-testflight-notes.sh --build-number <n> --audience <a>).Internal vs external presentation design
Documented in
docs/ios-release-notes.md: the version scheme testers see (MARKETING_VERSION (CURRENT_PROJECT_VERSION), the parens being the UTC build timestamp), and why there are two notes blocks per entry.Phase 2 (spec only, deferred)
docs/ios-release-notes.mdspecs an in-app "What's New" sheet on first launch after an update for external/founders builds, sourced from the External block: aUserDefaultslast-seen-version gate, the changelog parsed into a bundled resource at build time, a localized SwiftUI sheet, and a Settings re-open entry. That is iOS app runtime code (needs a tagged dogfood build + approval), so it's intentionally out of this PR to keep it scripts + docs only.How to cut a release with notes going forward
ios/CHANGELOG.md: add the new version as the top entry (Internal + External blocks) and bumpios/scripts/bump-ios-version.sh X.Y.Zto match. Never cut a beta without a changelog entry.ship ios(internal) orship ios founders(external). The upload sets the matching "What to Test" block automatically.ios/scripts/set-testflight-notes.sh --build-number <shipped> --audience internal|external.The hq
cmux-ios-releaseskill is updated with this step (not in this repo).Verification
shellcheckclean on all three bash scripts;py_compileclean;bash -nclean.dev.cmux.app.beta), build lookup by version, and existing en-US localization lookup all returned real IDs without mutating any build.--validate-onlyverified: passes valid input (with/without version, no creds/build-number), fails on version mismatch and missing changelog.autoreviewclean (Codex), cmux-policy clean.This is workflow/docs/script-only (no app/runtime behavior change), so it can merge after CI green with no dogfood per repo policy.
🤖 Generated with Claude Code
Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Low Risk
Scripts and docs only; no iOS app runtime changes. Release workflow now depends on changelog/version alignment and ASC API for notes (upload still succeeds if notes fail).
Overview
Adds
ios/CHANGELOG.mdas the per-build source of truth (Internal vs External blocks per audience) and bumpsMARKETING_VERSIONto 1.0.3 so the top entry matchesShared.xcconfig.After a successful TestFlight upload,
upload-testflight.shnow pushes the top changelog block to App Store Connect What to Test via newset-testflight-notes.sh(awk extract) andasc_set_testflight_notes.py(dependency-free ASC JWT +betaBuildLocalizations.whatsNew, with build-ingest polling).--skip-notes/CMUX_TESTFLIGHT_SKIP_NOTESopt out;cloud-testflight.shforwards--skip-notes. Internal cuts use the Internal block;--externaluses External.Fatal pre-upload: changelog present, non-empty audience block, and top version equals the archive marketing version (
--validate-onlybefore archive and again after). Non-fatal post-upload: ASC mutation failures warn only; notes can be re-applied withset-testflight-notes.sh.docs/ios-release-notes.mddocuments the workflow and defers in-app What's New (phase 2) to a follow-up.Reviewed by Cursor Bugbot for commit a057592. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds a per-build iOS changelog and auto-publishes it to TestFlight “What to Test” so testers see what changed on install and auto-update. No runtime app changes.
ios/CHANGELOG.mdis the single source of truth; each top entry has “Internal” and “External” blocks.asc_set_testflight_notes.py), driven by lane:ship ios→ Internal,--external→ External;--skip-notesto opt out.upload-testflight.shfails fast on missing/empty notes or version mismatch; post-upload notes are non-fatal and can be re-applied withios/scripts/set-testflight-notes.sh.cloud-testflight.shpasses--external/--skip-notesthrough;MARKETING_VERSIONbumped to1.0.3; addeddocs/ios-release-notes.md.Written for commit a057592. Summary will update on new commits.
Summary by CodeRabbit
Documentation
Chores