CLI Reference¶
Global options¶
| Option | Environment | Meaning |
|---|---|---|
-c, --config <PATH> |
SHIP_CONFIG |
Config path. Default .github/ship.yml. |
-R, --repo <OWNER/REPO> |
SHIP_REPO |
Target repository. Defaults to the current one. |
-v, --verbose |
Show error cause chains. |
Choosing the release line¶
preview, prepare, release and status accept:
| Option | Environment | Meaning |
|---|---|---|
--base <BRANCH> |
SHIP_BASE_BRANCH |
Base branch to release from. |
With release lines configured, this selects the line. Without them it simply overrides the branch the Release PR targets.
gh-ship works the branch out for itself, in order:
--base <BRANCH>, orSHIP_BASE_BRANCH.- The GitHub Actions environment — the branch a
pull_requesttargets, otherwise the branch of the run. A run on a tag names no branch and is not guessed at. - The local checkout's current branch, read from
.git/HEAD. Nogitbinary is needed and nothing is fetched or cloned. - The repository's default branch.
Detection only applies when branches is configured; otherwise the repository
default branch is used, exactly as before. gh ship status always reports which
source it used.
run also accepts --base, but takes its branch from the triggering event.
An explicit base must agree with that branch; it cannot redirect an event to
another release line.
Waiting¶
run, preview, prepare and release all block on a workflow run they dispatched,
unless run, prepare or release is given --no-wait (prepare,
release). Two environment
variables cap how long they wait:
| Variable | Default | Meaning |
|---|---|---|
SHIP_APPEAR_TIMEOUT |
90 |
Seconds to wait for a dispatched run to appear. |
SHIP_RUN_TIMEOUT |
3600 |
Seconds to wait for a run to finish. |
Both take a whole number of seconds. The appear timeout only covers the gap between dispatching and GitHub queueing the run, so it rarely needs touching.
Raising SHIP_RUN_TIMEOUT past an hour only makes sense with a PAT, or with a
GitHub App whose credentials are given to gh-ship
(see Authentication), which then replaces its own token. With
an installation token minted by a workflow step, a publish workflow that takes
longer than about 50 minutes cannot be waited on with a single token. Use
gh ship release --no-wait instead, or
retry with a fresh token.
When a command runs inside a workflow of your own, set an explicit job
timeout-minutes. For graceful timeout handling and retries, each gh-ship wait
must expire before the remaining job budget, with headroom for queueing,
token refresh, and subsequent attempts. The retry examples in
Workflows budget for this.
A shorter job timeout is a hard cancellation instead: GitHub may terminate
the command before it returns exit 75, so its retry logic cannot run.
The SHIP_RUN_TIMEOUT default of 3600 s is exactly the lifetime of a GitHub App
installation token. Inside GitHub Actions, gh-ship warns when the timeout is at
or above that, since a wait that long outlives the token. Lower it, and when it
expires the command exits with code 75, which a workflow can
retry with a fresh token. The warning does not appear when gh-ship mints its own
token, since that one is replaced before it expires.
gh ship run¶
An automatic entry point for a single-job Ship workflow.
It reads GITHUB_EVENT_NAME and the JSON payload at GITHUB_EVENT_PATH, with
GITHUB_ACTIONS=true:
| Event | Behavior |
|---|---|
| Branch push | Prepare when the branch matches a configured release line, or the repository default branch without branches. |
| Closed, merged Release PR | Release that exact PR when its head and base match the configured line and both belong to the target repository. |
| Other events or unrelated PRs | Skip successfully, reporting why. |
Tag pushes, branch deletions, and unconfigured branches also skip. PR identity is checked against GitHub, including when the payload omits its PR object but retains its number. A newer open Release PR cannot replace the triggering PR. Labels are not used for detection, and this command never merges a PR.
--no-wait is passed to the selected phase: prepare leaves its staging workflow
running; release leaves publication pending. Re-running release can adopt its
publish run. Re-running prepare starts a new preparation and sweeps the old
staging ref, so allow enough time for one complete preparation.
Manual dispatch and workflow_run events are not automatically
routed; call prepare or release explicitly for those workflows.
Outside Actions, or when a supported event's payload is missing or malformed,
the command fails with a diagnostic. Use gh ship prepare and gh ship release
locally. --base / SHIP_BASE_BRANCH must match the event branch.
Authentication¶
Authentication is gh's job: gh auth login locally, GH_TOKEN in CI. gh-ship
can instead act as a GitHub App, given its credentials:
| Variable | Meaning |
|---|---|
SHIP_APP_CLIENT_ID |
The App's Client ID (or numeric App ID). |
SHIP_APP_PRIVATE_KEY |
The contents of the App's private key, in PEM format. |
SHIP_APP_INSTALLATION_ID |
Optional. The installation to use, skipping the lookup. |
gh-ship then mints an installation token, scoped to the repository, sets it as
GH_TOKEN on every gh it runs, and replaces it before its one hour is up. Set
both of the first two or neither. See
Letting gh-ship mint the token.
Transient failures¶
GitHub occasionally answers a valid query with a 502 or 504 — most often on the
GraphQL endpoint behind the --json flags — and is fine again seconds later.
gh-ship retries those rather than failing the release:
| Variable | Default | Meaning |
|---|---|---|
SHIP_GH_RETRIES |
3 |
Extra attempts for a read-only gh call that fails transiently. 0 disables. |
SHIP_GH_RETRY_DELAY |
1 |
Seconds before the first retry. Doubles per attempt, capped at 8. |
Diagnostics¶
| Variable | Meaning |
|---|---|
RUST_BACKTRACE |
When set to anything, prints the full cause chain behind a diagnostic — the same as passing -v. |
Only read-only calls are retried — list, view, status, download,
diff, checks and GET requests to gh api. A gateway timeout means GitHub
stopped answering,
not that it stopped acting: a timed-out pr create may well have created the
pull request, so retrying it would open a second one. Writes still fail on the
first error, and are safe to re-run by hand because every gh-ship command is
idempotent.
Rate limits are not retried either. They clear in minutes rather than seconds, so the error tells you to wait instead of spending the budget for nothing.
Colour¶
gh-ship colours its output when it believes something will render it, following
the same variables as gh itself — there is no --color flag.
| Variable | Effect |
|---|---|
NO_COLOR |
Set to anything non-empty, colour is off. Wins over everything else. |
CLICOLOR=0 |
Colour off. |
CLICOLOR_FORCE |
Set to anything but 0, colour on even without a terminal. |
TERM=dumb |
Colour off, unless CLICOLOR_FORCE overrides it. |
Otherwise gh-ship colours its output when stderr is a terminal, or when
GITHUB_ACTIONS=true. Actions gives every process a pipe rather than a
terminal but renders ANSI in its logs, so workflow output is coloured without
any configuration.
Only the human-readable output on stderr is ever coloured. Stdout carries the
machine-readable payloads — --json, and the rendered PR body from
gh ship preview — and is never coloured, so it stays safe to pipe.
Exit codes¶
| Code | Meaning |
|---|---|
0 |
Success. Including changed: false. |
1 |
Failure. |
75 |
Timed out waiting for a run (SHIP_RUN_TIMEOUT) that is still in flight. release can adopt it on retry; prepare and preview restart work. |
Code 75 is EX_TEMPFAIL from sysexits.h. It is distinct from 1 so a
workflow can retry on this case alone — for instance with a fresh token —
without treating a real failure, such as a failed or cancelled run, as
retryable. A run that never appeared (SHIP_APPEAR_TIMEOUT) is not
reported as a completion timeout and still exits 1.
nothing to release is not a failure
A scheduled release job that finds nothing to ship exits 0. Treating it as a
failure would make every quiet week look broken.
gh ship init¶
Make a repository gh-ship enabled.
Scans .github/workflows/, lists the workflows gh-ship can dispatch, explains the
ones it cannot, offers to generate templates, and writes a documented
.github/ship.yml.
| Option | Meaning |
|---|---|
--force |
Overwrite an existing configuration. |
Requires an interactive terminal. In automation, write the config directly.
init is entirely local: it never contacts GitHub, so it needs no authentication
and ignores --repo.
gh ship validate¶
Check a release artifact, or the setup.
With a file — validates it against the artifact schema. Requires no network, no repository, and no GitHub authentication.
Without a file — validates .github/ship.yml and checks that the workflows it
names satisfy the contract. Requires no network and no GitHub
authentication either.
$ gh ship validate ship.release.json
✔ ship.release.json is a valid release artifact
version: 1.4.0
tag: v1.4.0
gh ship preview¶
Dry-run the prepare workflow and render the Release PR.
Dispatches the prepare workflow with dry_run: true, waits, downloads the artifact,
and renders the PR: the body on stdout, the title, labels and rules on stderr. So
gh ship preview > body.md captures exactly the PR body.
Nothing on GitHub is modified: no branch, no PR, no tag, no release.
| Option | Meaning |
|---|---|
--json |
Emit the artifact and rendered PR as JSON. |
gh ship prepare¶
Run the prepare workflow and open or update the Release PR.
- Stops if a merged Release PR is still awaiting
gh ship release. - Sweeps staging branches left behind by earlier runs.
- Cuts a throwaway staging branch,
ship/prepare-<token>, from the base branch (workflow_dispatchneeds the ref to exist). With release lines configured the name carries a SHA-256 identity of the full base name —ship/prepare-<hash>-<token>. - Dispatches the prepare workflow on that staging branch and waits.
- Downloads and validates the artifact.
- Stops with exit 0 if
changed: false. - Moves the release branch onto the staged release commit, then sweeps again.
- Opens or updates the Release PR, embedding the artifact in its body.
Re-running is safe, and is the supported way to refresh a Release PR: the release branch moves onto the new commit and the existing PR is updated in place.
Run one at a time per release line
Step 2 deletes staging branches, including one a concurrent run is using, so two prepares on the same line must not overlap.
Without branches, the sweep covers ship/prepare-<token> branches:
serialise everything with concurrency: group: ship, as the sample
workflow in Workflows does.
With branches configured the sweep uses a hash of the full base name and
checks the invocation-token format, so
different lines never collide and may prepare concurrently. Key the group
by branch instead — group: ship-${{ github.ref }}.
Why a staging branch
The work used to happen on the release branch, reset to base first. That left it momentarily identical to its base, and GitHub closes a pull request whose head becomes contained in its base — so the Release PR was closed and reopened on every prepare. Staging elsewhere and promoting afterwards means the release branch is never equal to the base.
| Option | Meaning |
|---|---|
--no-wait |
Dispatch and return. The PR is not created; run prepare again later. |
It refuses while a release is pending
If the Release PR has been merged but gh ship release has not run yet,
prepare stops and says so, exiting 0.
In that window the tag does not exist, so your changelog tool still reports the
same version as unreleased — preparing again would start a second release for a
version you already merged. Run gh ship release to finish, and the next
prepare proceeds normally.
Exiting 0 is deliberate: this is an expected state, and a workflow that runs
prepare on every push should not go red until someone ships.
It also skips right after a release
Merging the Release PR is itself a push to your base branch, so a
push-triggered prepare runs on it. If the release is already published and
its merge commit is still the tip of the base branch, there is nothing on
top of it to release: prepare says so and exits 0 without dispatching the
prepare workflow.
The check compares the merge commit recorded on the pull request against the
branch tip, so it holds whether you merge, squash or rebase. The next commit
to land makes prepare proceed as usual.
gh ship status¶
Show where the current release stands.
A pure query. It dispatches nothing, waits for nothing, and changes nothing.
Everything is reconstructed from GitHub — branch, PR, last run, the artifact embedded in the PR body — so the answer is the same on any machine.
| Option | Meaning |
|---|---|
--json |
Emit machine-readable JSON. |
gh ship release¶
Tag, publish, and release the merged Release PR.
- Finds the Release PR and recovers the artifact from its body.
- Requires the PR to be merged (or merges it with
--merge). - Tags the merge commit — never a remembered SHA, because a squash merge creates a new one.
- Creates the GitHub Release as a draft.
- Dispatches the publish workflow so it can attach assets — unless a run of it already succeeded for the tag, in which case it is skipped, or one is still in flight, in which case that run is adopted and waited on.
- Makes the release visible.
The tag is created before the release, deliberately
A draft release does not create its git ref — the tag only appears when the
release is published. Since the publish workflow is dispatched on that tag
and checks it out, gh-ship creates refs/tags/<tag> itself first.
The step is idempotent, so re-running after a failure part-way through is safe.
Re-running is cheap
Before dispatching, gh-ship lists the runs of the publish workflow on the tag. The tag is unique to the release, so any run on it belongs to this release — whoever started it.
- A run that succeeded means the assets are up: gh-ship skips the dispatch and only makes the release visible.
- A run still in flight is adopted and waited on, rather than raced.
- Only when every run failed — or none exists — is a new one dispatched.
So if a publish run fails and you re-run it from the GitHub UI, re-running
the job that calls gh ship release finishes the release instead of
rebuilding it.
| Option | Meaning |
|---|---|
--merge |
Merge the Release PR if it is still open. |
--no-wait |
Dispatch the publish workflow but do not wait for it. The release is left as a draft. |
Idempotent: if the release already exists, it is not recreated.
--no-wait: don't hold a token for the whole build
With --no-wait, release tags, creates the draft release, dispatches the
publish workflow and returns. The release stays a draft, because its assets
are not attached yet.
Running gh ship release again once the publish run has succeeded finds
that run on the tag, skips the dispatch, and only makes the release visible.
A run still in flight is reported, not waited on; a run that already
succeeded is honoured even with --no-wait.
Without --no-wait, a wait that outlasts SHIP_RUN_TIMEOUT exits with
code 75, which a workflow can retry with a fresh token.
That keeps a runner idle for the whole build; --no-wait does not.
This decouples build time from the one-hour lifetime of a GitHub App token. See Long publish builds.
gh ship sign¶
Re-create a commit so GitHub signs it.
Meant for the prepare workflow, not for you: it re-creates the tip commit of
BRANCH through the API, with the same tree, parents and message but no identity
of its own, and moves the branch onto the result. GitHub signs a commit it creates
for a bot, so the commit comes out Verified where git commit cannot.
BRANCH defaults to the branch the workflow was dispatched on — the staging
branch — read from GITHUB_REF, or from the checkout outside CI.
Two behaviours worth knowing:
- A commit whose signature is already verified by GitHub is left alone and the command succeeds. The check uses GitHub's verification status, not merely the presence of a signature; an unverified signature may be replaced.
- When GitHub returns the re-created commit unsigned, the command fails and the branch is not moved. That happens when the token is not a bot — a PAT or a user login cannot produce a signature, whatever its permissions — and moving the branch anyway would change the commit's author for no benefit.
Needs contents: write. See Getting a verified release
commit for where it fits in a
workflow, and for the alternatives.
Why this is not an option on prepare
Signing depends on who is authenticated, and only a bot gets a signature.
gh ship prepare is supported from a laptop as much as from CI, so a
sign: setting there would either sign only sometimes or stop prepare
working outside CI. The prepare workflow's token is already a bot, so that
is where the signing belongs.