Skip to content

CLI Reference

gh ship <COMMAND>

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:

  1. --base <BRANCH>, or SHIP_BASE_BRANCH.
  2. The GitHub Actions environment — the branch a pull_request targets, otherwise the branch of the run. A run on a tag names no branch and is not guessed at.
  3. The local checkout's current branch, read from .git/HEAD. No git binary is needed and nothing is fetched or cloned.
  4. 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

gh ship run [--base <BRANCH>] [--no-wait]

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.

$ gh ship init [--force]

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.

$ gh ship validate [FILE]

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.

$ gh ship preview [--json]

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.

$ gh ship prepare [--no-wait]
  1. Stops if a merged Release PR is still awaiting gh ship release.
  2. Sweeps staging branches left behind by earlier runs.
  3. Cuts a throwaway staging branch, ship/prepare-<token>, from the base branch (workflow_dispatch needs the ref to exist). With release lines configured the name carries a SHA-256 identity of the full base name — ship/prepare-<hash>-<token>.
  4. Dispatches the prepare workflow on that staging branch and waits.
  5. Downloads and validates the artifact.
  6. Stops with exit 0 if changed: false.
  7. Moves the release branch onto the staged release commit, then sweeps again.
  8. 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.

$ gh ship status [--json]

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.

$ gh ship release [--merge] [--no-wait]
  1. Finds the Release PR and recovers the artifact from its body.
  2. Requires the PR to be merged (or merges it with --merge).
  3. Tags the merge commit — never a remembered SHA, because a squash merge creates a new one.
  4. Creates the GitHub Release as a draft.
  5. 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.
  6. 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.

$ gh ship sign [BRANCH]

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.

$ gh ship sign
▶ signing ship/prepare-8f2c1a9e4b07 at c0ffee0
✔ signed as 51c8ed0

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.