storebound/docs/studios.md
CRT d2ce185441 One product per machine: the CLI and the desktop app share one version, the store accounts and the app folder list
- one version: package.json's version is the desktop app's too (desktopVersion removed); tools/desktop-version.ts
  keeps the plugin manifests and the app's copies equal, desktop-release publish refuses another version, release
  order app first then npm publish; the unpublished desktop 0.1.0 candidate is noted in the docs and CHANGELOG
- storebound desktop installs (per user, no admin: macOS DMG -> /Applications or ~/Applications, Windows NSIS /S,
  Linux AppImage + .desktop with the storebound:// handler), updates an older app, notes a newer one (downgrade only
  with --force), asks once ([Y/n], --yes, exit 3 without a terminal), opens on the folder; --dry / --json / --download;
  the install steps are pure plans (cli/lib/desktop-install.ts); the version's own manifest dl/<version>/latest.json
- credentials: one store, the CLI's credential profiles in ~/.storebound/credentials; the desktop writes and reads
  them (profiles.rs, files 0600), moves old keychain accounts into profiles on first start, shows profiles the CLI
  made; precedence env > named profile > project file > default profile; config `account`, "account" in the
  project's credentials.json; storebound accounts list | import (dedup by key id / service account e-mail, key files
  copied 0600, project file keeps keystore and review contact, backup, --dry, never prints values) | use
- app folders: one list ~/.storebound/projects.json shared by the desktop (registry.rs), storebound desktop,
  apps folders | add | remove, ws init and ws without a workspace file
- desktop: notice when a folder pins another storebound version; storebound-desktop --version
- tests: version drift, desktop decisions and per-OS plans, credential precedence and import, registry (TS + Rust),
  keychain migration and profile round trip (Rust), boot check with old keychain state and the new shared state
2026-10-13 06:17:37 +08:00

8.4 KiB

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:

{
  "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:<store app key> and account:<profile>, 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/<profile>.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_<NAME>_<KEY> (for CI). Any command takes --account <profile>. 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 <profile> 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 <file>) into a workspace file; its accounts name the credential profiles the app and the CLI share (~/.storebound/credentials), so the keys are already there. ws desktop export prints the workspace as a desktop state (--out <file>), and --merge --apply adds it to the desktop's state.json while the app is closed (a backup is kept; nothing is removed). Without a workspace file, ws works over the machine's app folder list (~/.storebound/projects.json, the desktop's App folders).

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 <version> 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):

# 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 <path>, ${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/<name>.json), --from <step> 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