mirror of
https://github.com/bulwarkmail/native.git
synced 2026-10-31 16:06:32 +00:00
Uploads only reached internal testers. The public link's "external" group still served 0.1.66, because nothing added later builds to it or submitted them for Beta App Review. A new `public-testflight` job runs after the upload on a Linux runner. It waits for App Store Connect to process the build, adds it to the external group, sets "What to Test" to the GitHub release and submits it for review (`scripts/testflight-public.mjs`, safe to re-run). The README links the TestFlight beta and the latest APK.
174 lines
7.6 KiB
Markdown
174 lines
7.6 KiB
Markdown
# iOS builds and TestFlight
|
|
|
|
The iOS app is built by [.github/workflows/release-ios.yml](../.github/workflows/release-ios.yml)
|
|
on a GitHub-hosted macOS runner and uploaded straight to TestFlight. macOS runner
|
|
minutes are free for this repository because it is public.
|
|
|
|
Unlike `android/`, which is committed and hand-maintained, **`ios/` is gitignored
|
|
and regenerated by `expo prebuild` on every run.** Never commit it, and never fix
|
|
anything by editing it - native iOS changes belong in `app.config.js` or a config
|
|
plugin, or they will be silently thrown away on the next build.
|
|
|
|
Without the secrets below the workflow still runs, but only as an unsigned
|
|
compile check; no IPA is produced.
|
|
|
|
## One-time Apple setup
|
|
|
|
You need an active Apple Developer Program membership ($99/yr). Everything here
|
|
can be done from Windows - no Mac required.
|
|
|
|
### 1. Team ID
|
|
|
|
developer.apple.com → Account → Membership details. A 10-character string like
|
|
`A1B2C3D4E5`.
|
|
|
|
### 2. Distribution certificate
|
|
|
|
Apple issues certificates against a certificate signing request. Keychain Access
|
|
is the usual way to make one, but OpenSSL works and does not need a Mac:
|
|
|
|
```bash
|
|
openssl genrsa -out dist.key 2048
|
|
openssl req -new -key dist.key -out dist.csr \
|
|
-subj "/emailAddress=you@bulwarkmail.org/CN=Bulwark Mail/C=DE"
|
|
```
|
|
|
|
Upload `dist.csr` at developer.apple.com → Certificates → **+** → **Apple
|
|
Distribution**, download the resulting `distribution.cer`, then bundle the cert
|
|
and its private key into a `.p12`:
|
|
|
|
```bash
|
|
openssl x509 -inform DER -in distribution.cer -out dist.pem
|
|
openssl pkcs12 -export -inkey dist.key -in dist.pem -out dist.p12 \
|
|
-passout pass:CHOOSE_A_PASSWORD
|
|
```
|
|
|
|
**On OpenSSL 3.x, add `-legacy` to that last command.** OpenSSL 3 defaults to
|
|
AES-256-CBC, which macOS `security import` refuses to read, and the workflow then
|
|
fails at the import step. OpenSSL 1.x has no `-legacy` flag and already writes
|
|
the format macOS wants - passing it there is an error. Check with `openssl
|
|
version` first; Git Bash on Windows currently ships 1.1.1.
|
|
|
|
Keep `dist.key` somewhere safe - you cannot re-download it, and losing it means
|
|
revoking the certificate and starting over.
|
|
|
|
### 3. App Store Connect API key
|
|
|
|
App Store Connect → Users and Access → Integrations → App Store Connect API →
|
|
**+**, with the **App Manager** role. Download the `.p8` (**one download only**)
|
|
and note the **Key ID** and the **Issuer ID** shown above the key list.
|
|
|
|
The key authenticates the TestFlight upload.
|
|
|
|
### 4. App ID, then app record
|
|
|
|
These live on two different Apple sites and have to be done in this order.
|
|
|
|
**developer.apple.com** → Certificates, Identifiers & Profiles → Identifiers →
|
|
**+** → App IDs → App. Explicit bundle ID `org.bulwarkmail.mobile`, matching
|
|
`ios.bundleIdentifier` in [app.config.js](../app.config.js). Leave every
|
|
capability off: push is not implemented on iOS, and sign-in uses the custom
|
|
`bulwarkmobile://` scheme rather than universal links, so no Associated Domains.
|
|
|
|
**appstoreconnect.apple.com** → Apps → **+** → New App. Platform iOS, any SKU.
|
|
The bundle ID here is a dropdown populated from the identifiers above, which is
|
|
why the App ID has to be registered first. The app name must be unique across
|
|
the entire App Store.
|
|
|
|
The TestFlight upload is rejected if the app record does not exist - and that
|
|
failure lands at the very end of a ~30 minute build.
|
|
|
|
### 5. App Store provisioning profile
|
|
|
|
developer.apple.com → Profiles → **+** → Distribution → **App Store Connect**.
|
|
Pick the App ID from step 4 and the distribution certificate from step 2, name it
|
|
`Bulwark Mobile App Store`, and download the `.mobileprovision`.
|
|
|
|
The build signs **manually** and this profile is not optional. Automatic signing
|
|
would be less to maintain, but `xcodebuild archive` under automatic signing always
|
|
signs with a *development* identity and leans on `-exportArchive` to re-sign for
|
|
distribution afterwards. That needs a development certificate and at least one
|
|
registered device - neither of which a CI-only TestFlight pipeline has any reason
|
|
to own, and Apple will not issue a development profile without a device. Naming
|
|
the App Store profile up front avoids the whole problem.
|
|
|
|
The profile expires when the certificate does (2027-08-01). Regenerate it here and
|
|
update the secret when that happens.
|
|
|
|
## Repository secrets
|
|
|
|
Settings → Secrets and variables → Actions:
|
|
|
|
| Secret | Value |
|
|
| --- | --- |
|
|
| `APPLE_TEAM_ID` | The 10-character team ID from step 1 |
|
|
| `IOS_DIST_CERT_BASE64` | `dist.p12` from step 2, base64-encoded |
|
|
| `IOS_DIST_CERT_PASSWORD` | The password you chose for `dist.p12` |
|
|
| `APP_STORE_CONNECT_KEY_ID` | Key ID from step 3 |
|
|
| `APP_STORE_CONNECT_ISSUER_ID` | Issuer ID from step 3 |
|
|
| `APP_STORE_CONNECT_PRIVATE_KEY` | The `.p8` from step 3, base64-encoded |
|
|
| `IOS_PROVISIONING_PROFILE_BASE64` | The `.mobileprovision` from step 5, base64-encoded |
|
|
|
|
To base64-encode a file:
|
|
|
|
```powershell
|
|
# PowerShell
|
|
[Convert]::ToBase64String([IO.File]::ReadAllBytes('dist.p12')) | Set-Clipboard
|
|
```
|
|
|
|
```bash
|
|
# bash
|
|
base64 -w0 dist.p12
|
|
```
|
|
|
|
## Running a build
|
|
|
|
Publishing a GitHub release triggers the iOS workflow alongside the Android one.
|
|
You can also run it by hand from the Actions tab, with an
|
|
`upload_to_testflight` toggle if you only want the IPA as a workflow artifact.
|
|
|
|
The IPA is deliberately not attached to the GitHub release: unlike the APK, it is
|
|
not installable outside TestFlight.
|
|
|
|
`CFBundleVersion` comes from the workflow run number via the `IOS_BUILD_NUMBER`
|
|
env var. App Store Connect rejects a build number it has already seen for a given
|
|
version, so never re-run a workflow expecting a re-upload to succeed - the run
|
|
number advances on its own, so just start a new run.
|
|
|
|
## After the upload
|
|
|
|
Processing takes roughly 5-30 minutes before the build shows up in TestFlight.
|
|
Internal testers (up to 100, must be on your App Store Connect team) get it
|
|
immediately. External testers (up to 10,000) need a Beta App Review, which takes
|
|
about a day the first time and is usually waved through afterwards.
|
|
|
|
The workflow's `public-testflight` job does the external part on its own. It
|
|
waits for processing, adds the build to the `external` group (the one behind
|
|
the public link in the README), sets "What to Test" to the GitHub release and
|
|
submits it for Beta App Review (`scripts/testflight-public.mjs`). To do this by
|
|
hand for a build, for example after a failed run:
|
|
|
|
```bash
|
|
ASC_KEY_ID=… ASC_ISSUER_ID=… ASC_KEY_PATH=path/to/AuthKey_….p8 node scripts/testflight-public.mjs 0.1.72 18 # marketing version, build number
|
|
```
|
|
|
|
If App Store Connect answers `REQUIRED_AGREEMENTS_MISSING_OR_EXPIRED` (altool
|
|
reports it as "Cannot determine the Apple ID from Bundle ID"), the Account
|
|
Holder has to accept an updated agreement under Business in App Store Connect.
|
|
|
|
## What does not work on iOS yet
|
|
|
|
- **Push notifications.** The push path is a hand-written Android module talking
|
|
to FCM (`BulwarkFcmModule`, `BulwarkMessagingService`). iOS needs an APNs key
|
|
and an equivalent native module; until then `push-notifications.ts` short-
|
|
circuits and the app polls like a device with push disabled.
|
|
- **Client certificates.** `BulwarkClientCertModule` is Android-only.
|
|
`secureFetch` falls back to plain `fetch`, so cert-authenticated servers will
|
|
not connect.
|
|
- **In-app updates.** Hidden on purpose - see `supportsSideloadUpdates` in
|
|
[src/lib/platform-capabilities.ts](../src/lib/platform-capabilities.ts).
|
|
Downloading and installing a binary is forbidden by App Store Review guideline
|
|
2.5.2, so iOS updates come through TestFlight instead. The Updates settings
|
|
pane is removed from the list on iOS; the version and commit are still shown
|
|
under About & Data.
|