7.1 KiB
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:
{"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:
{"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 itsprocessingState; 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:
{"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
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