Stage, commit, push, and open a PR for one app, gated on a passing validation, then watch CI to a terminal state. Use when the user says "ship it", "open a PR", "deploy <slug>", or after preview and review look good.
Installs into .claude/skills of the current project.
Are you the author of Ship App?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kyle-chalmers-ship-app)
---
name: ship-app
description: Stage, commit, push, and open a PR for one app, gated on a passing validation, then watch CI to a terminal state. Use when the user says "ship it", "open a PR", "deploy <slug>", or after preview and review look good.
argument-hint: "<slug>"
allowed-tools: [Bash, Read]
disable-model-invocation: true
---
# /ship-app
> **Repo overlay:** if `.streamsnow/overlays/ship-app.md` exists in this repo, read it first — committed, repo-specific additions/overrides ([_shared/overlays.md](../_shared/overlays.md)). Outside Claude Code, also read [_shared/other-agents.md](../_shared/other-agents.md).
Take a built app from working tree to open PR, gated on validation, then watch CI. **One app at a
time**; deployment is **CI-only on merge to `main`** — never a local Snowflake deploy. Repo-level
changes (templates, governance, CI) do not belong in a `/ship-app` PR; commit those separately.
## Steps
1. **Resolve the slug**; `git status --short apps/<slug>` must show changes to ship — zero changes
ahead of `main` → nothing to PR; stop and say so.
2. **Preflight 0 — review gate (asks, never blocks):**
`streamsnow review-gate classify <slug> --format json`. Reviewed/trivial/skip-marker → proceed.
`.apps[0].needs_review == true` → offer the choice: review first (`/review-app <slug> --auto`) or **ship
as-is** — always available (ships can be time-critical; /validate-app + CI are the real publish
gates), but note "shipped unreviewed" in the PR body so the approver sees it. If the app's SQL
changed, recommend `/sql-review <slug>` before merge (never required). Never auto-run either.
3. **Hard gate:** run /validate-app. Any FAIL → stop; report and do not stage, commit, or push.
/validate-app is the fix-it path — don't auto-fix here.
4. **Branch hygiene.** On `main` → `git switch -c ship/<slug>-<desc>` first. **Never reuse a
squash-merged branch** — Git's three-way merge can silently revert your own deletions. Check:
`gh pr list --search "head:$(git branch --show-current) is:merged" --json number` — non-empty
means the branch is spent; start fresh off `main` and re-apply (cherry-pick or copy edits, never
`git merge` from the old branch).
5. **Stage only the app:** `git add apps/<slug>` (plus the repo README only if its app-index row
changed). Show `git diff --cached --stat`; unstage anything else — don't widen scope to "fix one
more thing".
6. **Commit** conventionally (`feat(<slug>): <summary>`), the summary matching the diff —
underclaim, never overclaim. Pre-commit hooks block → **stop and surface the error**; never
`--no-verify` (the hooks run the same checks CI does).
7. **Sync with `origin/main` before pushing** per
[_shared/sync-with-main.md](../_shared/sync-with-main.md): rebase (never merge), then
`git push --force-with-lease`. A rebase conflict stops with manual instructions — don't guess a
resolution.
8. **Push** (`git push -u origin HEAD` if the sync didn't already) — refuse to push to `main`.
9. **Open the PR** (title/body: what changed, validation passed). Print the number and URL.
10. **Note the deploy path:** merging to `main` triggers CI, which deploys — no local deploy step.
11. **Watch checks to a terminal state** (`gh pr checks <num> --watch` in the background;
`gh pr view <num> --json state,mergeStateStatus`) and report once on exit.
## Reporting the outcome
- **A check fails** → name it; translate deploy failures via
[_shared/deploy-error-translator.md](../_shared/deploy-error-translator.md) (failure signatures
differ by runtime — see [_shared/runtime-decision.md](../_shared/runtime-decision.md)); stop.
- **Green but unmerged** → it's waiting on a teammate's approval (you can't approve your own PR) —
say so plainly rather than looping on the checks.
- **Merged** → confirm, then report the deploy run's outcome.
## Gotchas
- **Squash-merged branch reuse is the highest-severity trap** — it fails silently: CI passes, the
deploy ships the wrong code. The step 4 check is non-negotiable.
- **Commit message must match the diff.** A claimed change with no matching hunk means a fix was
lost (often in manual conflict resolution) — re-read the diff and correct one or the other before
opening the PR.
- Most deploy-run failures resolve to one-time, admin-applied DDL emitted by
`streamsnow deploy-setup --admin`: surface the named fix; never run DDL from here.
## Troubleshooting
Push rejected (stale `--force-with-lease`) → re-fetch, rebase, push — never plain `--force`. PR
opens "behind" → `main` moved; re-run the sync step and let checks re-run.
## Done when
The PR is open, validation passed before staging, the branch is rebased on current `origin/main`,
and checks reached a terminal state with the outcome reported: a named failed check, "awaiting
approval," or merged + the deploy result.
## System-evolution retro (always, even on a clean ship)
One question before closing: did anything go wrong or get re-done this ship? If so, **which
layer was insufficient** — the config (`streamsnow.config.yaml` / governance rules), a skill,
a check (`streamsnow validate-app` / CI), or the deploy path? Propose the concrete fix to
*that* artifact. If the gap is in StreamSnow itself, file it against the plugin repo (issue or
a note the user can act on) rather than patching around it locally. Fixing the layer, not the
instance, is what compounds. (Ported from ticketwright's /ship Phase C.)