Skip to content

Build and upload the phone app to TestFlight - #195

Merged
arzafran merged 2 commits into
mainfrom
ci/ios-testflight
Jul 29, 2026
Merged

Build and upload the phone app to TestFlight#195
arzafran merged 2 commits into
mainfrom
ci/ios-testflight

Conversation

@arzafran

@arzafran arzafran commented Jul 29, 2026

Copy link
Copy Markdown
Member

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 no UISupportedInterfaceOrientations. 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 in project.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-environment at 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:

  • Before archiving: orientations must be declared if the bundle is iPad-capable, and the app profile must declare aps-environment: production. Both fail in seconds instead of after a ~10 minute archive.
  • After export: the signed entitlements must show aps-environment: production, get-task-allow: false, and iCloud container environment Production. 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.sh on 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, matching sign-release-app.sh.
  • .github/workflows/ios-testflight.ymlworkflow_dispatch with an upload boolean so it can run as build-and-verify only.
  • ios/ProgramaSpike/project.yml — declares the orientations.
  • Manual signing. CI has no authenticated Xcode account, so -allowProvisioningUpdates cannot create or refresh anything; the cert and both profiles arrive as secrets.
  • Profile names are read from the imported profiles at runtime, not hardcoded. Regenerating a profile in the portal changes its name, and a stale hardcoded name fails export cryptically.
  • Build number is ${GITHUB_RUN_ID}${GITHUB_RUN_ATTEMPT}, matching the macOS lane. App Store Connect rejects a reused CFBundleVersion; the value in project.yml is only a local-dev default.
  • Concurrency group prevents two runs racing for the same build number. Signing material and the keychain are removed in an always() step.
  • No submodules on checkout — the iOS app needs vendor/CmuxIrohTransport (in-tree) and iroh-ffi (SwiftPM), not ghostty.

Known and deliberately not fixed

Iroh.framework ships no dSYM, so symbol upload warns and crash reports from inside that dependency will not symbolize. It is a prebuilt binary from iroh-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 Profiles rather than made from scratch.

Secret What
APPLE_IOS_DIST_CERT_BASE64 Apple Distribution cert as base64 .p12
APPLE_IOS_DIST_CERT_PASSWORD its password
APPLE_IOS_APP_PROFILE_BASE64 App Store profile for com.darkroom.programa.spike
APPLE_IOS_WIDGET_PROFILE_BASE64 App Store profile for …spike.widgets
APPSTORE_CONNECT_KEY_ID / _ISSUER_ID / _KEY_P8_BASE64 App Store Connect API key (upload only)

Test plan

  • bash -n scripts/build-ios-testflight.sh passes
  • Missing-env guard exits 1 naming the offending variable
  • Orientation pre-flight passes on current source; TARGETED_DEVICE_FAMILY parses as 1,2
  • Unsigned device build of both targets succeeds (generic/platform=iOS, Release)
  • Archive with orientations declared succeeds and the interface-orientation warning is gone
  • Archived bundle carries both orientation keys plus UIBackgroundModes, NSSupportsLiveActivities, ITSAppUsesNonExemptEncryption
  • The archive → export → verify sequence was run by hand and produced aps-environment: production, beta-reports-active: true, get-task-allow: false
  • workflow-guard-tests passes
  • One workflow_dispatch run with upload: false once the signing secrets exist

arzafran added 2 commits July 29, 2026 18:50
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.
@arzafran
arzafran merged commit e5c9609 into main Jul 29, 2026
6 of 8 checks passed
@arzafran
arzafran deleted the ci/ios-testflight branch July 29, 2026 22:13
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