| name | ship |
| description | Ship the current branch's PR — rebase onto fresh master if needed, squash-merge the PR (CI is the merge gate); prod then auto-deploys on merge — Vercel via its own GitHub integration, Convex via .github/workflows/deploy.yml — so the agent mainly sequences migrations + verifies the deploys went green. Use when the user says "ship it", "ship this", "/ship", "land it", "merge and deploy", or otherwise wants the current work merged to master AND live. Touches PRODUCTION — any manual prod command (a data migration, or an out-of-band deploy) gates on explicit per-turn approval. |
Ship
Take the current branch from "PR approved" to "live in prod" in one guided
flow: rebase if needed → squash-merge → prod auto-deploys → verify.
Prod auto-deploys on merge by two mechanisms: Vercel via its own GitHub
integration (makawulu, production on every master push) and Convex via
.github/workflows/deploy.yml (fantastic-dotterel-572, only when convex/**
changed). So for an ordinary PR you don't run the deploy by hand — you land the
PR and watch the deploys go green. You still drive the human parts of a
migration (run convex run migrations:<fn> --prod between the widen and
narrow merges), and the manual deploy commands below remain as the out-of-band /
fallback path. Those manual prod commands hit production — read the safety
rules first and don't skip the approval gates.
🚫 Safety rules (read first)
These come from .claude/rules/rabbithole-convex-deploys.md — that file is
authoritative; this is the operational checklist.
npx convex deploy ALWAYS targets prod (fantastic-dotterel-572),
regardless of CONVEX_DEPLOYMENT. Never run it without Andy's explicit OK
in the current turn.
- Deploy from the MASTER checkout, never a worktree. Both
convex deploy
and vercel --prod run from the primary (non-worktree) checkout — the
env's main_checkout_path (member_repositories[].main_checkout_path), e.g.
/Users/andys/code/rabbithole-2 on this machine. A worktree must cd there
for the deploy steps. (scripts/deploy-vercel-prod.sh hard-refuses to run from
a worktree, so use it rather than a bare vercel.)
- Per-turn approval is required before EACH prod command (the Convex deploy
and the Vercel deploy are two separate approvals). Approval to merge is NOT
approval to deploy.
- After each deploy, read the target back and confirm the slug is
fantastic-dotterel-572 (Convex) / the alias repointed (Vercel). The command
succeeding ≠ the command being correct.
- Non-interactive flags are mandatory from Codex's shell:
convex deploy -y, vercel --prod --yes (they only answer the interactive confirmation prompt; they do
NOT bypass the approval gate).
- Toolchain: prefix prod/deploy commands with
export PATH="/Users/andys/.local/share/mise/shims:$PATH" and pass
dangerouslyDisableSandbox: true (deploys need network + keychain).
Preconditions
Before starting, establish:
- Which PR.
gh pr view --json number,title,state,headRefName,mergeable.
If no PR exists for the current branch, STOP and ask whether to create one
(gh pr create --fill) — don't invent a merge.
- Working tree is clean (
git status -s). Uncommitted work must be
committed (or explicitly stashed/dropped) first — surface it, don't
silently stash.
- The PR is approved / the user wants it shipped. This skill assumes the
review is done. If unsure, confirm.
Step 1 — Rebase onto fresh master
First, decide whether you even need a local rebase. Squash-merge (Step 2)
takes the net master..head diff and never replays your individual commits, so
branch-history tangle no longer blocks the merge — and CI (.github/ workflows/ci.yml) re-runs the full suite on the actual merge result. So a local
rebase is now mostly for your own fast feedback, not a merge prerequisite. If
master moved but your branch has no real conflicts, you can let GitHub do a
server-side squash-merge directly. Only rebase locally when you want to
re-run the suite yourself against the new merge base, or to resolve real
conflicts. Check for conflicts without rewriting anything first:
git fetch origin
git merge-tree $(git merge-base HEAD origin/master) HEAD origin/master
If you do rebase locally, from the worktree (where the branch lives):
export PATH="/Users/andys/.local/share/mise/shims:$PATH"
git fetch origin
git rebase origin/master
git push --force-with-lease
⚠️ Auto-sync footgun — push the instant the rebase finishes. This
environment runs a worktree git auto-sync that, whenever local HEAD
diverges from origin/<branch>, reconciles by running git merge <origin-tip> as you — producing a no-op Merge commit '<sha>' into <branch> seconds later. A local rebase is a divergence, so leaving the
branch un-pushed (e.g. while the re-test below runs for minutes) lets the
auto-sync inject that merge commit, which corrupts the PR's diff (GitHub
starts counting master's commits as part of the PR — the file list balloons).
Pushing immediately makes origin == local, so there's nothing to merge. If a
stray merge commit still appears: git reset --hard <sha> (the rebase (finish) entry in git reflog), then re-push. Also in CLAUDE.md →
"Rebasing a pushed branch".
- If the tree is dirty, stash first (
git stash -u), rebase, git stash pop — but prefer committing real work over stashing it.
- On conflicts: resolve them,
git add, git rebase --continue. Don't
--skip a conflicted commit. If a conflict is non-trivial, stop and show
Andy the conflicting hunks before resolving.
- If already up to date, say so and move on.
Re-test after rebase (CI is the enforced gate; local is optional)
CI is the required merge gate now — a branch ruleset on master blocks the
merge until Typecheck (convex + frontend), Unit tests (vitest), and
Production build (next) pass (see .claude/rules/rabbithole-multi-agent-workflow.md). It runs
on the PR merge result and again on push to master. So you don't need to
re-run tsc / vitest / next build locally to gate a merge, and must not
re-run them mechanically on every rebase. Run them locally only when it speeds up
your own loop — don't push-and-pray to use CI as your compiler.
Two things CI does NOT cover — still your job locally:
CONVEX_DEPLOYMENT=dev:<this-worktree-slug> npx convex dev --once
npx eslint <touched files>
- Convex codegen freshness (required). CI typechecks the committed
convex/_generated. If you changed a Convex function and didn't regenerate, CI
typechecks stale types and can false-green — so after Convex changes run
npx convex dev --once and commit the regenerated _generated. (Worktree dev
slug is in .env.local; never push the rebase to prod here.)
- eslint (not gated). Lint isn't a CI job (master's baseline is red), so it
won't block a merge — but it also won't be caught. Run it on touched files if
you care; nothing enforces it.
If a rebase broke something, fix it before merging — don't lean on CI to
discover the obvious. Commit the fix and re-push (git push --force-with-lease).
If you skip the optional local pass, let CI go green before merging rather
than merging blind (or use gh pr merge <N> --squash --auto to let GitHub merge
the moment it's green).
Step 2 — Squash-merge the PR
Default to squash-merge. Squash collapses the PR to a single commit built
from its net master..head diff, ignoring branch-history tangle entirely. This
is the robust choice under heavy concurrency (the new normal here — ~10 agents
merging to master at once): it defangs the whole class of merge failures we hit
repeatedly —
- Stacked-PR replay failure. Rebase-merge replays each commit server-side;
when a lower PR already merged, GitHub tries to re-apply its commits onto a
master that has them →
This branch can't be rebased. Squash takes the net
diff, so there's nothing to replay.
- The auto-sync merge-commit footgun (Step 1): squash doesn't care if a
stray
Merge commit '…' is in the branch history — it diffs endpoints.
- Duplicate-commit conflicts from re-applying already-landed work.
Master becomes one-commit-per-PR: clean git revert <sha>, clean bisect, clean
audit. AI-authored intra-PR micro-commits have near-zero archival value, so the
lost granularity costs nothing. (The old rebase-merge default existed to keep
patch-ids stable for locally-rebased stacked branches; squash makes stacks Just
Work a different way — see below — so that rationale no longer applies.)
2a — Stacked PRs: merge bottom-up
If this PR is stacked on another open PR, merge the lower PR first, then for
this (upper) PR reset to fresh master and re-apply only its net diff before
squashing — don't try to preserve/replay the lower PR's commits:
git fetch origin
git checkout -B <branch> origin/master
git cherry-pick <lower>..<this-head>
git push --force-with-lease
Then squash-merge as below. (If the PRs are independent, just merge each.)
2b — Squash-merge
A quick clean-history glance is still nice but no longer load-bearing — a
stray auto-sync merge commit inflates the PR's displayed file list but squash
merges the correct net diff regardless:
git fetch origin
gh pr view <N> --json commits --jq '.commits | length'
Let CI be green (or run the local pass in Step 1), then merge:
gh pr merge <N> --squash --delete-branch
gh pr merge <N> --squash --auto --delete-branch
--delete-branch cleans up the remote branch. The local worktree branch is
removed at teardown (see /cleanup), not here.
- Confirm it merged:
gh pr view <N> --json state → MERGED. (A benign
trailing git error like "master is already used by worktree …" can appear even
when the merge succeeded — check state, not the stderr.)
- Squash takes the net diff, so you do NOT need a pristine linear branch
history to merge — this is exactly why it survives the auto-sync footgun and
stacked-PR tangle that broke rebase-merge.
Step 3 — Prod auto-deploys on merge
Both halves of prod auto-deploy on merge, by two mechanisms:
- Vercel — its own GitHub integration (
makawulu is Git-linked,
productionBranch: master). Production deploys on every master push, previews
on PRs. Nothing in this repo drives it; no secret needed.
- Convex —
.github/workflows/deploy.yml (on: push: master,
paths: convex/**). Deploys fantastic-dotterel-572 only when the backend
changed. Needs the repo secret CONVEX_DEPLOY_KEY.
So the squash landing is the deploy. For an ordinary PR you run no prod
command — you watch both go green:
git rev-parse origin/master
gh run watch "$(gh run list --workflow=deploy.yml --branch=master --limit=1 \
--json databaseId --jq '.[0].databaseId')" --exit-status
GitHub emails you + reds-out the commit if the Convex job fails, so you can also
just trust the loud-failure path. If it fails, open the run — the Convex job
writes a step-summary naming the likely cause (usually an owed migration).
(One-time setup: the Convex job needs the repo secret CONVEX_DEPLOY_KEY. If a
Convex deploy fails with an auth/missing-secret error, that's the gap.)
3a — Migrations: you still sequence them ⚠️ (one prod command stays yours)
Auto-deploy does NOT run your data migration, and a narrow merged before its
migration ran will (correctly) fail the Convex deploy — schema rejects a
data-at-rest mismatch. That failure is safe (atomic; old functions keep
serving, no data loss), but prod stays on the old schema until you act. So drive
a widen→migrate→narrow ship merge-by-merge:
- Merge the widen PR, wait for its Convex auto-deploy to go green (the
migration fn is now live in prod).
- Run the migration by hand — the one prod command you still run yourself
(it's data, not a deploy), with per-turn approval, from the master checkout:
export PATH="/Users/andys/.local/share/mise/shims:$PATH"
npx convex export --prod --path snapshot-pre-migrate.zip
npx convex run migrations:<fn> --prod
- Merge the narrow PR — its Convex auto-deploy now passes (data matches).
The rename runbooks still apply — auto-deploy only removes the manual
convex deploy keystrokes around the migrations, not the human sequencing.
3b — Out-of-band / fallback manual deploy ⚠️ REQUIRES APPROVAL
Deploy by hand only when you're NOT shipping via a merge — a hotfix you can't
wait to merge, or re-deploying after a failed Action without re-running the job.
Same prod-safety rules (master checkout only, per-turn approval per command, read
the slug back). From the master checkout:
cd "$MAIN_CHECKOUT"
git checkout master && git pull --ff-only
export PATH="/Users/andys/.local/share/mise/shims:$PATH"
npx convex deploy -y
bash scripts/deploy-vercel-prod.sh
⚠️ Do NOT use /version to verify Convex — it returns the cloud backend build,
identical across prod+dev and unchanged by a push. The ✔ Deployed line is the
real signal. If you bypass the Vercel script, the command MUST be
vercel --prod --yes --scope tradewinds-school (the bare CLI defaults to the wrong
greenfield scope). Re-check the site:
curl -sI https://learn.tradewinds.school | head -1 (expect HTTP/2 200).
Step 4 — Post-ship
- Report: PR # merged, Convex slug confirmed, Vercel curl result.
- Offer
/cleanup to tear down the worktree + dev server. /cleanup also
prunes orphan dev deployments — every retired worktree leaves a private
dev deployment behind, and they accumulate against the project's finite
deployment quota until pnpm worktree:up starts failing. If you're shipping
without running /cleanup, still run the read-only orphan check (safe,
unprompted) from
.claude/rules/rabbithole-deployment-cleanup.md
and offer to prune (deleting is destructive → confirm scope first; the
recipe's KEEP set never touches prod or the shared default dev).
Failure modes
- Accidental prod deploy / wrong slug: stop, tell Andy immediately, don't
chain more commands. Follow "If a prod deploy happens by accident" in the
deploys rule.
- Vercel deploy succeeds but alias didn't repoint: the curl returns the old
build or errors — re-run
scripts/deploy-vercel-prod.sh (or
vercel --prod --yes --scope tradewinds-school / vercel alias), tell Andy.
- Vercel deploy fails with
NEXT_PUBLIC_CONVEX_URL ... is required (or other
missing env vars): the checkout is linked to the WRONG Vercel project.
.vercel/project.json is gitignored, so a checkout can silently point at
rabbithole-2 (team greenfield, zero env vars) instead of makawulu (team
tradewinds-school, the real prod project for learn.tradewinds.school). The
wrong project's name matches the local dir, which hides the mistake. Fix:
scripts/deploy-vercel-prod.sh self-heals this; or manually
vercel link --project makawulu --scope tradewinds-school --yes.
- Merge conflict you can't cleanly resolve: stop at Step 1, show the hunks,
let Andy decide.
- Stray
Merge commit '…' into <branch> after a rebase (no-op, but it
balloons the PR's displayed file list with master's changes): the worktree
auto-sync merged the old origin tip back because local diverged from origin
(Step 1 footgun). With squash-merge (the default) this is cosmetic — squash
merges the net diff regardless, so it won't corrupt what actually lands. To
tidy the PR's displayed diff anyway: git reset --hard <rebase-finish-sha>
(from git reflog), git push --force-with-lease. Prevent it by pushing the
instant the rebase finishes.
- If you can't out-push the auto-sync (your clean commit is an ancestor
of the polluted tip, so
git push --force-with-lease no-ops with "Everything
up-to-date" and the auto-sync keeps re-pushing the merge commit faster than
you can rewind it): this is a non-issue now — just confirm the PR's net
diff is your intended files (gh pr diff <N> --name-only) and
gh pr merge <N> --squash --delete-branch. Squash collapses the branch to
that net diff and lands one clean commit on master regardless of the stray
merge commit.