Where this stands
The iOS companion is not on TestFlight. No build has ever reached a tester
through CI — ios-testflight.yml existed for weeks and had never run once.
That lane now ships by itself on any push to main that touches the phone app
(#201), and pairing was collapsed from two hand-copied strings to one QR scan
(#202). Both are merged. The lane is wired, verified, and currently skipping every
run because the Apple signing material is not on the repo.
Three things left. Only the first blocks shipping.
1. Add the Apple signing secrets (blocking, needs Apple account access)
ios-testflight.yml runs on every ios/** push to main and stops at
preflight with configured=false. It warns and skips rather than failing, so
main stays green and nothing reaches testers. Two consecutive runs have skipped
this way — that is the designed behaviour, not a bug, but it means the companion
is not shipping.
Add under Settings → Secrets and variables → Actions:
| secret |
what it is |
APPLE_IOS_DIST_CERT_BASE64 |
Apple Distribution cert as .p12, base64. A Developer ID or Apple Development cert cannot sign for TestFlight — the build script rejects them explicitly. |
APPLE_IOS_DIST_CERT_PASSWORD |
the .p12 export password |
APPLE_IOS_APP_PROFILE_BASE64 |
App Store profile for com.darkroom.programa.spike |
APPLE_IOS_WIDGET_PROFILE_BASE64 |
App Store profile for com.darkroom.programa.spike.widgets |
APPSTORE_CONNECT_KEY_ID |
App Store Connect → Users and Access → Integrations |
APPSTORE_CONNECT_ISSUER_ID |
same page |
APPSTORE_CONNECT_KEY_P8_BASE64 |
the .p8 key, base64 |
Encode each with base64 -i <file> | pbcopy.
Regenerate both provisioning profiles — do not reuse the existing ones.
plans/golden-tumbling-gray.md:490-493 records that the iOS profiles were minted
before the iCloud container was attached to the App ID, so they carry no
containers. scripts/build-ios-testflight.sh also hard-fails when the app profile
has aps-environment != production, which is what happened on the first manual
upload. Mint them with Push Notifications enabled on the App ID.
Verify, in order:
gh workflow run ios-testflight.yml -f upload=false → run completes
successfully with build no longer skipped, and the "Verifying signed
entitlements" step printing ok aps-environment = production,
ok get-task-allow = false,
ok com.apple.developer.icloud-container-environment = Production.
- Only once that passes:
gh workflow run ios-testflight.yml -f upload=false
again is unnecessary — push any ios/** change to main and confirm the
upload step runs and the build appears in App Store Connect after processing.
2. Localize the iOS strings
ios/ProgramaSpike/ has no String Catalog and its convention is bare English
literals, so the strings added in #202 (PairConnectView, QRScannerView,
AppStore) are unlocalized. The Mac side of the same feature is localized in
Resources/Localizable.xcstrings (en + ja).
CLAUDE.md's localization rule is repo-wide, so this is a real gap rather than a
target-local style choice. It needs a String Catalog added to the iOS target and
the existing literals migrated — worth its own change, and worth doing before
Japanese testers get a build.
Verify: every user-facing string in ios/ProgramaSpike/ProgramaSpike/*.swift
reaches the UI through String(localized:defaultValue:), with en + ja entries,
and the app builds:
xcodebuild -project ios/ProgramaSpike/ProgramaSpike.xcodeproj -scheme ProgramaSpike -configuration Release -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build
→ ** BUILD SUCCEEDED **
3. Optional: make the pairing QR an https:// universal link
Today the QR encodes programa-pair://pair?v=1&t=<ticket>&k=<token>, a custom
scheme. It works, but a tester has to open Programa on the phone, find "Scan QR
Code", then scan — three steps.
pingdotgg/t3code encodes a real https:// URL instead
(packages/shared/src/remote.ts:165-170), so any camera app opens it directly:
point, tap, done. Their in-app scanner is a convenience layer rather than the only
route in.
Cost: an apple-app-site-association file on a domain we control. A static file
on an existing site is not "a server we host", so it does not break that
constraint, but it is real setup.
If we do this, the token must move into the URL fragment (#token=), not the
query string — that is exactly why t3code puts it there. A query string reaches
the web origin and its access logs; a fragment does not. This does not apply to
the current custom scheme, which never touches a server, which is why it was left
alone in #202.
Defer until the companion is in testers' hands and we know whether the two extra
taps actually matter.
Not in scope, noted so it is not rediscovered
- The bundle id is still
com.darkroom.programa.spike. Renaming means a new App
ID, a new App Store Connect record, and re-attaching the iCloud container.
Testers only ever see the display name "Programa", so this is not worth doing.
MARKETING_VERSION in ios/ProgramaSpike/project.yml is 0.1.
- The two pairing-code parsers (
Sources/MobileBridge/MobileBridgePairingCode.swift
and ios/ProgramaSpike/ProgramaSpike/PairingCode.swift) are separate copies kept
in sync by hand, because the two targets share no module. Both validate v=1 and
reject anything else. Any format change must land on the phone before the Mac
starts emitting it, since the companion ships through TestFlight and will lag.
Where this stands
The iOS companion is not on TestFlight. No build has ever reached a tester
through CI —
ios-testflight.ymlexisted for weeks and had never run once.That lane now ships by itself on any push to
mainthat touches the phone app(#201), and pairing was collapsed from two hand-copied strings to one QR scan
(#202). Both are merged. The lane is wired, verified, and currently skipping every
run because the Apple signing material is not on the repo.
Three things left. Only the first blocks shipping.
1. Add the Apple signing secrets (blocking, needs Apple account access)
ios-testflight.ymlruns on everyios/**push tomainand stops atpreflightwithconfigured=false. It warns and skips rather than failing, somainstays green and nothing reaches testers. Two consecutive runs have skippedthis way — that is the designed behaviour, not a bug, but it means the companion
is not shipping.
Add under Settings → Secrets and variables → Actions:
APPLE_IOS_DIST_CERT_BASE64.p12, base64. A Developer ID or Apple Development cert cannot sign for TestFlight — the build script rejects them explicitly.APPLE_IOS_DIST_CERT_PASSWORD.p12export passwordAPPLE_IOS_APP_PROFILE_BASE64com.darkroom.programa.spikeAPPLE_IOS_WIDGET_PROFILE_BASE64com.darkroom.programa.spike.widgetsAPPSTORE_CONNECT_KEY_IDAPPSTORE_CONNECT_ISSUER_IDAPPSTORE_CONNECT_KEY_P8_BASE64.p8key, base64Encode each with
base64 -i <file> | pbcopy.Regenerate both provisioning profiles — do not reuse the existing ones.
plans/golden-tumbling-gray.md:490-493records that the iOS profiles were mintedbefore the iCloud container was attached to the App ID, so they carry no
containers.
scripts/build-ios-testflight.shalso hard-fails when the app profilehas
aps-environment != production, which is what happened on the first manualupload. Mint them with Push Notifications enabled on the App ID.
Verify, in order:
gh workflow run ios-testflight.yml -f upload=false→ run completessuccessfully with
buildno longer skipped, and the "Verifying signedentitlements" step printing
ok aps-environment = production,ok get-task-allow = false,ok com.apple.developer.icloud-container-environment = Production.gh workflow run ios-testflight.yml -f upload=falseagain is unnecessary — push any
ios/**change tomainand confirm theupload step runs and the build appears in App Store Connect after processing.
2. Localize the iOS strings
ios/ProgramaSpike/has no String Catalog and its convention is bare Englishliterals, so the strings added in #202 (
PairConnectView,QRScannerView,AppStore) are unlocalized. The Mac side of the same feature is localized inResources/Localizable.xcstrings(en + ja).CLAUDE.md's localization rule is repo-wide, so this is a real gap rather than a
target-local style choice. It needs a String Catalog added to the iOS target and
the existing literals migrated — worth its own change, and worth doing before
Japanese testers get a build.
Verify: every user-facing string in
ios/ProgramaSpike/ProgramaSpike/*.swiftreaches the UI through
String(localized:defaultValue:), with en + ja entries,and the app builds:
xcodebuild -project ios/ProgramaSpike/ProgramaSpike.xcodeproj -scheme ProgramaSpike -configuration Release -destination 'generic/platform=iOS' CODE_SIGNING_ALLOWED=NO build→
** BUILD SUCCEEDED **3. Optional: make the pairing QR an
https://universal linkToday the QR encodes
programa-pair://pair?v=1&t=<ticket>&k=<token>, a customscheme. It works, but a tester has to open Programa on the phone, find "Scan QR
Code", then scan — three steps.
pingdotgg/t3codeencodes a realhttps://URL instead(
packages/shared/src/remote.ts:165-170), so any camera app opens it directly:point, tap, done. Their in-app scanner is a convenience layer rather than the only
route in.
Cost: an
apple-app-site-associationfile on a domain we control. A static fileon an existing site is not "a server we host", so it does not break that
constraint, but it is real setup.
If we do this, the token must move into the URL fragment (
#token=), not thequery string — that is exactly why t3code puts it there. A query string reaches
the web origin and its access logs; a fragment does not. This does not apply to
the current custom scheme, which never touches a server, which is why it was left
alone in #202.
Defer until the companion is in testers' hands and we know whether the two extra
taps actually matter.
Not in scope, noted so it is not rediscovered
com.darkroom.programa.spike. Renaming means a new AppID, a new App Store Connect record, and re-attaching the iCloud container.
Testers only ever see the display name "Programa", so this is not worth doing.
MARKETING_VERSIONinios/ProgramaSpike/project.ymlis0.1.Sources/MobileBridge/MobileBridgePairingCode.swiftand
ios/ProgramaSpike/ProgramaSpike/PairingCode.swift) are separate copies keptin sync by hand, because the two targets share no module. Both validate
v=1andreject anything else. Any format change must land on the phone before the Mac
starts emitting it, since the companion ships through TestFlight and will lag.