# Runbook: App Store submission (go / no-go)
The order matters: the web pages must be live before the binary is submitted, and the binary must be an App Store channel build. Nothing here is improvised — each step names the command or the file…
- Page:
- Updated: 2026-10-27
- Pinned copy of `Algorythmos-AI/gurbani-soul-ios` `docs/process/runbooks/app-store-submission.md` at `a386422`; the canonical file is
- From the Sri Guru Granth Sahib Ji — Knowledge Base wiki. Sri Guru Granth Sahib Ji is quoted verbatim, cited by Ang, and never altered.
---
The order matters: the web pages must be live before the binary is submitted, and the binary must
be an **App Store channel** build. Nothing here is improvised — each step names the command or the
file that proves it. Audit and plan: 2026-09-20.
## 0. What cannot be undone
- There is **no binary rollback on iOS**. A bad release is fixed by a new build ([ios-hotfix](https://docs.gurbanisoul.com/ios/process/runbooks/ios-hotfix/)).
- The EU trader details (address, phone, email) become **public** once declared.
- The app name can be changed only with a new version once it has been submitted.
## 1. Machine-checkable gates (an agent or CI closes these)
| # | Gate | Proof |
|---|---|---|
| M1 | The platform release `vX.Y.Z` is live and this app vendors it | platform: `/api/health.commit` == the `vX.Y.Z` tag commit; here: `vendor.lock.json` platform ref `vX.Y.Z`, `make gates` green |
| M2 | `/privacy` and `/support` are live on production and name **Gurbani Soul** | `@smoke` specs in the deploy gate; `uptime` workflow green |
| M3 | Repo gates green on the RC commit | `make gates` (vendored inputs, versions, iOS source/listing/archive gates) |
| M4 | The RC is an **App Store channel** build from green trunk | `make testflight TEAM_ID=… BUILD=N CHANNEL=appstore UPLOAD=1` — refuses a dirty tree, an off-trunk commit, red CI, Xcode < 26, or an unsigned Nitnem review |
| M5 | The ledger row is committed | `ios/testflight-builds.json` has the build with `channel: appstore`, `sdk: iphoneos26.x`; PR merged |
| M6 | This version may be submitted | `make appstore-preflight` exits 0 |
| M7 | Listing text is within limits and true | listing lint (part of M3); paste from `docs/ios/app-store-listing.md` only |
| M8 | App Store Connect holds exactly the listing doc | `make asc-diff` prints `RESULT: NO DRIFT` (GET only; run after pasting and again on submission day) |
## 2. Owner gates (only a person can close these — record name + date in the build log)
| # | Gate | Where |
|---|---|---|
| H1 | Scholar review of the Nitnem non-SGGS text → `REVIEWED: true`, and `NitnemReview.extraTextReviewed = true` in the **same commit** | `ios/Resources/NITNEM-REVIEW.md`, `docs/nitnem/review-pack/` |
| H2 | G3 — Granthi / scholar acceptance of the ੴ icon treatment and the default saroop rendering | platform [`docs/brand/gurbani-soul-brand-book.md`](https://github.com/Algorythmos-AI/sggs-platform/blob/main/docs/brand/gurbani-soul-brand-book.md) |
| H3 | G4 — legal / trade-dress clearance; the source edition's publisher noted; the ShabadOS 4.8.7 licence text recorded | brand book; `NOTICE.md` |
| H4 | Charter S (scripture fidelity) signed **on the RC build number** | `docs/ios/testflight-test-plan.md` |
| H5 | Hardware pass on the RC (below) | `ios/App/Tests/UI/A11Y_CHECKLIST.md` |
| H6 | App Store Connect forms (below) | `docs/ios/app-store-listing.md` |
| H7 | A monitored support address exists and is the one in the listing | platform [support-inbox](https://github.com/Algorythmos-AI/sggs-platform/blob/main/docs/process/runbooks/support-inbox.md) |
| H8 | Rulesets applied so the iOS `parity` + `app` checks are required | `scripts/gh/apply_rulesets.sh` |
| H9 | Submit for review; after approval, **Release** manually (7-day phased release applies from the first *update*) | App Store Connect |
### H5 — hardware pass (signed RC build, real devices)
- [ ] Oldest supported iPhone on iOS 17.x: **first** launch after install — note the seconds spent on "Verifying scripture integrity…" (budget: under 5 s; the app stays responsive throughout).
- [ ] Turn a Nitnem reminder on → the system permission prompt appears → at the chosen time a **banner with sound** arrives; tapping it opens Nitnem.
- [ ] Live Activity: More → turn it on → open a bani, stay 20 s, lock the device → progress shows; tapping the Lock Screen banner returns to that bani; it ends when the bani is left.
- [ ] Reader: scroll down an Ang, swipe to the next, swipe back — is the scroll position kept? (Record the answer; the pager's neighbour handling is untouched pending this check.)
- [ ] Change the accent in Themes, return to the Reader — the open Ang uses the new accent at once.
- [ ] iPad: portrait + landscape, Split View ⅓ and ½, Stage Manager resize.
- [ ] `A11Y_CHECKLIST.md`: VoiceOver, largest accessibility text size, Reduce Motion, Increase Contrast, Bold Text.
- [ ] Widgets (all three) populate after the first app launch; More → About → Integrity shows "Widget App Group available".
- [ ] Airplane Mode for the whole session: nothing degrades.
- [ ] Storage almost full: the app launches and reads; saving a verse fails gracefully.
### H6 — App Store Connect
Paste from `make asc-sheet` (every field in console order, exactly as the reviewed listing doc says,
markdown stripped). When done, `make asc-diff` (with the API key env from CLAUDE.local / the
TestFlight command) reads the console — GET only, it never writes — and must print
`RESULT: NO DRIFT`. It does not see App Privacy, DSA, Mac/Vision Pro, agreements or the age-rating
answers; tick those by hand.
- [ ] Name available; subtitle, promo text, keywords, description pasted from the listing doc.
- [ ] Support / Marketing / Privacy URLs = the **"Submit this"** column (they must resolve today).
- [ ] App Privacy: **Data Not Collected**.
- [ ] Age rating questionnaire → 4+. Read the live form; answer what it asks.
- [ ] **New app record / version**: create the App Store version with the string **exactly matching
the binary's `MARKETING_VERSION`** (a new app record defaults to 1.0; the build is not
selectable until the version string matches). **Submit the build whose version equals the live
site** (`/api/meta.version` on `gurbanisoul.com`) — one number everywhere. Confirm with
`python3 scripts/release/check_release_complete.py X.Y.Z` in the **sggs-platform** repository
(the script lives there) before selecting. Any earlier
superseded upload (e.g. a prior patch) is left unsubmitted; Apple expires it. Select the
processed build once it leaves "Processing".
- [ ] **App Information**: Primary category Reference, Secondary Education; Content Rights.
- [ ] Content rights: **Yes, contains third-party content, and I have the rights** (list from the doc).
- [ ] Export compliance: answered by `ITSAppUsesNonExemptEncryption = false`.
- [ ] EU DSA trader status declared and verified (the contact details become public).
- [ ] Accessibility labels: declare only what H5 proved.
- [ ] Availability: all territories. Price: free. Agreements: nothing pending under Business.
- [ ] **Untick "Make this app available on Mac" and "… on Apple Vision Pro"** — both are ON by
default. The app is iPhone/iPad only (`TARGETED_DEVICE_FAMILY "1,2"`) and is never tested on
those platforms; a broken Mac/visionOS run is a 2.1 rejection on a surface we never shipped.
- [ ] App Review Information: contact name, **phone**, email; review notes pasted; no sign-in.
- [ ] Screenshots retaken from the RC (`ios/AppStore/README.md` checklist). Mandatory display sizes
today: **iPhone 6.9"** and — because the app supports iPad — **iPad 13"**; 6.5" iPhone is
optional. **Confirm the live requirement in the upload UI on the day** (Apple changes it).
- [ ] **An update**: App Store Connect will not create version X.Y.Z while the previous version is
still "Pending Developer Release" — release (or cancel) that one first. Paste **What's New**
from the sheet; the field exists from the first update on.
- [ ] Version release: **manual**. Phased release: **on for every update**; Apple offers it only for
updates, so a first version goes to everyone at once when you press Release. `make asc-diff`
reads both back (`Version · What's New`, `Version · Phased release`).
- [ ] After approval the version sits at **"Pending Developer Release"** — it is NOT live. Open the
version page and press **Release** (for an update this starts the 7-day phased rollout);
record the timestamp.
## 3. Re-check on submission day
`make asc-diff` again (someone may have edited the console since), then:
Apple changes these without notice — read :
minimum Xcode/SDK (and raise `MIN_XCODE_MAJOR` in `ios/tools/testflight_archive.sh` +
`MIN_SDK_MAJOR` in `ios/tools/appstore_preflight.py` together), the age-rating questions, whether
accessibility labels have become mandatory.
## 4. If App Review rejects
Read the cited guideline, reply in Resolution Center with the specific evidence (the review notes
already explain offline use, location, notifications and the Live Activity). A metadata-only
rejection is fixed in App Store Connect and in `docs/ios/app-store-listing.md` **in the same sitting**
— the doc and the listing must never diverge. A binary rejection that needs **no source change**
(signing, a processing fault) is rebuilt from the same tag with the next build number. One that
needs a source change cannot reuse the version: the tag `vX.Y.Z` is immutable and an App Store build
must be at it, so the fix ships as `X.Y.(Z+1)` on the platform and here (`ios-hotfix.md`).
## 5. After release — halt criteria (decided in advance)
For an **update**, pause the phased release (App Store Connect → the version → Pause Phased Release)
if any of the criteria below holds. A **first version** has no phased release: its levers are the
timing of the manual Release (hold it while anything is in doubt) and, once live, a fixed build with
an expedited review ([ios-hotfix](https://docs.gurbanisoul.com/ios/process/runbooks/ios-hotfix/) step 2). The criteria:
- an integrity-gate false positive in the field ("Scripture integrity check failed" on a clean install);
- a wrong-text report **confirmed against the printed Bir** by a person;
- a crash or hang reported by more than one user on the same screen, or an Organizer crash rate
that is clearly above the previous build's.
Then follow [ios-hotfix](https://docs.gurbanisoul.com/ios/process/runbooks/ios-hotfix/). Weekly during the rollout: Xcode Organizer crashes and
hangs → symbolicate with the dSYM UUIDs recorded in the candidate JSON → issue → decide.