Skip to content

Commit b969cb3

Browse files
committed
Add publishing guides
1 parent b2e4146 commit b969cb3

2 files changed

Lines changed: 535 additions & 0 deletions

File tree

‎doc/publish-apple-app-store.md‎

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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

Comments
 (0)