| name | ship |
| description | Open a PR and merge it in one step — combines `bin/pr.sh` create with `bin/wait-for-pr-checks.sh` polling and `gh pr merge`. Default mode enables GitHub's native auto-merge so the terminal returns immediately; `--client-side` polls in the foreground and merges when CI goes green. TRIGGER when the user says "ship this PR", "ship it", "open and merge", "PR and merge", "create PR and auto-merge it", "wait for CI then merge", "block until merged", "/nyann:ship". Do NOT trigger on "open a PR" alone (no merge intent) — that's the `pr` skill. Do NOT trigger on "merge this existing PR #N" — ship is PR-creation + merge, not standalone merge of an already-open PR. Do NOT trigger on "release X.Y.Z" — that's the `release` skill, which tags a merged commit rather than creating one.
|
ship
Wraps bin/ship.sh. The script composes bin/pr.sh (PR creation),
bin/wait-for-pr-checks.sh (poll), and gh pr merge (merge) into a
single ShipResult. The output schema is at
schemas/ship-result.schema.json.
0. Drift check (quick, non-blocking)
Run bash bin/session-check.sh --flow=ship. If it produces output,
surface the line to the user verbatim. Do not block the flow.
1. Pick the mode up front
Two modes, decided at invocation. They have very different terminal
behavior, so confirm with the user when it isn't obvious which they
want.
| Mode | Default | Terminal behavior | Use when |
|---|
auto-merge | yes | returns ~instantly with outcome:"queued" | repo allows auto-merge, user wants to walk away |
client-side (--client-side) | opt-in | blocks until ship-or-fail | repo doesn't allow auto-merge, or user wants the wait surfaced |
If the user says "ship it and let me know when it's in" or "block
until merged", use --client-side. If they say "queue it for merge"
or "ship and I'll come back later", use the default.
When the user's intent doesn't clearly map to either mode, you MUST
call the AskUserQuestion tool (not plain text):
{
"questions": [
{
"question": "How should this PR be merged after CI passes?",
"header": "Ship mode",
"multiSelect": false,
"options": [
{ "label": "Auto-merge (Recommended)", "description": "Returns immediately; GitHub merges when checks pass" },
{ "label": "Client-side", "description": "Blocks here until CI is green, then merges" }
]
}
]
}
2. Pre-flight (same as pr skill)
bin/ship.sh runs the same gh guard as bin/pr.sh. Skip records
with reason gh-not-installed or gh-not-authenticated short-circuit
before any PR is created. Relay these as-is.
- The current branch must not be
main/master. The underlying
pr.sh enforces this; if it dies, route the user to
/nyann:branch first.
- Synthesize the title + body the same way the
pr skill does:
bin/pr.sh --target <cwd> --context-only first, then build a
Conventional-Commits title and a Summary/Test-plan markdown body.
Pre-action guards
bash bin/pre-action-guard.sh --flow ship --target <cwd> [--profile <resolved-profile.json>]
By default ship runs the PR-flow guards (branch-pushed, wip-commits). A
guards.ship array REPLACES that default set (subset semantics — it does
not append) and may promote severities.
coverage-delta is an opt-in guard — off by default. Since
guards.ship replaces the default set, enable it by re-listing the
built-ins alongside it:
"guards":{"ship":[{"name":"branch-pushed"},{"name":"wip-commits"},{"name":"coverage-delta"}]}.
Naming only coverage-delta would silently drop them. It warns (advisory
by default; promote to confirm via the profile) when the branch lowers
test coverage versus the cached .nyann/coverage-baseline.json, tolerant
up to guards.coverage_delta_threshold percentage points (default 0). It
reuses an existing CI coverage artifact and soft-skips when none is
present (it never runs the suite), so it's best paired with a profile/CI
that already produces coverage. Refresh the baseline on the base branch /
post-merge with bash bin/guards/coverage-delta.sh --update-baseline.
| Exit | Meaning | Action |
|---|
| 0 | Pass | Continue |
| 3 | Critical guard failed | Refuse to ship. The user has to fix the underlying issue or pass --skip-guards. |
| 4 | Confirm-severity guard failed | AskUserQuestion: "Proceed with ship?" — explicit confirm required. |
3. Confirm before shipping
Show the user the proposed title, body, and mode before invoking.
- Auto-merge: "Ship it via auto-merge? (returns immediately;
GitHub merges when checks pass)"
- Client-side: "Block here while CI runs and merge when green?
Default timeout is 30 minutes (polling every 30 seconds).
Override with
--timeout / --interval if your CI is slower."
Skip the confirmation only when the user said "just ship it" / "don't
ask, ship it now".
4. Invoke
bin/ship.sh \
--target <cwd> \
--title "<conventional-title>" \
--body "<markdown-body>" \
[--base <branch>] \
[--draft] \
[--client-side] \
[--merge-strategy squash|rebase|merge] \
[--timeout <sec>] [--interval <sec>] \
[--allow-no-checks]
--draft opens as draft (auto-merge will wait for ready-for-review +
checks; client-side mode merges as draft if you pass --draft, which
is rarely what you want). --merge-strategy defaults to squash;
respect the user's preference if they name a different one. --timeout
and --interval are forwarded to the wait phase in client-side mode
(defaults: 30 min / 30 s).
--allow-no-checks is a safety opt-out for client-side mode. By
default, the script refuses to merge when the waiter reports
no-checks — that outcome on a fresh PR almost always means
"workflows haven't attached yet" rather than "this repo has no PR
CI", so silently merging would defeat the gate. Pass
--allow-no-checks only when you know the repo runs no PR-side
checks (no GitHub Actions, no required reviews, etc.) and the empty
state is intentional. Auto-merge mode doesn't need this flag —
GitHub's server-side auto-merge already handles required-checks
state correctly.
5. Interpret the outcome
Branch on .outcome:
| Outcome | Mode | What happened | What the user should do |
|---|
queued | auto-merge | GitHub's auto-merge enabled; merge happens server-side when checks + reviews pass | trust it, walk away |
shipped | client-side | merge call succeeded after green CI | done; PR is merged + branch deleted |
ci-failed | client-side | one or more checks failed OR the PR had no checks attached without --allow-no-checks; loop bailed | inspect ci_failed_reason in the JSON; for the no-checks case re-run with --allow-no-checks if the empty state is intentional, otherwise wait for workflows to attach and retry |
ci-timeout | client-side | wait deadline hit, some still in_progress | retry with --timeout higher, or investigate runners |
merge-failed | either | auto-merge couldn't be enabled (e.g. branch protection) or gh pr merge failed | inspect merge_failed_reason; common causes: required reviews missing, branch protection forbids the strategy |
pr-failed | either | bin/pr.sh died before producing a URL | rerun /nyann:pr to surface the underlying error |
skipped | either | gh missing/unauthed; nothing was created | tell the user; can't proceed |
The script always exits 0. Branch on the .outcome field in the
JSON to determine what happened.
6. Handling errors
merge-failed with reason like "auto-merge is not allowed" → suggest
retrying with --client-side, which polls then calls gh pr merge
directly (works even when server-side auto-merge is disabled).
merge-failed with reason like "required reviews" → relay verbatim;
the user needs a reviewer to approve before merge can proceed.
ci-failed → don't suggest re-running ship; route the user to fix
the failing job first. Mention they can check it via
gh pr checks <num> directly.
When to hand off
- "Just open the PR" / "don't merge yet" →
pr skill.
- "Wait for CI but don't merge" →
wait-for-pr-checks skill.
- "Tag a release after merge" →
release skill — invoke after ship
reports shipped or after the auto-merge actually lands.