Ship GitHub releases your way.
A GitHub CLI extension that orchestrates Release PRs and GitHub Actions release workflows.
The GitHub Release Orchestrator.
A GitHub CLI extension that orchestrates the lifecycle of GitHub Releases around workflows you already own.
$ gh ship prepare
▶ preparing acme/widgets
▶ staging on ship/prepare-8f2c1a9e4b07 from main
▶ dispatching prepare-release on ship/prepare-8f2c1a9e4b07
ship id: 8f2c1a9e4b07
run: https://github.com/acme/widgets/actions/runs/42
▶ waiting for prepare-release
✔ prepare-release succeeded
▶ downloading ship-release
✔ artifact is valid
▶ updating release/next to a1b2c3d
▶ opening Release PR
✔ Release PR opened
pr: https://github.com/acme/widgets/pull/7gh-ship orchestrates. Your workflows do the work.
| gh-ship does | your workflow does |
|---|---|
| create the release branch | bump the version |
| dispatch workflows | generate the changelog |
| wait and correlate runs | update files |
| validate the release artifact | commit and push |
| render the Release PR | |
| tag and create the GitHub Release |
- Not a workflow engine. There is no DSL, no step registry, no
run:key. - Not a replacement for GitHub Actions. It dispatches your workflows.
- Not a replacement for Commitizen, git-cliff, cargo-release, semantic-release, Changesets, or anything else. Keep using them.
- It never manages secrets. Authentication is
gh's job. - It never knows how you version.
1.4.0,2026.08.1,banana— all fine. - It never generates changelogs.
$ gh extension install noirbizarre/gh-ship$ gh ship init # under a minute
$ gh ship validate # check the setup
$ gh ship preview # see the Release PR, change nothing
$ gh ship prepare # open the Release PR
# ...review, merge...
$ gh ship release # tag, publish, releaseYour workflow and gh-ship communicate through exactly one thing: a JSON artifact.
{
"$schema": "https://noirbizarre.github.io/gh-ship/schema/release/v1.json",
"schemaVersion": 1,
"changed": true,
"version": "1.4.0",
"tag": "v1.4.0",
"release": {
"notes": "## What's Changed\n\n* ..."
}
}Your workflow uploads it as ship.release.json in an artifact named ship-release.
That is the whole protocol. It is versioned and
specified, and any tool can produce it —
jq is enough.
Validate it before uploading, and a protocol mistake becomes a red workflow with a precise error instead of a confusing failure later:
$ gh ship validate ship.release.json
× the artifact has unknown field `tags`
╭─[ship.release.json:5:3]
5 │ "tags": "v1.4.0",
· ───┬──
· ╰── not allowed here
help: did you mean `tag`?gh ship validate FILE needs no network, no repository, and no GitHub
authentication, so it works in any CI system.
A trimmed .github/ship.yml — gh ship init writes a documented one for you:
# $schema: https://noirbizarre.github.io/gh-ship/schema/config/v1.json
version: 1
release_branch: release/next
workflows:
prepare: prepare-release
publish: publish-release
pull_request:
title: "Release {{ version }}"
header: |
This PR prepares the next release.
footer: |
Generated automatically by gh-ship.
labels: [release]Only version and workflows.prepare are required.
Maintaining several release lines at once — a 1.x branch alive while main
moves on — is a two-line addition; see
Release lines.
| Command | What it does |
|---|---|
gh ship init |
Detect workflows, generate templates, write the config. |
gh ship validate [FILE] |
Check an artifact, or the setup and its workflows. |
gh ship preview |
Dry-run the prepare workflow and render the PR. Mutates nothing. |
gh ship prepare |
Run the prepare workflow, open or update the Release PR. |
gh ship status |
Where the release stands. A pure query. |
gh ship release |
Tag the merge commit, draft the release, publish assets, then make it visible. Requires the Release PR to be merged, or pass --merge. |
gh-ship starts workflows through the API, which can only start workflows declaring
on: workflow_dispatch. A workflow_call-only workflow — what people usually mean
by "reusable" — cannot be started this way. Declare both to have it both ways.
Workflows must also stamp gh-ship's nonce into their name:
run-name: prepare-release (ship:${{ inputs.ship_id }})gh workflow run returns no run id, so this is how gh-ship finds the run it
started, rather than guessing from timestamps.
Your prepare workflow must also declare a dry_run boolean input — it is what
gh ship preview sets to produce the artifact without committing anything.
gh ship validate checks all three.
GitHub's default GITHUB_TOKEN cannot trigger other workflows. A Release PR it
authors will not run your CI. If the Release PR must be tested before merging,
supply a GitHub App token or a fine-grained PAT; the generated workflow prefers
the SHIP_TOKEN secret when present.
Zero local state. Everything is reconstructed from GitHub. The release artifact
is embedded in the Release PR body as an HTML comment, so gh ship release works
days later, on another machine, run by someone else.
Draft-first releases. gh-ship creates the release as a draft, lets your publish workflow attach assets to it, and only then makes it visible. Publishing first would notify every watcher of an empty release.
The merge commit, not the branch tip. A squash merge creates a new commit, so
gh-ship always reads mergeCommit.oid rather than trusting a SHA it saw earlier.
changed: false is a success. A scheduled release job that finds nothing to
ship exits 0.
https://noirbizarre.github.io/gh-ship
MIT