Context
The Academorix launch (2026-10-22 to 10-24) lost a day to store and secret work that was data, not code: no App Store Connect API key with the Admin role, so the iOS build went through Transporter by hand; iPad and Play screenshot sets re-exported from App Store Connect because the designed frames were not in the repository; the review demo account half-seeded on production; privacy and Data safety answers typed from memory; and credentials pasted into chat. On 2026-10-29 the owner asked for launches without that manual work: a few keys created once, every other step done through the platform APIs by agents, an owner approval only where a step is irreversible, and still nobody reading a secret value.Decision
- A root kit, created once by the owner. Four credentials, each delivered through
~/Secrets-inboxandpnpm secrets:importinto Infisicalprod:/ci/store(except the Infisical identity, which is an id, not a secret): an App Store Connect API key with the Admin role (ASC_API_KEY_ID,ASC_API_ISSUER_ID,ASC_API_PRIVATE_KEY); a Google Play Console organisation account with a service account granted API access (GOOGLE_PLAY_SERVICE_ACCOUNT_JSON); an EAS robot token (EXPO_TOKEN); and an Infisical machine identity that trusts only each product repository’sproductionenvironment over GitHub OIDC and reads/ci/store(its id is passed to the workflow as a repository variable). Nothing else in a launch needs a new owner credential. - Store presence is data in the product repository (0441
decisions 1 to 3). The app’s identity (bundle id, package, name per locale) and capabilities come from the root
runway.yamlmobile.<app>entry; its listing from theassets/store/<app>/<locale>/folder that entry names (name.txt,subtitle.txt,description.md,keywords.txt,privacy.yaml, and optionalshortDescription.txt,promotionalText.txt,whatsNew.txt); app-level facts fromassets/store/<app>/store.yaml(the App Store Connect app id once the record exists, urls, review contact and notes, the demo account, seed and probe as secret NAMES, TestFlight groups, app groups).privacy.yamlholds one row per data type and source, naming a@stackra/consentcategory, and is the same in every locale. Screenshots are captured in CI from the app built from its template line (0441 decision 7) and never committed.pnpm store build --root <product> --app <key>checks all of it and generates every store screenshot size, the App Store App Privacy answers, the Play Data safety answers and a checklist into the gitignored.stackra/store/<app>/. An analytics or crash SDK in the product’s or the template checkout’s dependencies with no row naming it fails the build, so privacy answers travel with the feature. Core’sapps/mobileis a reference app and has no store data. - API first. The releaser drives the App Store Connect API (bundle ids, capabilities, versions, metadata, review
details with the demo account, screenshots, TestFlight groups and beta review, submit for review), the Google Play
Developer API (one edit per run: listings, screenshots, tracks, validate, commit) and EAS (build and submit). The
clients are
tools/scripts/store-asc.mjsandstore-play.mjs, with no SDK dependency: requests are signed withnode:crypto(ES256 for Apple, RS256 for Google). - Browser only where no API exists. Creating the first App Store Connect app record, creating app group identifiers, the App Privacy form, creating the Play app and its first content-rating, target-audience and Data safety forms. These are done with the owner signed in, from the generated checklist and answers. No agent types a password, and no agent signs in.
- Dry run by default; real calls only from CI. Every external call goes through
store-http.mjs, whose default mode sends nothing and prints the planned calls;--replayserves recorded fixtures (the tests);--liveand--recordrun only inside GitHub Actions. Nothing calls Apple, Google or Expo for real until the owner has created the root kit and approved the first live run. - One owner approval per irreversible step.
.github/workflows/runway-store.yml, Runway’s reusablestoreprovider, runs each launch step from the product’s main, building in Runway’s checkout of the app’s template line. A live run of any step runs in the product’sproductionenvironment, which accepts only the protected main branch: the store and signing keys are production secrets, and production secrets live only there, never repository-wide (owner decision 2026-10-29). A dry run reads no secret and takes no environment. The CLI refuses a livesubmit(App Review),promote(Play production) orseed(a production write) outsideproduction, andpromoteis the owner’s own dispatch: no agent starts it (the promote-guard hook). Production deploys keep their own approval under 0170. - The review demo account is a release check.
pnpm store review seedcalls the app’s seed endpoint andpnpm store review probesigns in with the demo account on production and must see data before a submission. - The vault-keeper agent works by name.
pnpm vault preflight --env <e>lists every secret a deploy or store release ofeneeds (the runway CI and member names, and forprodthe root kit and the review secrets) as present, missing or placeholder, by NAME only, and fails on any gap.pnpm vault generate NAME [--rotate]creates or rotates a random machine secret written blind into Infisical through the secrets-import core; production needs the dry run’s one-time token, bound to the operation, as in 0257. Third-party credentials are never generated here: they arrive through the inbox. - Agent Vault stays deferred (0267). Credentials reach the CI job as environment values over OIDC and never the model.
Consequences
- A launch is: the owner creates the root kit once, does the few browser steps from the checklist, approves the
submit in the
productionenvironment and dispatches the promote itself. Every other step is a workflow dispatch an agent can make. - The iPad set, the 9:16 Play set, the privacy answers and the demo account can no longer be forgotten:
store buildand the probe fail first. - Apple and Google change their APIs; the fixtures are in the documented response shapes until the first live run
re-records them (
--record), and a drift shows up as a replay failure, not on launch day. - The App Store Connect key has the Admin role because app management needs it; it lives only in
prod:/ci/store, readable only by jobs in theproductionenvironment. - Data safety and App Privacy stay browser forms until the APIs accept them; the answers are generated, so the form is copying, not deciding.
Enforcement
tools/scripts/__tests__/store.test.mjs: the dry run sends nothing; live and record refuse outside CI; recorded fixtures for the App Store Connect and Play flows;submit,promoteandseedrefuse outsideproduction; no credential or demo password reaches the output;store buildchecks consent categories, SDK rows, text limits and screenshot counts, and renders the exact store sizes.tools/scripts/__tests__/vault-keeper.test.mjs: preflight is names-only; generate and rotate write blind; the production token is bound to the operation..github/workflows/runway-store.yml: a live run takes theproductionenvironment; its protection is applied by the owner (runbook), not checked by CI..agents/agents/releaser.mdand.agents/agents/vault-keeper.md: each agent is allowed only its own commands (pnpm store, the dispatch of the product’s store workflow;pnpm vault).- Planned — a
vault preflightstep in each deploy workflow (STA-1598).
Supersedes and sources
Supersedes: none. Extends 0257; keeps
0267. Sources: owner request 2026-10-29; Academorix launch lessons
2026-10-24 (stackra notes); epic STA-1590, root kit STA-1591, which supersedes the manual store steps of STA-1555, STA-1557 and STA-1558.