storebound/docs/ship-step.md

130 lines
7.1 KiB
Markdown

# Ship step contract
How a release pipeline (a Jenkins node, a Forgejo runner, any CI job) calls storebound without a person. The
commands below keep their normal behaviour in a terminal; the contract switches on when a run passes `--json`,
`--result-file`, `--progress json`, `--env-file` or `--version-from`. The code is in one module:
`cli/lib/ship-step.ts`.
| Command | step | store |
|---|---|---|
| `storebound play build` | `build` | `play` |
| `storebound play upload` (`--track internal\|alpha\|beta\|production`) | `upload` | `play` |
| `storebound play release` (promote / several tracks in one edit) | `release` | `play` |
| `storebound apple build` | `build` | `apple` |
| `storebound apple upload` | `upload` | `apple` |
| `storebound apple submit` | `submit` | `apple` |
| `storebound apple resubmit` | `resubmit` | `apple` |
A write still needs `--yes` in a pipeline: without it the confirmation is declined and the run ends with exit 3
(`CONFIRMATION_REQUIRED`). `apple submit` without `--yes` is a dry run (exit 0, `state: "planned"`).
## Exit codes
| Code | Meaning | `error.code` |
|---|---|---|
| 0 | ok | - |
| 1 | failure (build failed, network, timeout, blockers) | `FAILED`, `TIMEOUT`, `BLOCKERS` |
| 2 | usage (unknown flag, bad value, bad `--version-from` / `--env-file`) | `USAGE`, `ENV_FILE` |
| 3 | needs the owner | `AGREEMENT_PENDING`, `SIGN_IN_REQUIRED`, `COMPLIANCE_ANSWER_MISSING`, `CONSOLE_STEP`, `CONFIRMATION_REQUIRED` |
| 4 | the store refused it (4xx answer, ITMS error, invalid build, rejected upload) | `STORE_REFUSED`, `BUILD_INVALID` |
| 7 | nothing to do: the build / release is on the store already (the result names what is there) | - |
An exit 3 carries `error.consoleUrl`: the absolute console deep link of the page where the owner acts (App Store
Connect Business > Agreements, Users and Access > Integrations, the encryption / App Privacy / age rating pages; Play
Console API access, Data safety, Content ratings, Publishing overview). The links come from
`cli/lib/console-links.ts`, with the app's console ids when the config knows them.
Note: `apple upload` and `apple testflight wait` keep their old exit 7 for a processing timeout when the contract is
off; under the contract a timeout is exit 1 `TIMEOUT` and an invalid build exit 4 `BUILD_INVALID`.
## `--json`: one final line
stdout carries exactly one line, at the end, also on failure:
```json
{"ok":true,"step":"upload","store":"play","track":"production","version":"1.2.0","build":"42","state":"completed","url":"https://play.google.com/console/u/0/developers/<dev>/app/<app>/tracks/production","warnings":[]}
```
| Key | |
|---|---|
| `ok` | true for exit 0 and 7 |
| `step`, `store` | see the table above |
| `track` | Play track(s) (`internal`, `production`, `internal,production`); null on Apple |
| `version`, `build` | marketing version and build number / versionCode, as strings |
| `state` | `built`, `planned` (dry run), Play release status (`completed`, `draft`, `inProgress`), App Store `processingState` (`VALID`, `PROCESSING`) or version state (`WAITING_FOR_REVIEW`), `failed` |
| `url` | the console page to look at |
| `warnings[]` | e.g. a versionCode that is on Play but on another track |
| `error` | `{code, message, consoleUrl?}` when not ok |
| `artifact` | build steps: the AAB / IPA path |
The command's own `--json` fields follow in the same object when they do not clash (`versionCode`, `sha256`,
`tracks`, `aab`, `processingState` ...), so readers of the old output keep working. Every log line goes to stderr.
Under `storebound serve` and flows (`STOREBOUND_MACHINE=1`) a bare `--json` keeps the old result line.
## `--result-file <path>`
The same JSON, written atomically (a temp file in the same folder, then a rename), on success and on failure. Also
written for an unknown flag (exit 2).
## `--progress json`
One JSON line on stderr per phase: `{"phase":"upload","pct":40,"message":"4/10 parts"}`. Phases: `start`,
`prebuild`, `build`, `sign`, `verify`, `upload` (with `pct`), `commit`, `processing`, `submit`, `done`. Other stderr
lines are plain logs: read the lines that start with `{`.
## `--env-file <path>`
`KEY=VALUE` lines (`#` comments, `export ` prefix, quotes; `\n` inside double quotes) loaded into the environment
before the credentials resolve; the file wins over the environment. On macOS / Linux the file must not be readable by
group or others (mode 600 or stricter), else exit 2. On Windows it is read as is. Every value of the file, and every
environment variable whose name says secret (`*_KEY`, `*PASSWORD*`, `*TOKEN*`, `ASC_P8`, `PLAY_SERVICE_ACCOUNT` ...),
is replaced with `[redacted]` in stdout, stderr and the result file (multi-line keys line by line).
## `--version-from <version.json>`
Reads the version and build for the step. Accepted shapes:
```json
{"version": "1.2.0", "build": 42}
{"versionName": "1.2.0", "versionCode": 42}
{"marketing": "1.2.0", "build": "42"}
```
(`marketingVersion` and `buildNumber` work too.) What it sets: `play build` `--version-name` + `--version-code`,
`play upload` `--version-code` (picks the AAB with that versionCode), `play release` `--version-code` (without an
.aab), `apple build` `--version` + `--build-number`, `apple upload` `--build`, `apple submit` / `resubmit` `--version`
+ `--build`. A flag given by hand must agree with the file (else exit 2). An Android build must be an integer.
## Idempotency
- `play upload`: a versionCode already in Play's bundle list is never uploaded again: exit 7 with the track and
status it has (a warning when it is not on the asked track: `play release --version-code N --<track> <status>`).
- `apple upload`: a build number App Store Connect already has (a build, or a finished build upload) is exit 7 with
its `processingState`; nothing is sent.
- `play release`: every track already holds the same release: exit 7, nothing committed.
- `apple submit`: the version is already submitted (or further) with this build, or the submission is in review:
exit 7.
## `storebound status`
```
storebound status --app <name|store id|folder> --json
```
Read-only, before any step: per platform the release reaching users (`live`) and the one above it (`next`), each with
`version`, `build`, `track`, `state`, `phase`, `reviewState` and `rolloutPct`. `--app` takes an app folder path, a
workspace app name or a store id (bundle id, package name, App Store app id). One JSON line:
```json
{"apps":[{"app":"notes","folder":"/srv/notes","platforms":[{"platform":"android","store":"play","live":{"version":"1.2.0","build":"42","track":"production","state":"inProgress","phase":"rollout","reviewState":null,"rolloutPct":20},"next":null}],"errors":[],"warnings":[]}],"at":"2026-10-23T10:00:00.000Z"}
```
Exit 1 when a store could not be read (`platforms[].error`).
## A pipeline stage
```sh
storebound play build --version-from version.json --env-file /run/secrets/ship.env --json --result-file out/build.json --progress json --yes
storebound play upload --track production --version-from version.json --env-file /run/secrets/ship.env --json --result-file out/upload.json --yes
case $? in 0|7) ;; 3) echo "owner: $(jq -r .error.consoleUrl out/upload.json)"; exit 1 ;; *) exit 1 ;; esac
```