|
| 1 | +# Release signing setup |
| 2 | + |
| 3 | +One-time setup the repo owner has to do before the first `mobile-vX.Y.Z` |
| 4 | +tag can produce signed Android and TestFlight builds. After this is |
| 5 | +done, every tag push runs `.github/workflows/mobile-release.yml` end to |
| 6 | +end with no further manual steps. |
| 7 | + |
| 8 | +All secrets go to **GitHub → Settings → Secrets and variables → |
| 9 | +Actions → New repository secret** (or "Environment secret" if you want |
| 10 | +a manual approval gate around release). |
| 11 | + |
| 12 | +--- |
| 13 | + |
| 14 | +## Android |
| 15 | + |
| 16 | +### 1. Generate the upload keystore (one time, ever) |
| 17 | + |
| 18 | +```bash |
| 19 | +keytool -genkey -v \ |
| 20 | + -keystore upload.jks \ |
| 21 | + -alias myfaq-upload \ |
| 22 | + -keyalg RSA -keysize 4096 -validity 36500 \ |
| 23 | + -storetype JKS |
| 24 | +``` |
| 25 | + |
| 26 | +You'll be prompted for: |
| 27 | + |
| 28 | +- **Keystore password** — store it in 1Password / your password manager. |
| 29 | +- **Key password** — can be the same as the keystore password. |
| 30 | +- Distinguished Name fields — `CN=phpMyFAQ Mobile`, `O=phpMyFAQ`, |
| 31 | + rest as you like. The Play Store doesn't show this. |
| 32 | + |
| 33 | +> **Critical:** back up `upload.jks` somewhere durable |
| 34 | +> (1Password attachment, encrypted off-site backup). Losing it means |
| 35 | +> you can never publish another update under the same Play listing |
| 36 | +> without going through Play's "reset upload key" support flow. |
| 37 | +
|
| 38 | +### 2. Encode the keystore for GitHub |
| 39 | + |
| 40 | +```bash |
| 41 | +base64 -i upload.jks | pbcopy # macOS, copies to clipboard |
| 42 | +# or: base64 -i upload.jks > upload.jks.b64 |
| 43 | +``` |
| 44 | + |
| 45 | +### 3. Add the four Android secrets |
| 46 | + |
| 47 | +| Secret name | Value | |
| 48 | +|-----------------------------|-------| |
| 49 | +| `ANDROID_KEYSTORE_BASE64` | the base64 string from step 2 | |
| 50 | +| `ANDROID_KEYSTORE_PASSWORD` | the keystore password from step 1 | |
| 51 | +| `ANDROID_KEY_ALIAS` | `myfaq-upload` | |
| 52 | +| `ANDROID_KEY_PASSWORD` | the key password from step 1 | |
| 53 | + |
| 54 | +That's it for producing a signed AAB + APK. They'll be attached to |
| 55 | +the GitHub Release automatically when you push a `mobile-v*` tag. |
| 56 | + |
| 57 | +### 4. (Optional, deferred) Play Console upload |
| 58 | + |
| 59 | +To skip the manual "upload AAB to Play Console" step: |
| 60 | + |
| 61 | +1. In Play Console → Setup → API access, create a service account. |
| 62 | +2. Grant it "Release manager" on the app. |
| 63 | +3. Download the JSON key. |
| 64 | +4. Add as `PLAY_SERVICE_ACCOUNT_JSON` secret (paste the whole JSON). |
| 65 | + |
| 66 | +The current workflow doesn't yet wire this in (Phase 1.1). Until |
| 67 | +then, download the `androidApp-release-aab` artifact from the |
| 68 | +release run and upload it manually to the internal track. |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## iOS / TestFlight |
| 73 | + |
| 74 | +You need an Apple Developer account on the phpMyFAQ team and |
| 75 | +admin access to App Store Connect. |
| 76 | + |
| 77 | +### 1. Create the app record (one time) |
| 78 | + |
| 79 | +In App Store Connect → My Apps → "+": |
| 80 | + |
| 81 | +- Platform: iOS |
| 82 | +- Name: **MyFAQ.app** |
| 83 | +- Bundle ID: **app.myfaq.ios** (must match the locked bundle ID; |
| 84 | + create it under Certificates, Identifiers & Profiles → Identifiers |
| 85 | + first if it doesn't exist) |
| 86 | +- SKU: anything unique, e.g. `myfaq-ios` |
| 87 | + |
| 88 | +### 2. Distribution certificate |
| 89 | + |
| 90 | +In Xcode → Settings → Accounts → your team → Manage Certificates: |
| 91 | + |
| 92 | +1. "+" → **Apple Distribution**. |
| 93 | +2. Right-click the new cert → **Export Certificate…** → save as |
| 94 | + `dist.p12`, set a password (store it in 1Password). |
| 95 | +3. Encode and add to GitHub: |
| 96 | + ```bash |
| 97 | + base64 -i dist.p12 | pbcopy |
| 98 | + ``` |
| 99 | + |
| 100 | +| Secret name | Value | |
| 101 | +|---|---| |
| 102 | +| `IOS_DIST_CERT_P12_BASE64` | base64 of `dist.p12` | |
| 103 | +| `IOS_DIST_CERT_PASSWORD` | the export password | |
| 104 | + |
| 105 | +### 3. App Store provisioning profile |
| 106 | + |
| 107 | +In the Apple Developer portal → Profiles → "+": |
| 108 | + |
| 109 | +- Type: **App Store** (Distribution) |
| 110 | +- App ID: `app.myfaq.ios` |
| 111 | +- Certificate: the Apple Distribution cert from step 2 |
| 112 | +- Name: `MyFAQ App Store` |
| 113 | + |
| 114 | +Download the `.mobileprovision`, then: |
| 115 | + |
| 116 | +```bash |
| 117 | +base64 -i MyFAQ_App_Store.mobileprovision | pbcopy |
| 118 | +``` |
| 119 | + |
| 120 | +| Secret name | Value | |
| 121 | +|---|---| |
| 122 | +| `IOS_PROVISIONING_PROFILE_BASE64` | base64 of the `.mobileprovision` | |
| 123 | + |
| 124 | +### 4. App Store Connect API key (for the TestFlight upload) |
| 125 | + |
| 126 | +In App Store Connect → Users and Access → Integrations → App Store |
| 127 | +Connect API → "+": |
| 128 | + |
| 129 | +- Name: `MyFAQ CI` |
| 130 | +- Access: **App Manager** (or Developer if you want the minimum) |
| 131 | +- Download the `.p8` immediately — Apple shows it exactly once. |
| 132 | + |
| 133 | +Note the **Key ID** (10-char string) and the **Issuer ID** (UUID at |
| 134 | +the top of the page) shown next to the key. |
| 135 | + |
| 136 | +```bash |
| 137 | +base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy |
| 138 | +``` |
| 139 | + |
| 140 | +| Secret name | Value | |
| 141 | +|---|---| |
| 142 | +| `APP_STORE_CONNECT_API_KEY_ID` | the 10-char Key ID | |
| 143 | +| `APP_STORE_CONNECT_API_ISSUER_ID` | the Issuer UUID | |
| 144 | +| `APP_STORE_CONNECT_API_KEY_BASE64` | base64 of the `.p8` file | |
| 145 | + |
| 146 | +### 5. Set the development team in `project.yml` |
| 147 | + |
| 148 | +`mobile/iosApp/project.yml` currently has `DEVELOPMENT_TEAM: ""` — |
| 149 | +fill it with your 10-char Apple Team ID (Apple Developer portal → |
| 150 | +Membership). Commit that change. Without it, `xcodebuild archive` |
| 151 | +can't pick the right cert. |
| 152 | + |
| 153 | +--- |
| 154 | + |
| 155 | +## Verifying |
| 156 | + |
| 157 | +Once all the above are in place: |
| 158 | + |
| 159 | +```bash |
| 160 | +git tag -a mobile-v0.1.0 -m "Phase 1 read-only MVP" |
| 161 | +git push origin mobile-v0.1.0 |
| 162 | +``` |
| 163 | + |
| 164 | +Watch the run at **Actions → mobile-release**. Both jobs should |
| 165 | +finish green. After ~15 minutes the build appears under TestFlight → |
| 166 | +Internal Testing. The signed AAB + APK appear on the GitHub Release |
| 167 | +page automatically. |
| 168 | + |
| 169 | +If a secret is missing the corresponding job logs a `::warning::` |
| 170 | +and falls back to producing an unsigned artifact instead of failing |
| 171 | +the workflow — useful for sanity-checking the pipeline before the |
| 172 | +full secret set is in place. |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## Rotating / leaked secrets |
| 177 | + |
| 178 | +- **Android upload keystore leaked**: contact Google Play support to |
| 179 | + reset the upload key. Generate a new keystore, update all four |
| 180 | + Android secrets. The app-signing key (held by Google after Play |
| 181 | + App Signing enrollment) is unaffected. |
| 182 | +- **iOS distribution cert leaked**: revoke in Apple Developer portal, |
| 183 | + generate a new one, regenerate the provisioning profile, update |
| 184 | + `IOS_DIST_CERT_*` and `IOS_PROVISIONING_PROFILE_BASE64`. |
| 185 | +- **App Store Connect API key leaked**: revoke in App Store Connect, |
| 186 | + generate a new key, update the three `APP_STORE_CONNECT_API_*` |
| 187 | + secrets. Key revocation is instant. |
0 commit comments