# Ship

> 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.

- Skill: `thettwe/ship` (Agent Skill)
- Install (CLI): `npx skillmds@latest add thettwe/ship`
- Raw SKILL.md: https://api.skillmd.com/api/skills/thettwe/ship/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: thettwe (https://skillmd.com/u/thettwe)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/thettwe/ship

---


# 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):

```json
{
  "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.

