Build and upload the phone app to TestFlight - #195
Merged
Conversation
Adds a manual workflow that builds the companion app, signs it for the App Store, checks it will actually work, and uploads it. Manual on purpose. Every green build of the Mac app ships itself, but a TestFlight upload burns a build number that can never be used again and lands on real testers' phones, so it should be someone deciding rather than a side effect of merging. The check before uploading matters more than it sounds. The first build was signed with a profile that had no push permission at all, which still installs, still runs, and silently never receives anything. That looks like the app being broken rather than the build being wrong, so the script now refuses to upload unless push, iCloud and the build type all read correctly on the signed app.
App Store validation rejects the app outright because it says it works on iPad but never says which orientations it supports. iPad needs all four for split screen; the phone gets everything except upside down, which is what Apple's own apps do. This one is easy to miss. Building locally only prints a warning, so it looks fine right up until the upload is rejected. It cost two attempts, one of which had already registered the app, so the CI script now checks for it before starting a build rather than after burning an upload.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Adds a manual workflow that builds the phone companion app, signs it for the App Store, checks it will actually work, and uploads it to TestFlight. Also fixes the one thing that stopped the first manual upload.
Manual on purpose. Every green build of the Mac app ships itself, but a TestFlight upload burns a build number that can never be reused and lands on real testers' phones. That should be someone deciding, not a side effect of merging.
The checks before uploading are the point
Getting build 1 up by hand hit two failures, and both are the same shape: a problem that is a warning locally and a hard failure at upload, after the slow part is already done.
1. No declared orientations. The app claims iPad support (
TARGETED_DEVICE_FAMILY "1,2") but declared noUISupportedInterfaceOrientations. Archiving prints only "All interface orientations must be supported unless the app requires full screen"; App Store validation rejects the bundle outright with "Invalid bundle. No orientations were specified". Fixed inproject.yml— all four on iPad because multitasking requires it, iPhone without upside-down as Apple's own apps do.2. No production push entitlement. The App Store profile had no
aps-environmentat all — it predated Push Notifications being enabled on the App ID. That build installs, runs, and silently receives nothing. For an app whose entire purpose is push, it reads as "the companion is broken" rather than "the profile is stale".So the script gates three times:
aps-environment: production. Both fail in seconds instead of after a ~10 minute archive.aps-environment: production,get-task-allow: false, and iCloud container environmentProduction. The profile is what was requested; the signature is what the device enforces, and they can differ.Same fail-closed shape as
verify-provision-profile.shon the macOS side.Summary
scripts/build-ios-testflight.sh— keychain setup, profile install, pre-flight checks, xcodegen, archive, export, verify, upload. Logic lives here rather than in YAML, matchingsign-release-app.sh..github/workflows/ios-testflight.yml—workflow_dispatchwith anuploadboolean so it can run as build-and-verify only.ios/ProgramaSpike/project.yml— declares the orientations.-allowProvisioningUpdatescannot create or refresh anything; the cert and both profiles arrive as secrets.${GITHUB_RUN_ID}${GITHUB_RUN_ATTEMPT}, matching the macOS lane. App Store Connect rejects a reusedCFBundleVersion; the value inproject.ymlis only a local-dev default.always()step.vendor/CmuxIrohTransport(in-tree) and iroh-ffi (SwiftPM), not ghostty.Known and deliberately not fixed
Iroh.frameworkships no dSYM, so symbol upload warns and crash reports from inside that dependency will not symbolize. It is a prebuilt binary fromiroh-ffi; fixing it means getting debug symbols upstream. Non-blocking — the upload proceeds.Secrets required
None of these exist yet. The distribution cert and both profiles were created locally by Xcode during build 1's export, so they can be exported from Keychain Access and
~/Library/Developer/Xcode/UserData/Provisioning Profilesrather than made from scratch.APPLE_IOS_DIST_CERT_BASE64.p12APPLE_IOS_DIST_CERT_PASSWORDAPPLE_IOS_APP_PROFILE_BASE64com.darkroom.programa.spikeAPPLE_IOS_WIDGET_PROFILE_BASE64…spike.widgetsAPPSTORE_CONNECT_KEY_ID/_ISSUER_ID/_KEY_P8_BASE64Test plan
bash -n scripts/build-ios-testflight.shpassesTARGETED_DEVICE_FAMILYparses as1,2generic/platform=iOS, Release)UIBackgroundModes,NSSupportsLiveActivities,ITSAppUsesNonExemptEncryptionaps-environment: production,beta-reports-active: true,get-task-allow: falseworkflow-guard-testspassesworkflow_dispatchrun withupload: falseonce the signing secrets exist