Merge and deploy
Contract
| Field | Bound contract |
|---|---|
| Trigger | The user runs /merge-and-deploy |
| Authority | Remote: merges the PR, triggers or verifies deployment, and reverts on request; requires explicit human invocation. Every merge, deploy trigger, and revert requires explicit human confirmation at a named gate. The model never auto-merges, auto-deploys, or auto-reverts without a confirmed decision. |
| Side effect | Merges the PR into the base branch, deletes the feature branch, and triggers or verifies the deployment path. A revert creates a new commit on the base branch. |
| Done | Changes are landed and deployment is initiated or verified, reported in a final report |
Inputs
- A PR on the current branch (auto-detected), or a PR number given as
#NNN. - Optional: a production URL for post-deploy canary verification.
- Optional: a staging URL or staging workflow for staging-first verification.
- The GitHub CLI (
gh) must be authenticated. - The project test command (read from project config, default
bun test).
Procedure
Pre-flight. Verify
gh auth statussucceeds; if not, stop and tell the user to rungh auth login. Detect the PR: if a#NNNargument was given, strip the#and use the number as$PR_NUMBER; otherwise leave$PR_NUMBERempty. Rungh pr view ${PR_NUMBER:-} --json number,state,title,url,mergeStateStatus,mergeable,baseRefName,headRefName. If no PR exists, stop. IfstateisMERGEDorCLOSED, stop and report. IfOPEN, continue. Record the base branch frombaseRefNameas$BASEand the head branch as$HEAD. Done when:gh auth statussucceeds and one OPEN PR is recorded with its number, base branch, and head branch, or a stop condition is reported with the specific failure.First-run dry-run validation. If this project has no record of a prior confirmed deploy, run a dry run before any irreversible action. Detect deploy infrastructure: check for persisted deploy configuration in the project (conventionally a
## Deploy Configurationsection inCLAUDE.md); auto-detect platform from config files (fly.toml,render.yaml,vercel.jsonor.vercel/,netlify.toml,Procfile,railway.jsonorrailway.toml); detect deploy workflows under.github/workflows/whose name or content matchesdeploy,release,production, orcd. Validate each detected command: platform CLI status, production URL reachability viacurl -sf <url> -o /dev/null -w "%{http_code}". Detect staging environments (staging URL in config, staging workflow in.github/workflows/). If any validation fails, stop and report the failing component. Done when: every detected deploy path is validated (CLI present, URL reachable, workflow exists) or a failing component is named and the stop is reported, or the project has a prior confirmed deploy and this step is skipped.Pre-merge checks. Run
gh pr checks --json name,state,status,conclusion. If any required check is failing, stop. If required checks are pending, proceed to step 4. If all checks pass, skip to step 5. Checkgh pr view --json mergeable -q .mergeable; ifCONFLICTING, stop and tell the user to resolve conflicts and re-run. Done when: the check state is classified as all-passing (skip to step 5), pending (proceed to step 4), or failing/conflicting (stop with the specific check or conflict named).Wait for CI. If required checks are pending, poll with
gh pr checks --watch --fail-fastup to 15 minutes. If CI fails, stop. If timeout, stop. Record the CI wait duration. Done when: all required checks reach a terminal conclusion (pass or fail) and the outcome is recorded, or the 15-minute timeout fired and the stop is reported.Readiness gate. This is the last check before an irreversible merge. Gather evidence for each:
- Review staleness: Query
gh pr view --json reviews. Find the most recent review and capture its commit OID into$REVIEW_COMMIT. Compare against current HEAD withgit rev-list --count $REVIEW_COMMIT..HEAD. 0 commits since review → CURRENT; 1-3 → RECENT; 4+ → STALE. If the rev-list fails (the stored commit was rebased away), treat as STALE. If review is STALE or not run, offer an inline quick review of the diff before proceeding. - Test results: Run the project test command. If tests fail, this is a blocker: stop.
- PR body accuracy: Read the PR body with
gh pr view --json body. Compare againstgit log --oneline $BASE..HEAD. Flag missing features, stale descriptions, or version mismatches as warnings. - Documentation check: Check whether
CHANGELOG.mdandVERSIONwere modified on this branch withgit diff --name-only $BASE...HEAD -- CHANGELOG.md VERSION. If new features are present but docs were not updated, warn. Build a readiness report listing reviews, tests, documentation, and PR body accuracy with warnings and blockers counted. Present it to the user and require explicit confirmation: merge, hold to fix warnings, or merge anyway understanding the risks. If the user chooses to hold, stop with specific next steps. Only proceed to merge on explicit confirmation. Done when: the readiness report is presented with warnings and blockers counted, and the user has explicitly chosen merge, hold, or merge-with-risk, or a blocker stopped the process.
- Review staleness: Query
Merge the PR. Record the start timestamp. Try auto-merge first:
gh pr merge --squash --auto --delete-branch. If--autosucceeds, recordMERGE_PATH=auto. If--autoexits non-zero, fall through to direct merge only when the error names auto-merge unavailability (auto-merge disabled for the repo, or the PR is already mergeable with no pending required checks):gh pr merge --squash --delete-branch. Any other--autofailure skips the direct merge and goes to the authoritative-state rule below; a second merge attempt is never the response to an ambiguous failure. If direct merge succeeds, recordMERGE_PATH=direct. If the merge fails with a permission error, stop. After any non-zero exit fromgh pr merge, never retry the merge. Query authoritative PR state:gh pr view --json state,mergeCommit,mergedAt,mergedBy,autoMergeRequest. IfstateisMERGED, the server-side merge succeeded (possibly a concurrent merge); capture the merge SHA frommergeCommit.oidand record it as$MERGE_SHA, then continue. Do not require the PR head SHA to be an ancestor of the base branch, since squash merges create a new commit. IfstateisOPEN, checkautoMergeRequest: if non-null, a merge queue is in use; if null, surface both the merge stderr and the open state, then stop. IfstateisCLOSED, stop. Merge queue: ifMERGE_PATH=autoand the PR does not immediately becomeMERGED, pollgh pr view --json state,autoMergeRequestevery 30 seconds up to 30 minutes. A PR that isOPENwith a non-nullautoMergeRequestis still waiting for required checks or queue processing: keep polling. If merged, capture the merge SHA and record it as$MERGE_SHA. If the PR isOPENandautoMergeRequestis null (removed from the queue or auto-merge cancelled), stop. IfCLOSED, stop. If timeout, stop. CI auto-deploy detection: after the merge, check whether a deploy workflow was triggered by the merge commit:gh run list --branch $BASE --limit 5 --json name,status,workflowName,headSha. Match runs to the merge commit SHA. Done when: the PR state isMERGEDand the merge SHA is captured, or a stop condition is reported with the specific error and the PR state at failure.Detect deploy strategy. Check for persisted deploy config in the project; if found, use it directly. Otherwise auto-detect platform from config files and deploy workflows as in step 2. Classify the diff scope: run
git diff --name-only $BASE...HEADand categorize changes as frontend, backend, config, or docs. If the only scope is docs, skip verification entirely; report and finish. If no deploy workflow and no URL were detected, ask the user whether this is a web app (provide a URL) or a library/CLI (nothing to verify). If staging was detected and the changes include code, offer staging-first verification: verify on staging, then proceed to production. Done when: the deploy strategy is classified as docs-only (finish), web-app with URL, library/CLI with nothing to verify, or staging-first.Wait for deploy. If a GitHub Actions deploy workflow was detected, find the run matching the merge commit SHA with
gh run list --branch $BASE --limit 10 --json databaseId,headSha,status,conclusion,name,workflowName,url, assign$RUN_IDand$URLfrom the matching run, and pollgh run view $RUN_ID --json status,conclusionevery 30 seconds. For platform CLI deploys (Fly.io, Render, Heroku), poll the platform status command or the production URL withcurl -sf <url> -o /dev/null -w "%{http_code}". For auto-deploy platforms (Vercel, Netlify), wait 60 seconds then proceed to canary. Record deploy duration. If the deploy fails, offer: investigate logs, revert the merge, or continue to health checks. If timeout (20 min), warn and ask whether to continue. Done when: the deploy run or platform command reaches a terminal conclusion and the verdict is recorded, or a timeout is reported with the chosen next action.Canary verification. If a production URL is available, verify deploy health. Run
curl -sf <url> -o /dev/null -w "%{http_code}"and confirm 200 status. Check that the page has real content (not blank or an error page). Check that load time is under 10 seconds. If all pass, mark HEALTHY. If any fail, present the evidence and offer: mark as warming up, revert the merge, or investigate further. Done when: the production URL returns 200 with real content under 10 seconds and is marked HEALTHY, or a failure is presented with the specific failing check and the user chose an action.Revert (if requested). If the user chooses to revert at any failure point:
git fetch origin $BASE,git checkout -b revert/$MERGE_SHA origin/$BASE,git revert $MERGE_SHA --no-edit. If the base branch is unprotected, push the revert directly to base:git push origin revert/$MERGE_SHA:$BASE. If the base branch has push protections, push the revert branch and create a revert PR:git push origin revert/$MERGE_SHA, thengh pr create --base $BASE --head revert/$MERGE_SHA --title "$PR_TITLE", where$PR_TITLEisrevert: <original PR title>. If the revert has conflicts, tell the user the merge commit SHA and stop. Note the revert commit SHA. Done when: the revert commit is pushed to base, the revert PR is created and its head branch is pushed, or a conflict stopped the process and the merge SHA is reported.Deploy report. Produce a final report: PR number and title, branch flow, merge timestamp and method, merge path (auto/direct/queue), merge SHA, timing for each stage (CI wait, queue, deploy, staging, canary, total), review status, CI status, deploy status, staging status, verification verdict and scope, and a final VERDICT: DEPLOYED AND VERIFIED, DEPLOYED (UNVERIFIED), STAGING VERIFIED, or REVERTED. Display it to the user and save it. Done when: the report contains every stage's timing and status and a terminal VERDICT, and it is displayed and saved to disk.
Failure and recovery
- CI failing: stop; do not merge code that has not passed CI.
- Merge conflicts: stop; tell the user to resolve and re-run.
- Merge command non-zero exit: never retry
gh pr merge. Querygh pr view --json stateand act on authoritative server state. A concurrent merge may have succeeded; reportstate == MERGEDas "PR is merged on GitHub," not "the merge succeeded." - Merge queue timeout (30 min): stop; tell the user to check the merge queue page.
- Deploy workflow failure: the code is merged but may not be live. Offer investigate, revert, or continue to health checks. Never silently proceed.
- Canary health failure: present evidence and offer revert. Never silently mark unhealthy as healthy.
- Permission denied on merge: stop; tell the user a maintainer is needed or branch protection rules must be checked.
- Partial result rule: if any stage fails after the merge, the merge is already landed; report the partial state explicitly (merged but deploy unverified, or merged and reverted) rather than claiming the done predicate holds.
- Rollback: revert creates a new commit undoing the merge; the previous version restores once the revert deploys.
Output
A final deploy report with PR details, merge metadata, per-stage timing, review/CI/deploy/verification status, and a terminal VERDICT. The report is displayed to the user and saved to a deploy-reports directory.