Skip to content

ci(ios): automate App Store Connect build and upload for Testflight & Production - #2591

Merged
ethicnology merged 1 commit into
developfrom
ci/app-store-connect-upload
Aug 11, 2026
Merged

ci(ios): automate App Store Connect build and upload for Testflight & Production#2591
ethicnology merged 1 commit into
developfrom
ci/app-store-connect-upload

Conversation

@ethicnology

Copy link
Copy Markdown
Member
  • Add a manually dispatched, branch-selectable App Store Connect upload workflow on GitHub's macos-15 arm64 runner with Xcode 26.3 and the iOS 26 SDK.
  • Reproduce the app's pinned Flutter and Rust toolchains, including the separate Rust 1.85.1 toolchain required by bdk_dart and the pinned replacement for Cargokit's floating stable toolchain.
  • Import and validate an App Store distribution certificate and provisioning profile, build a signed IPA, verify its signature and metadata, preserve the IPA and dSYMs, then upload the processed build to App Store Connect.
  • Add make ios-release BUILD_NUMBER=<number> as the canonical local entry point for the signed IPA build.

Security boundary

GitHub only builds, signs, and uploads a build with a dedicated Developer API key. TestFlight group assignment, App Store version selection, review submission, and production release remain manual actions in App Store Connect, so the exact binary tested through TestFlight can later be selected for production without rebuilding it.

All external actions are pinned to immutable commit SHAs. FVM is downloaded at a pinned version and checked against a committed SHA-256. The workflow validates that the provisioning profile belongs to com.bullbitcoin.app and rejects development, ad hoc, and enterprise profiles.

Create a protected GitHub environment named app-store-connect-upload with required reviewers, prevent self-review, and restrict its deployment branches before adding these environment secrets:

  • APP_STORE_CONNECT_API_KEY_ID
  • APP_STORE_CONNECT_ISSUER_ID
  • APP_STORE_CONNECT_API_PRIVATE_KEY
  • IOS_DISTRIBUTION_CERTIFICATE_BASE64
  • IOS_DISTRIBUTION_CERTIFICATE_PASSWORD
  • IOS_APP_STORE_PROVISIONING_PROFILE_BASE64

@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown

Claude finished @ethicnology's task in 5m 55s —— View job


Review: App Store Connect upload workflow

  • Read repo guidelines and gather context
  • Review .github/workflows/upload-app-store-connect.yml
  • Review makefile changes
  • Verify pinned action SHAs and toolchain versions against repo config
  • Post review feedback

Overall this is a carefully hardened workflow: pinned SHAs, checksummed FVM binary instead of the mutable installer script, environment-gated secrets, profile-type validation, post-build signature/metadata verification, and artifact preservation before upload. The Rust pins (1.95.0 / 1.85.1) match Containerfile.tools and the makefile's BDK_RUST_VERSION, and the cache keys mirror .github/actions/flutter-setup. The findings below are ordered by severity.

Note: I don't have an inline-review tool in this environment, so findings are listed here with file/line references instead of as inline comments.


1. Nothing wires the imported certificate/profile into the build — the archive step will likely fail (correctness, high)

ios/Runner.xcodeproj/project.pbxproj uses CODE_SIGN_STYLE = Automatic with no PROVISIONING_PROFILE_SPECIFIER. The workflow imports the distribution cert into a temp keychain and installs the profile (upload-app-store-connect.yml:182-219), but then runs plain fvm flutter build ipa --release (makefile:105) with no --export-options-plist. The IOS_PROVISIONING_PROFILE_PATH env var is written but never consumed except by the cleanup step.

flutter build ipa invokes xcodebuild archive / -exportArchive without -allowProvisioningUpdates, and automatic signing does not use manually installed profiles — it needs an authenticated Apple account/API key in Xcode to resolve one. On a fresh macos-15 runner this typically dies at archive with No profiles for 'com.bullbitcoin.app' were found / No Accounts.

