# Many apps, studios, release trains and workflows ## Many apps and studios A studio with twenty games, or a developer with apps under two store accounts, works from one folder above the app folders. `storebound ws init` writes `storebound.workspace.json`: ```json { "apps": ["games/*", { "path": "apps/notes", "apple": "solo-apple" }], "studios": [ { "name": "Acme Games", "color": "indigo", "apps": ["games/*"], "accounts": [{ "store": "apple", "profile": "acme-apple" }, { "store": "play", "profile": "acme-play" }] } ], "accounts": [{ "store": "apple", "profile": "solo-apple", "label": "Solo" }], "ignore": ["games/old-*"], "overrides": { "pairs": [], "unpairs": [], "links": [], "appStudio": [] }, "concurrency": 4 } ``` - **apps**: folders or globs (only folders with a `storebound.config.*` count; `node_modules` and dot folders are skipped). An object entry names the folder's own credential profiles. - **studios**: name, colour (the desktop's set), the store accounts and the app folders (globs) that belong to it. - **accounts**: store accounts without a studio. An account is a store and a **credential profile** name, never a key. - **ignore**: folder globs, `app:` and `account:`, as in the desktop's ignore list. - **overrides**: the desktop's pairing corrections (`pairs`, `unpairs`, `links` of a store app to a folder, `appStudio`), with folders named by their path. A `storebound.workspace.ts` with `export default { ... }` works too. Which keys an app uses: its own entry's profiles, else the single account of that store in its studio, else the single account of that store in the workspace, else the folder's own credentials (env or `.storebound/`). **Credential profiles** keep many accounts apart on one machine: `~/.storebound/credentials/.json` with the account names of `.storebound/credentials.json` (`PLAY_SERVICE_ACCOUNT`, `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_P8`, `ASC_VENDOR_NUMBER`; the Android upload keystore belongs to the app and stays in the project; relative key paths are relative to that folder, which `STOREBOUND_CREDENTIALS_DIR` can move), or env `STOREBOUND_PROFILE__` (for CI). Any command takes `--account `. `ws list` shows per account whether its profile has keys; no command prints a key. `ws status`, `ws check`, `ws validate` and `ws run` read the apps a few at a time (`concurrency`, 1-8, or `--concurrency`), each app with its own keys handed to its child process only. `ws run` takes any read command and the MCP tool set; write commands get `--dry` unless `--apply`, and submit, release, rollout, send, start / stop, testers and `--yes` are refused: those stay with the owner in the app folder. `ws apps import --account ` lists the apps a store account sees (as `apps list`) into `.storebound/workspace/` and pairs them with the folders by the desktop's rules (`cli/lib/identity.ts`, the same code): a folder takes the store apps its config names, the remaining iOS and Android apps pair by id, close id or name, `unpairs` keep two apart, and the rest are store-only apps that `ws status` reads too. **The desktop app** keeps the same model (studios, accounts, folders, overrides). `ws desktop import` turns its `state.json` (or `--state `) into a workspace file and lists the credential profiles to create: the desktop's keychain is never read. `ws desktop export` prints the workspace as a desktop state (`--out `), and `--merge --apply` adds it to the desktop's `state.json` while the app is closed (a backup is kept; nothing is removed; new accounts get their keys in the desktop's Accounts screen). MCP: `workspace_list` (offline) and `workspace_status` (read-only). ## Release trains One version, both stores, one track: **build > testing > review > live > rollout**. The CLI and the desktop's Releases tab share the code (`cli/lib/train.ts`, `cli/lib/portfolio.ts`), so both show the same stage for the same store state. ``` $ storebound release status app version train App Store Google Play ------ ------- ------- ---------------------- ----------------------------- Puzzle 2.3.0 current rollout 20% (build 45) rollout 10% (build 230) Puzzle 2.4.0 next build testing (build 240, internal) ``` `release plan ` lists what is left for that version on each store in fix order: config problems (`check`), the version on the store (how to create it when it is missing), store blockers (`validate`, while nothing is submitted), the next move and the warnings. Owner steps (submit for review, promote to production, release an approved version, raise or complete a rollout) are named with where the owner does them; storebound does not do them. A store it cannot read is "unknown", never "not there". Both commands work in an app folder (that app) and in a workspace (every app, or `--app a,b`); `--json` gives the structured result. MCP: `release_status`, `release_plan` (read-only). ## Repo-local workflows Your release routine as a file in the repo, run step by step. `storebound init` writes two to start from (`store/workflows/release.yml`, `beta.yml`; app and game versions): ```yaml # store/workflows/release.yml (app profile, shortened) params: VERSION: "1.0.0" steps: - id: next-build if: apple run: apple next-build capture: BUILD - id: build if: apple run: apple build --version ${VERSION} --build-number ${BUILD} - id: upload if: apple run: apple upload --no-wait # wait, play-build, play-upload, whats-new, listing, validate, scan, plan ... - id: submit owner: "App Store Connect: Submit for Review (version ${VERSION}, build ${BUILD}) ..." ``` The plan of that file (`--dry`: nothing runs; without it the reads run and `${BUILD}` is filled in): ``` $ storebound flow run release VERSION=1.2.0 --dry [flow] 1/12 next-build: storebound apple next-build (plan only) [flow] 2/12 build: storebound apple build --version 1.2.0 --build-number ${BUILD} --dry (plan only) [flow] 3/12 upload: storebound apple upload --no-wait --dry (plan only) [flow] 4/12 wait: storebound apple testflight wait --build ${BUILD} --version 1.2.0 --dry (plan only) [flow] 5/12 play-build: storebound play build --version-name 1.2.0 --dry (plan only) [flow] 6/12 play-upload: storebound play upload --track internal --dry (plan only) [flow] 7/12 whats-new: storebound whats-new apply (if exists store/whats-new: false) [flow] 8/12 listing: storebound apple listing --version 1.2.0 --build ${BUILD} --dry (plan only) [flow] 9/12 validate: storebound validate (plan only) [flow] 10/12 scan: storebound scan (plan only) [flow] 11/12 plan: storebound release plan 1.2.0 (plan only) [flow] 12/12 submit: YOUR CLICK App Store Connect: Submit for Review (version 1.2.0, build ${BUILD}). Play Console: promote the internal release and Send changes for review ``` - Read steps (check, validate, scan, release plan, next-build, status) run; **write steps run with `--dry`** unless `--apply`, and even then each command asks its own confirmation; wait steps check once; `sh:` steps (your own build commands) run only with `--apply`. `--dry` prints the plan without running anything. `--apply --yes` is your approval of every write step of the run (no terminal needed); without it, a step whose confirmation cannot be answered stops the flow as "declined", never "ok". - Owner steps (submit, release, phased release, rollout, review replies, price imports, tester invitations, Beta App Review) are never executed, also when written as `run:`; the flow ends with the list of your clicks. - `capture:` turns a step's last output line into a variable (`${BUILD}`), `if:` skips a step (`apple`, `play`, `exists `, `${VAR} == x`), `flow:` reuses another flow (`private: true` sub-flows), `before` / `after` / `onError` are hooks, `continueOnError` keeps going. - A failed step stops the flow; `--resume` starts again at that step with the captured variables (`.storebound/flows/.json`), `--from ` anywhere. `flow validate` checks commands, variables, duplicate ids and sub-flow cycles. Several flows can also live in `storebound.workflows.yml|json|ts`. - MCP: `flow_list` and `flow_run` (the dry mode only; `--apply` stays a terminal command). Skill: `release-flows`. --- [All docs](README.md) ยท [README](../README.md)