|
| 1 | +# Publishing Range FEA in the Apple Mac App Store |
| 2 | + |
| 3 | +This guide covers the one-time setup of the Apple accounts and the GitHub |
| 4 | +repository, and the steps repeated for every release. The package itself is |
| 5 | +built and uploaded by the GitHub Actions workflow |
| 6 | +[`.github/workflows/store.yml`](../.github/workflows/store.yml) (job |
| 7 | +`mac-app-store`). It runs **only when a GitHub release is published**. |
| 8 | + |
| 9 | +Overview of what happens on a release: |
| 10 | + |
| 11 | +1. A release is published on GitHub. |
| 12 | +2. `store.yml` builds the application on a macOS runner with |
| 13 | + `-DRANGE_STORE_BUILD=ON`. The bundle is sandboxed, has no built-in updater, |
| 14 | + and is signed with the App Store certificates. |
| 15 | +3. The bundle is wrapped in a signed installer package (`.pkg`) and uploaded to |
| 16 | + App Store Connect. |
| 17 | +4. In App Store Connect you test the build through TestFlight and submit it for |
| 18 | + review. You also decide when it goes on sale. |
| 19 | + |
| 20 | +--- |
| 21 | + |
| 22 | +## 1. One-time setup |
| 23 | + |
| 24 | +### 1.1 Apple Developer Program membership |
| 25 | + |
| 26 | +1. Enrol at <https://developer.apple.com/programs/enroll/> with the Apple Account |
| 27 | + that will own the application. A company enrolment needs a D-U-N-S number. |
| 28 | + The fee is yearly. |
| 29 | +2. Wait for the enrolment to be approved. Everything below needs an active |
| 30 | + membership. |
| 31 | + |
| 32 | +### 1.2 Team ID → repository variable `MACOS_TEAM_ID` |
| 33 | + |
| 34 | +1. Open <https://developer.apple.com/account> and go to **Membership details**. |
| 35 | +2. Copy the **Team ID**, a 10-character value such as `ABCDE12345`. |
| 36 | + |
| 37 | +Without it the `mac-app-store` CMake target is not created and the workflow fails. |
| 38 | + |
| 39 | +### 1.3 Bundle identifier → repository variable `MACOS_BUNDLE_IDENTIFIER` |
| 40 | + |
| 41 | +The workflow uses `com.range-software.fea` unless the variable is set. Register |
| 42 | +the identifier once: |
| 43 | + |
| 44 | +1. In <https://developer.apple.com/account/resources/identifiers/list> click **+**. |
| 45 | +2. Choose **App IDs** → **App**, then **Continue**. |
| 46 | +3. Description: `Range FEA`. Bundle ID: **Explicit**, `com.range-software.fea`. |
| 47 | +4. Leave all capabilities unticked. The application needs none beyond the App |
| 48 | + Sandbox, which comes from the entitlements. |
| 49 | +5. Click **Continue**, then **Register**. |
| 50 | + |
| 51 | +If you register a different identifier, set the repository variable |
| 52 | +`MACOS_BUNDLE_IDENTIFIER` to it. The bundle identifier can never change once a |
| 53 | +build has been uploaded. |
| 54 | + |
| 55 | +### 1.4 Signing certificates → secrets `MAC_APP_STORE_CERT_P12`, `MAC_APP_STORE_CERT_P12_PASSWORD`, `MACOS_APP_STORE_APP_CERT`, `MACOS_APP_STORE_INSTALLER_CERT` |
| 56 | + |
| 57 | +Two certificates are needed. Both are created on a Mac. |
| 58 | + |
| 59 | +| Certificate type in the portal | Signs | Identity name in the keychain | |
| 60 | +|---|---|---| |
| 61 | +| **Mac App Distribution** | the application bundle | `3rd Party Mac Developer Application: <Team name> (<Team ID>)` | |
| 62 | +| **Mac Installer Distribution** | the `.pkg` installer | `3rd Party Mac Developer Installer: <Team name> (<Team ID>)` | |
| 63 | + |
| 64 | +Create them: |
| 65 | + |
| 66 | +1. On a Mac open **Keychain Access** → **Keychain Access** menu → **Certificate |
| 67 | + Assistant** → **Request a Certificate From a Certificate Authority**. |
| 68 | + - Enter your e-mail and a common name, and leave the CA e-mail empty. |
| 69 | + - Select **Saved to disk**. This produces a `.certSigningRequest` file. |
| 70 | +2. In <https://developer.apple.com/account/resources/certificates/list> click **+**. |
| 71 | + - Select **Mac App Distribution**, then **Continue**. |
| 72 | + - Upload the request file and download the `.cer`. |
| 73 | +3. Repeat step 2 for **Mac Installer Distribution**. The same request file can be |
| 74 | + reused. |
| 75 | +4. Double-click both `.cer` files to import them into the **login** keychain. |
| 76 | + |
| 77 | +Export both identities into **one** `.p12` file: |
| 78 | + |
| 79 | +1. In Keychain Access → **My Certificates**, select both `3rd Party Mac Developer |
| 80 | + …` entries. Each must have a private key underneath it. |
| 81 | +2. Right-click → **Export 2 items…** → format **Personal Information Exchange |
| 82 | + (.p12)**, for example `appstore.p12`. |
| 83 | +3. Choose a strong password. It becomes the secret `MAC_APP_STORE_CERT_P12_PASSWORD`. |
| 84 | +4. Encode the file for the secret `MAC_APP_STORE_CERT_P12`: |
| 85 | + |
| 86 | + ```bash |
| 87 | + base64 -i appstore.p12 -o appstore.p12.base64.txt |
| 88 | + ``` |
| 89 | + |
| 90 | +Read the exact identity names for the two remaining secrets: |
| 91 | + |
| 92 | +```bash |
| 93 | +security find-identity -v |
| 94 | +``` |
| 95 | + |
| 96 | +- `MACOS_APP_STORE_APP_CERT` gets the full name in quotes, for example |
| 97 | + `3rd Party Mac Developer Application: Range Software (ABCDE12345)`. |
| 98 | +- `MACOS_APP_STORE_INSTALLER_CERT` gets |
| 99 | + `3rd Party Mac Developer Installer: Range Software (ABCDE12345)`. |
| 100 | + |
| 101 | +If `find-identity` does not list the installer certificate, copy its **Common |
| 102 | +Name** from Keychain Access instead. It is the same string. |
| 103 | + |
| 104 | +Keep `appstore.p12` and its password in a password manager, then delete the |
| 105 | +local copies of the `.p12` and the `.txt`. |
| 106 | + |
| 107 | +### 1.5 Provisioning profile → secret `MAC_PROVISION_PROFILE` |
| 108 | + |
| 109 | +1. In <https://developer.apple.com/account/resources/profiles/list> click **+**. |
| 110 | +2. Under **Distribution** select **Mac App Store Connect**, then **Continue**. |
| 111 | +3. Profile type **Mac**. App ID: the one registered in 1.3. |
| 112 | +4. Certificate: the **Mac App Distribution** certificate from 1.4. |
| 113 | +5. Name it `Range FEA App Store`, then **Generate** → **Download** to get a |
| 114 | + `.provisionprofile` file. |
| 115 | +6. Encode it: |
| 116 | + |
| 117 | + ```bash |
| 118 | + base64 -i Range_FEA_App_Store.provisionprofile -o profile.base64.txt |
| 119 | + ``` |
| 120 | + |
| 121 | +The profile expires together with the certificate, after one year. Renew both |
| 122 | +and update the secrets when that happens. |
| 123 | + |
| 124 | +### 1.6 App Store Connect API key → secrets `AC_API_KEY_ID`, `AC_API_ISSUER_ID`, `AC_API_KEY_P8` |
| 125 | + |
| 126 | +The release workflow `ci.yml` already uses these secrets for notarization. The |
| 127 | +same key also works for the upload if its role is **App Manager** or **Admin**. |
| 128 | +If the existing key has the **Developer** role, create a new one: |
| 129 | + |
| 130 | +1. <https://appstoreconnect.apple.com> → **Users and Access** → **Integrations** → |
| 131 | + **App Store Connect API** → **Team Keys** → **+**. |
| 132 | +2. Name `GitHub Actions`, access **App Manager**, then **Generate**. |
| 133 | +3. Copy the **Issuer ID** shown above the table → secret `AC_API_ISSUER_ID`. |
| 134 | +4. Copy the **Key ID** of the new key → secret `AC_API_KEY_ID`. |
| 135 | +5. **Download** the `AuthKey_<KeyID>.p8`. It can be downloaded only once. Encode it: |
| 136 | + |
| 137 | + ```bash |
| 138 | + base64 -i AuthKey_XXXXXXXXXX.p8 -o authkey.base64.txt |
| 139 | + ``` |
| 140 | + |
| 141 | + The encoded text becomes the secret `AC_API_KEY_P8`. |
| 142 | + |
| 143 | +### 1.7 Keychain password → secret `MAC_KEYCHAIN_PASSWORD` |
| 144 | + |
| 145 | +This is the password of the temporary keychain created on the runner. Any random |
| 146 | +string works. The secret already exists if `ci.yml` signs macOS builds; otherwise |
| 147 | +generate one: |
| 148 | + |
| 149 | +```bash |
| 150 | +openssl rand -base64 24 |
| 151 | +``` |
| 152 | + |
| 153 | +### 1.8 Store the values in GitHub |
| 154 | + |
| 155 | +Repository → **Settings** → **Secrets and variables** → **Actions**. |
| 156 | + |
| 157 | +| Name | Kind | Value from | |
| 158 | +|---|---|---| |
| 159 | +| `MACOS_TEAM_ID` | Variable | 1.2 | |
| 160 | +| `MACOS_BUNDLE_IDENTIFIER` | Variable, optional | 1.3, defaults to `com.range-software.fea` | |
| 161 | +| `MAC_APP_STORE_CERT_P12` | Secret | 1.4, contents of `appstore.p12.base64.txt` | |
| 162 | +| `MAC_APP_STORE_CERT_P12_PASSWORD` | Secret | 1.4 | |
| 163 | +| `MACOS_APP_STORE_APP_CERT` | Secret | 1.4 | |
| 164 | +| `MACOS_APP_STORE_INSTALLER_CERT` | Secret | 1.4 | |
| 165 | +| `MAC_PROVISION_PROFILE` | Secret | 1.5, contents of `profile.base64.txt` | |
| 166 | +| `AC_API_KEY_ID` | Secret | 1.6 | |
| 167 | +| `AC_API_ISSUER_ID` | Secret | 1.6 | |
| 168 | +| `AC_API_KEY_P8` | Secret | 1.6, contents of `authkey.base64.txt` | |
| 169 | +| `MAC_KEYCHAIN_PASSWORD` | Secret | 1.7 | |
| 170 | + |
| 171 | +With the GitHub CLI, the same can be done from the folder holding the files: |
| 172 | + |
| 173 | +```bash |
| 174 | +gh variable set MACOS_TEAM_ID --body "ABCDE12345" |
| 175 | +gh secret set MAC_APP_STORE_CERT_P12 < appstore.p12.base64.txt |
| 176 | +gh secret set MAC_APP_STORE_CERT_P12_PASSWORD |
| 177 | +gh secret set MACOS_APP_STORE_APP_CERT --body "3rd Party Mac Developer Application: Range Software (ABCDE12345)" |
| 178 | +gh secret set MACOS_APP_STORE_INSTALLER_CERT --body "3rd Party Mac Developer Installer: Range Software (ABCDE12345)" |
| 179 | +gh secret set MAC_PROVISION_PROFILE < profile.base64.txt |
| 180 | +gh secret set AC_API_KEY_P8 < authkey.base64.txt |
| 181 | +``` |
| 182 | + |
| 183 | +### 1.9 Create the application record in App Store Connect |
| 184 | + |
| 185 | +The upload fails until the record exists. |
| 186 | + |
| 187 | +1. <https://appstoreconnect.apple.com> → **Apps** → **+** → **New App**. |
| 188 | +2. Platforms **macOS**. Name `Range FEA`, which must be unique in the store. |
| 189 | + Primary language **English (U.S.)**. |
| 190 | +3. Bundle ID: the one from 1.3. SKU: any internal identifier, for example |
| 191 | + `range-fea`. User access **Full Access**. |
| 192 | +4. Click **Create**. |
| 193 | + |
| 194 | +--- |
| 195 | + |
| 196 | +## 2. Every release |
| 197 | + |
| 198 | +### 2.1 Prepare the version |
| 199 | + |
| 200 | +1. Set the release version in `src/Config.cmake` (`PROJECT_VERSION`). It becomes |
| 201 | + `CFBundleShortVersionString`, the version customers see. App Store Connect |
| 202 | + accepts each version string for one approved release only, so every |
| 203 | + submission to review needs a new version. |
| 204 | +2. Update the release notes, commit and push. |
| 205 | + |
| 206 | +The build number (`CFBundleVersion`) is set automatically to the run number of |
| 207 | +`store.yml`. It grows with every run, so re-running a failed release needs no |
| 208 | +manual change. |
| 209 | + |
| 210 | +### 2.2 Publish the GitHub release |
| 211 | + |
| 212 | +1. GitHub → **Releases** → **Draft a new release**. |
| 213 | +2. Create a new tag, for example `v1.3.0`, on the commit to release. Fill in the |
| 214 | + notes. |
| 215 | +3. Saving the draft triggers nothing. Click **Publish release**. Pre-releases |
| 216 | + trigger the store build too. |
| 217 | + |
| 218 | +Publishing starts two workflows: `CI (Linux/macOS/Windows)` for the GitHub and |
| 219 | +Range Cloud packages, and `Store packages (App Store / Microsoft Store)`. |
| 220 | + |
| 221 | +### 2.3 Follow the store workflow |
| 222 | + |
| 223 | +Repository → **Actions** → **Store packages (App Store / Microsoft Store)** → |
| 224 | +the run for the release → job **Apple App Store package**. |
| 225 | + |
| 226 | +- **Configure** fails with *MACOS_TEAM_ID is empty*: set the variable from 1.2. |
| 227 | +- **Import App Store certificates** lists no identities: the `.p12` or its |
| 228 | + password is wrong (1.4). |
| 229 | +- **Verify package** fails: the bundle lacks the provisioning profile or the |
| 230 | + application identifier entitlement. Check `MAC_PROVISION_PROFILE` and |
| 231 | + `MACOS_TEAM_ID`. |
| 232 | +- **Upload to App Store Connect** fails with an authentication error: the API |
| 233 | + key role is too low or a key secret is wrong (1.6). A rejection message that |
| 234 | + names an `ITMS-` code comes from Apple's validation. Fix the cause and re-run |
| 235 | + the job. |
| 236 | + |
| 237 | +The signed `.pkg` is also kept as the artifact `range-fea-mac-app-store` on the |
| 238 | +run page. |
| 239 | + |
| 240 | +To re-run after fixing a secret, use **Re-run jobs** on the same run. A failed |
| 241 | +upload does not use up the build number. |
| 242 | + |
| 243 | +### 2.4 Test with TestFlight |
| 244 | + |
| 245 | +1. App Store Connect e-mails you when the build has been processed, usually |
| 246 | + within 15–60 minutes. It appears under **TestFlight**. |
| 247 | +2. Under **Export Compliance** nothing has to be answered. The bundle declares |
| 248 | + `ITSAppUsesNonExemptEncryption = NO`. |
| 249 | +3. Add yourself to an internal testing group, install **TestFlight** from the Mac |
| 250 | + App Store, and install the build. |
| 251 | + |
| 252 | +A bundle signed for the App Store does not launch when started directly. |
| 253 | +TestFlight is the way to run it. Check at least: |
| 254 | + |
| 255 | +- opening, saving and solving a model stored **outside** the app container (for |
| 256 | + example on the Desktop), including a transient run that writes several result |
| 257 | + records; |
| 258 | +- the help browser, the materials database and Range Cloud login. |
| 259 | + |
| 260 | +### 2.5 Fill in the store listing (first release, then as needed) |
| 261 | + |
| 262 | +In App Store Connect → the app: |
| 263 | + |
| 264 | +- **App Information**: category (Productivity or Education), content rights, |
| 265 | + age rating questionnaire. |
| 266 | +- **Pricing and Availability**: price and countries. |
| 267 | +- **App Privacy**: privacy policy URL (required) and the data-collection |
| 268 | + questionnaire. Range Cloud, the AI assistant and the optional usage |
| 269 | + statistics send data over the network; describe them. |
| 270 | +- **The version page** (`macOS App` → version): |
| 271 | + - screenshots in 16:10, one of 1280×800, 1440×900, 2560×1600 or 2880×1800, |
| 272 | + at least one and up to ten; |
| 273 | + - description, keywords, support URL, marketing URL, copyright; |
| 274 | + - **App Review Information**: contact details, and a demo account if a |
| 275 | + reviewer needs Range Cloud access to see the features. Note in the review |
| 276 | + notes that the solver runs as a bundled helper executable. |
| 277 | + |
| 278 | +### 2.6 Submit for review and release |
| 279 | + |
| 280 | +1. On the version page, under **Build**, click **+** and select the build tested |
| 281 | + in 2.4. |
| 282 | +2. Choose the release option: **Manually release this version** or |
| 283 | + **Automatically release this version** after approval. |
| 284 | +3. **Add for Review** → **Submit for Review**. |
| 285 | +4. Review usually takes one to three days. If the build is rejected, the reason |
| 286 | + appears in the **Resolution Center**. Fix it, publish a new GitHub release |
| 287 | + (or re-run the workflow), select the new build and submit again. |
| 288 | +5. After approval, if manual release was chosen, click **Release This Version**. |
| 289 | + |
| 290 | +--- |
| 291 | + |
| 292 | +## 3. Building the store package by hand |
| 293 | + |
| 294 | +To reproduce the workflow on a Mac with the certificates in the login keychain |
| 295 | +(Homebrew `ninja`, `libomp`, `openssl@3`, and Qt 6.12 in `CMAKE_PREFIX_PATH`): |
| 296 | + |
| 297 | +```bash |
| 298 | +cmake -S src -B build-store -G Ninja \ |
| 299 | + -DCMAKE_BUILD_TYPE=Release \ |
| 300 | + -DRANGE_STORE_BUILD=ON \ |
| 301 | + -DRANGE_BUILD_NUMBER=<next unused build number> \ |
| 302 | + -DOpenMP_INCLUDES="$(brew --prefix libomp)/include" \ |
| 303 | + -DOpenMP_LIBRARY="$(brew --prefix libomp)/lib/libomp.dylib" \ |
| 304 | + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" \ |
| 305 | + -DMACOS_TEAM_ID=ABCDE12345 \ |
| 306 | + -DMACOS_APP_STORE_APP_CERT="3rd Party Mac Developer Application: Range Software (ABCDE12345)" \ |
| 307 | + -DMACOS_APP_STORE_INSTALLER_CERT="3rd Party Mac Developer Installer: Range Software (ABCDE12345)" \ |
| 308 | + -DMACOS_PROVISION_PROFILE="$HOME/Downloads/Range_FEA_App_Store.provisionprofile" |
| 309 | +cmake --build build-store --parallel |
| 310 | +cmake --build build-store --target mac-app-store |
| 311 | +``` |
| 312 | + |
| 313 | +The `.pkg` lands in `build-store/`. Upload it with **Transporter** from the Mac |
| 314 | +App Store, or: |
| 315 | + |
| 316 | +```bash |
| 317 | +xcrun altool --upload-app --type macos --file build-store/<package>.pkg \ |
| 318 | + --apiKey <Key ID> --apiIssuer <Issuer ID> |
| 319 | +``` |
| 320 | + |
| 321 | +`altool` looks for `AuthKey_<Key ID>.p8` in `~/.private_keys`. The build number |
| 322 | +must be higher than any build already uploaded, so pick one above the latest |
| 323 | +`store.yml` run number. |
0 commit comments