If you have already run this workflow end-to-end successfully, disregard. Otherwise the usual fix is to commit an ios/ExportOptions.plist with method: app-store, signingStyle: manual, signingCertificate: Apple Distribution, teamID: BX99T32YGS, and a provisioningProfiles map for com.bullbitcoin.app, pass it via flutter build ipa --export-options-plist, and switch the Runner Release config to manual signing (or override via xcconfig) so the archive step also uses the installed profile. Fix this →

2. pod install --deployment may run before the Flutter iOS engine artifacts exist (correctness, medium)

upload-app-store-connect.yml:176-180 runs pod install --deployment right after make deps/make build-runner, but the repo's own ios-pod-update target (makefile:90-94) runs fvm flutter precache --ios first — the Flutter podhelper needs the iOS engine artifacts, which neither fvm install nor pub get downloads. On a cache-miss runner this step can fail before flutter build ipa (which precaches implicitly) ever runs. Add fvm flutter precache --ios before the pod install step.

3. Unverifiable action inputs: backend and wait-for-processing (correctness, medium — please verify)

upload-app-store-connect.yml:283-290 passes backend: appstore-api and wait-for-processing: true to apple-actions/upload-testflight-build. GitHub Actions silently ignores unknown inputs (a log warning only), so if these inputs don't exist at the pinned v5.3.0 commit, the run stays green while the "wait for processing" behavior promised in the PR description silently never happens. I couldn't reach the network from this sandbox to check the action.yml at 5e75ff5, nor to confirm any of the pinned SHAs actually match their stated tags (checkout v7.0.1, cache v6.1.0, upload-artifact v7.0.1, import-codesign-certs v7.0.0) or the FVM tarball SHA-256 — worth one manual confirmation of each since the whole security model rests on those pins.

4. Profile app-identifier check is a loose suffix match (security hardening, low)

upload-app-store-connect.yml:202 uses [[ "$app_identifier" != *.com.bullbitcoin.app ]], which also accepts e.g. TEAMID.evil.com.bullbitcoin.app (bundle IDs are attacker-choosable in another team's portal). The later CFBundleIdentifier check on the built app (line 247) limits real impact, but since this check exists as defense-in-depth, anchor it fully — ideally including the team ID already committed in the pbxproj:

if ! [[ "$app_identifier" =~ ^BX99T32YGS\.com\.bullbitcoin\.app$ ]]; then

5. Rustup wrapper hardcodes 1.95.0 and rewrites any bare stable argument (maintainability, low)

The quoted heredoc at upload-app-store-connect.yml:154-162 embeds 1.95.0 instead of $RUST_VERSION, so bumping the env var alone leaves the wrapper mapping stable to the old version. The sanity check at line 165 will catch the drift, but as a confusing failure; interpolating the version into the wrapper (e.g. [ "$arg" = "stable" ] && arg="__RUST_VERSION__" + sed, or an unquoted heredoc with escaped $@/$0) keeps one source of truth. Also note the wrapper substitutes every argv equal to stable, not just the toolchain position — harmless today, but worth a comment.

Nits

  • upload-app-store-connect.yml:174: git diff --exit-code doesn't catch newly created untracked files from codegen; test -z "$(git status --porcelain)" is the stricter purity gate.
  • makefile:103: ios-release only checks BUILD_NUMBER is non-empty; the workflow validates it's a positive integer, but the "canonical local entry point" would happily pass garbage into CFBundleVersion.
  • Default build number base_build + GITHUB_RUN_NUMBER (upload-app-store-connect.yml:83): run_number resets if the workflow file is ever renamed, which can reproduce already-used numbers. App Store Connect rejects duplicates and the manual build_number input recovers it, so this is just something to know.

Summary: solid security posture (pinning, environment gating, profile validation, artifact hashing), but I'd hold merge until #1 is either demonstrated working by a real green run of this workflow or fixed with an explicit manual-signing export path, and until #2/#3 are confirmed. #4/#5 are cheap tightenings.

@ethicnology ethicnology self-assigned this Aug 11, 2026
@ethicnology
ethicnology force-pushed the ci/app-store-connect-upload branch from c9a95e6 to 8f0d9d7 Compare August 11, 2026 22:27
@ethicnology
ethicnology merged commit 2a237d1 into develop Aug 11, 2026
2 checks passed
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