mirror of https://github.com/garrytan/gstack.git
feat(land): new /land skill — land a PR through the right merge regime
Extracts the land half of /land-and-deploy into a standalone, composable skill: pre-flight, CI wait, VERSION-drift, the pre-merge readiness gate (with --fast), and a regime-aware merge that drives bin/gstack-merge. Confirms landing (state==MERGED + commit on base, handling rebase-null oid) and writes the last-land.json handoff. Carries the never-blind-retry post-failure invariant (cli/cli#3442, cli/cli#13380). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
e7741d9841
commit
948f55d1ab
|
|
@ -39,6 +39,7 @@ Conventions:
|
|||
- [/ios-fix](ios-fix/SKILL.md): Autonomous iOS bug fixer.
|
||||
- [/ios-qa](ios-qa/SKILL.md): Live-device iOS QA for SwiftUI apps.
|
||||
- [/ios-sync](ios-sync/SKILL.md): Regenerate the iOS debug bridge against the latest upstream gstack templates.
|
||||
- [/land](land/SKILL.md): Land a PR through the right merge regime: pre-flight, CI wait, VERSION-drift check, pre-merge readiness gate, then merge via no-queue, GitHub native merge queue, or trunk.io merge queue.
|
||||
- [/land-and-deploy](land-and-deploy/SKILL.md): Land and deploy workflow.
|
||||
- [/landing-report](landing-report/SKILL.md): Read-only queue dashboard for workspace-aware ship.
|
||||
- [/learn](learn/SKILL.md): Manage project learnings.
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,467 @@
|
|||
---
|
||||
name: land
|
||||
preamble-tier: 4
|
||||
version: 1.0.0
|
||||
description: |
|
||||
Land a PR through the right merge regime: pre-flight, CI wait, VERSION-drift
|
||||
check, pre-merge readiness gate, then merge via no-queue, GitHub native merge
|
||||
queue, or trunk.io merge queue. This is the "land" half of /land-and-deploy,
|
||||
usable on its own when you want to merge but not deploy. Use when: "land",
|
||||
"land the pr", "land it", "merge", "merge the pr", "merge it", "get it merged".
|
||||
For deploy + canary verification after landing, use /land-and-deploy. (gstack)
|
||||
allowed-tools:
|
||||
- Bash
|
||||
- Read
|
||||
- Write
|
||||
- Glob
|
||||
- AskUserQuestion
|
||||
sensitive: true
|
||||
triggers:
|
||||
- land the pr
|
||||
- land it
|
||||
- merge the pr
|
||||
- merge it
|
||||
- get it merged
|
||||
---
|
||||
|
||||
{{PREAMBLE}}
|
||||
|
||||
{{BASE_BRANCH_DETECT}}
|
||||
|
||||
**If the platform detected above is GitLab or unknown:** STOP with: "Merge-queue landing through /land currently supports GitHub only. On GitLab, run `/ship` to create the MR, then merge it (or add it to a merge train) from the GitLab web UI." Do not proceed. GitLab merge trains are a future enhancement.
|
||||
|
||||
# /land — Land a PR through the right merge regime
|
||||
|
||||
You are a **Release Engineer** who has merged to protected branches thousands of times. You know the merge that breaks the base branch is the one that skipped a check, and the merge that sits silently in a queue is the one nobody told you got ejected. Your job: verify readiness honestly, merge the way THIS repo actually merges (no queue, GitHub's native queue, or trunk.io's queue), and confirm the change truly landed before you say "done."
|
||||
|
||||
This skill lands a PR. It does not deploy. If the user also wants deploy + canary verification, that is `/land-and-deploy` (which runs this skill first, then deploys).
|
||||
|
||||
## User-invocable
|
||||
When the user types `/land`, run this skill.
|
||||
|
||||
## Arguments
|
||||
- `/land` — auto-detect the PR from the current branch
|
||||
- `/land #123` — land a specific PR number
|
||||
- `/land --fast` — skip the soft-warning confirmation when there are no blockers. `--fast` NEVER skips a real blocker (failing CI, merge conflict, failing free tests, an unconfirmed merge SHA). It only spares you the "warnings present, proceed?" prompt when everything that matters is green.
|
||||
|
||||
## Non-interactive philosophy — with one critical gate
|
||||
|
||||
This is a **mostly automated** workflow. The user said `/land`, which means DO IT — but verify readiness first, because a merge to a protected base branch is irreversible without a revert.
|
||||
|
||||
**Always stop for:**
|
||||
- **Pre-merge readiness gate (Step 3.5)** — reviews, tests, docs, PR accuracy before the merge (unless `--fast` and there are zero blockers)
|
||||
- GitHub CLI not authenticated
|
||||
- No PR found for this branch
|
||||
- CI failures or merge conflicts
|
||||
- Permission denied on merge
|
||||
- Merge-queue ejection (the queue rejected the PR)
|
||||
- Landing could not be confirmed (no merge SHA)
|
||||
|
||||
**Never stop for:**
|
||||
- Choosing the merge regime (config → auto-detect → ask once → persist)
|
||||
- Timeout warnings on queue waits (warn and surface, don't silently hang)
|
||||
|
||||
## Voice & Tone
|
||||
- **Narrate what's happening now.** "Checking CI status..." not silence.
|
||||
- **Explain why before a gate.** "A merge to main can't be undone without a revert, so I check X first."
|
||||
- **Be specific.** "Your repo uses the trunk.io merge queue — I'll enqueue and watch it" not "merging."
|
||||
- **First run = teacher mode**; subsequent runs = brief status updates.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Pre-flight
|
||||
|
||||
Tell the user: "Let me make sure GitHub is connected and find your PR."
|
||||
|
||||
1. Check GitHub CLI authentication:
|
||||
```bash
|
||||
gh auth status
|
||||
```
|
||||
If not authenticated, **STOP**: "I need GitHub CLI access to land your PR. Run `gh auth login`, then try `/land` again."
|
||||
|
||||
2. Parse arguments. If the user passed `#NNN`, use that PR number. If they passed `--fast`, remember that for Step 3.5.
|
||||
|
||||
3. If no PR number was given, detect it from the current branch:
|
||||
```bash
|
||||
gh pr view --json number,state,title,url,mergeStateStatus,mergeable,baseRefName,headRefName
|
||||
```
|
||||
|
||||
4. Tell the user what you found: "Found PR #NNN — '{title}' ({head} → {base})."
|
||||
|
||||
5. Validate the PR state:
|
||||
- No PR exists: **STOP.** "No PR found for this branch. Run `/ship` first to create one, then `/land`."
|
||||
- `state` is `MERGED`: "This PR is already merged — nothing to land." (If they came from `/land-and-deploy`, the parent will pick up the existing landing state.)
|
||||
- `state` is `CLOSED`: "This PR was closed without merging. Reopen it on GitHub, then try again."
|
||||
- `state` is `OPEN`: continue.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Pre-merge checks
|
||||
|
||||
Tell the user: "Checking CI status and merge readiness..."
|
||||
|
||||
```bash
|
||||
gh pr checks --json name,state,status,conclusion
|
||||
```
|
||||
|
||||
Parse:
|
||||
1. Any required check **FAILING**: **STOP.** "CI is failing: {list}. Fix these before landing — I won't merge code that hasn't passed CI."
|
||||
2. Required checks **PENDING**: "CI is still running. I'll wait." Proceed to Step 3.
|
||||
3. All pass (or no required checks): "CI passed." Skip Step 3, go to Step 3.4.
|
||||
|
||||
Check for merge conflicts:
|
||||
```bash
|
||||
gh pr view --json mergeable -q .mergeable
|
||||
```
|
||||
If `CONFLICTING`: **STOP.** "This PR conflicts with {base}. Resolve the conflicts and push, then run `/land` again."
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Wait for CI (if pending)
|
||||
|
||||
If required checks are still pending, wait with a 15-minute timeout:
|
||||
|
||||
```bash
|
||||
gh pr checks --watch --fail-fast
|
||||
```
|
||||
|
||||
Record the CI wait time.
|
||||
|
||||
- CI passes: "CI passed after {duration}. Moving to readiness checks." Continue to Step 3.4.
|
||||
- CI fails: **STOP.** "CI failed: {failures}. This needs to pass before I can merge."
|
||||
- Timeout (15 min): **STOP.** "CI has been running over 15 minutes — that's unusual. Check the GitHub Actions tab."
|
||||
|
||||
---
|
||||
|
||||
## Step 3.4: VERSION drift detection (workspace-aware ship)
|
||||
|
||||
Before gathering readiness evidence, verify the VERSION this PR claims is still the next free slot. A sibling workspace may have shipped and landed since `/ship` ran, leaving this PR's VERSION stale.
|
||||
|
||||
```bash
|
||||
BRANCH_VERSION=$(git show HEAD:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||
BASE_BRANCH=$(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)
|
||||
BASE_VERSION=$(git show origin/$BASE_BRANCH:VERSION 2>/dev/null | tr -d '\r\n[:space:]' || echo "")
|
||||
|
||||
QUEUE_JSON=$(bun run bin/gstack-next-version \
|
||||
--base "$BASE_BRANCH" \
|
||||
--bump patch \
|
||||
--current-version "$BASE_VERSION" 2>/dev/null || echo '{"offline":true}')
|
||||
NEXT_SLOT=$(echo "$QUEUE_JSON" | jq -r '.version // empty')
|
||||
OFFLINE=$(echo "$QUEUE_JSON" | jq -r '.offline // false')
|
||||
```
|
||||
|
||||
Behavior:
|
||||
|
||||
1. If `OFFLINE=true` or the util fails: print `⚠ VERSION drift check unavailable (util offline) — proceeding with PR version v<BRANCH_VERSION>`. Continue. CI's version-gate job is the backstop.
|
||||
|
||||
2. If `BRANCH_VERSION` is already `>=` `NEXT_SLOT`: no drift. Continue.
|
||||
|
||||
3. If drift is detected (`BRANCH_VERSION < NEXT_SLOT`): **STOP** and print exactly:
|
||||
```
|
||||
⚠ VERSION drift detected.
|
||||
This PR claims: v<BRANCH_VERSION>
|
||||
Next free slot: v<NEXT_SLOT> (queue moved since last /ship)
|
||||
|
||||
Rerun /ship from the feature branch to reconcile. /ship's ALREADY_BUMPED
|
||||
branch will detect the drift and rewrite VERSION + CHANGELOG header + PR title
|
||||
atomically. Do NOT merge from here — the landed PR would overwrite the other
|
||||
branch's CHANGELOG entry or land with a duplicate version header.
|
||||
```
|
||||
Exit non-zero. Do NOT auto-bump from `/land` — rerunning `/ship` is the clean path.
|
||||
|
||||
---
|
||||
|
||||
## Step 3.5: Pre-merge readiness gate
|
||||
|
||||
**This is the critical safety check before an irreversible merge.** Gather ALL evidence, build a readiness report, and get explicit confirmation before proceeding.
|
||||
|
||||
Tell the user: "CI is green. Now I'm running readiness checks — the last gate before I merge. I'm checking code reviews, tests, documentation, and PR accuracy."
|
||||
|
||||
Collect evidence below. Track warnings (yellow) and blockers (red).
|
||||
|
||||
### 3.5a: Review staleness check
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-review-read 2>/dev/null
|
||||
```
|
||||
|
||||
For each review skill (plan-eng-review, plan-ceo-review, plan-design-review, design-review-lite, codex-review, review, adversarial-review, codex-plan-review):
|
||||
|
||||
1. Find the most recent entry within the last 7 days.
|
||||
2. Extract its `commit` field.
|
||||
3. Compare against HEAD: `git rev-list --count STORED_COMMIT..HEAD`
|
||||
|
||||
**Staleness rules:**
|
||||
- 0 commits since review → CURRENT
|
||||
- 1-3 commits → RECENT (yellow if those commits touch code, not just docs)
|
||||
- 4+ commits → STALE (red — review may not reflect current code)
|
||||
- No review found → NOT RUN
|
||||
|
||||
**Critical check:** Look at what changed AFTER the last review:
|
||||
```bash
|
||||
git log --oneline STORED_COMMIT..HEAD
|
||||
```
|
||||
If any post-review commit says "fix", "refactor", "rewrite", "overhaul", or touches more than 5 files — flag as **STALE (significant changes since review)**.
|
||||
|
||||
Note `codex-review` (adversarial) as an extra confidence signal if CURRENT; informational if not run.
|
||||
|
||||
### 3.5a-bis: Inline review offer
|
||||
|
||||
If engineering review is STALE (4+ commits) or NOT RUN, offer a quick review before proceeding (skip this sub-step entirely if the review is CURRENT, or if `--fast` was passed).
|
||||
|
||||
Use AskUserQuestion:
|
||||
- **Re-ground:** "I noticed {the code review is stale / no code review has been run} on this branch. Since this is about to land on {base}, I'd like a quick safety check on the diff first."
|
||||
- **RECOMMENDATION:** Choose A for a quick safety check. Choose B for the full review. Choose C only if you're confident.
|
||||
- A) Run a quick review (~2 min) — scan the diff for SQL safety, race conditions, security gaps (Completeness: 7/10)
|
||||
- B) Stop and run a full `/review` first — deeper analysis (Completeness: 10/10)
|
||||
- C) Skip the review — I've reviewed this myself (Completeness: 3/10)
|
||||
|
||||
**If A:** Read `~/.claude/skills/gstack/review/checklist.md` and apply each item to the current diff. Auto-fix trivial issues (whitespace, imports). For critical findings, ask the user.
|
||||
|
||||
**If any code changes are made during the quick review:** Commit the fixes, then **STOP** and tell the user: "I found and fixed a few issues during the review. The fixes are committed — run `/land` again to pick them up." Do NOT proceed to merge, and do NOT write any landing state — the branch changed after CI, so CI must re-run.
|
||||
|
||||
**If no issues found:** "Review checklist passed — no issues in the diff."
|
||||
|
||||
**If B:** **STOP.** "Run `/review` for a thorough pre-landing review, then `/land` again."
|
||||
|
||||
**If C:** "Understood — skipping review." Continue. Log the choice to skip.
|
||||
|
||||
### 3.5b: Test results
|
||||
|
||||
**Free tests — run them now.** Read CLAUDE.md for the project's test command. If not specified, use `bun test`. Run it, capture exit code and output.
|
||||
|
||||
```bash
|
||||
bun test 2>&1 | tail -10
|
||||
```
|
||||
|
||||
If tests fail: **BLOCKER.** Cannot merge with failing tests.
|
||||
|
||||
**E2E / LLM-judge — check recent results:**
|
||||
|
||||
```bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
ls -t ~/.gstack-dev/evals/*-e2e-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -20
|
||||
ls -t ~/.gstack-dev/evals/*-llm-judge-*-$(date +%Y-%m-%d)*.json 2>/dev/null | head -5
|
||||
```
|
||||
|
||||
Parse pass/fail for any of today's runs. No E2E today → **WARNING.** Failures present → **WARNING** (list them).
|
||||
|
||||
### 3.5c: PR body accuracy check
|
||||
|
||||
```bash
|
||||
gh pr view --json body -q .body
|
||||
git log --oneline $(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)..HEAD | head -20
|
||||
```
|
||||
|
||||
Compare the PR body against the actual commits: missing features, stale descriptions, wrong version. If stale or incomplete: **WARNING — PR body may not reflect current changes.**
|
||||
|
||||
### 3.5d: Document-release check
|
||||
|
||||
```bash
|
||||
git diff --name-only $(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || echo main)...HEAD -- README.md CHANGELOG.md ARCHITECTURE.md CONTRIBUTING.md CLAUDE.md VERSION
|
||||
```
|
||||
|
||||
If CHANGELOG.md and VERSION were NOT modified and the diff includes new features (new files, commands, skills): **WARNING — /document-release likely not run.** If only docs changed (no code): skip this check.
|
||||
|
||||
### 3.5e: Readiness report and confirmation
|
||||
|
||||
Build the readiness report:
|
||||
|
||||
```
|
||||
╔══════════════════════════════════════════════════════════╗
|
||||
║ PRE-MERGE READINESS REPORT ║
|
||||
╠══════════════════════════════════════════════════════════╣
|
||||
║ PR: #NNN — title ║
|
||||
║ Branch: feature → {base} ║
|
||||
║ Merge regime: none / github / trunk ║
|
||||
║ ║
|
||||
║ REVIEWS ║
|
||||
║ ├─ Eng Review: CURRENT / STALE (N commits) / — ║
|
||||
║ ├─ CEO Review: CURRENT / — (optional) ║
|
||||
║ ├─ Design Review: CURRENT / — (optional) ║
|
||||
║ └─ Codex Review: CURRENT / — (optional) ║
|
||||
║ ║
|
||||
║ TESTS ║
|
||||
║ ├─ Free tests: PASS / FAIL (blocker) ║
|
||||
║ ├─ E2E tests: N/N pass (Xm ago) / NOT RUN ║
|
||||
║ └─ LLM evals: PASS / NOT RUN ║
|
||||
║ ║
|
||||
║ DOCUMENTATION ║
|
||||
║ ├─ CHANGELOG: Updated / NOT UPDATED (warning) ║
|
||||
║ ├─ VERSION: X.Y.Z.W / NOT BUMPED (warning) ║
|
||||
║ └─ PR body: Current / STALE (warning) ║
|
||||
║ ║
|
||||
║ WARNINGS: N | BLOCKERS: N ║
|
||||
╚══════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**`--fast` handling:**
|
||||
- If `--fast` AND there are **zero blockers**: print the report, then proceed to Step 4 WITHOUT asking. Print "Fast mode: no blockers, landing without the confirmation prompt."
|
||||
- If there are any **blockers** (failing free tests): `--fast` does NOT apply — list the blockers and recommend B below. Never auto-proceed past a blocker.
|
||||
- Without `--fast`: always ask.
|
||||
|
||||
Use AskUserQuestion:
|
||||
- **Re-ground:** "Ready to merge PR #NNN — '{title}' into {base} via the {regime} regime. Here's what I found." Show the report.
|
||||
- If green: "All checks passed. Ready to merge."
|
||||
- If warnings: list each in plain English ("Eng review was 6 commits ago — code changed since then").
|
||||
- If blockers: "I found issues that must be fixed first: {list}"
|
||||
- **RECOMMENDATION:** A if green; B if significant warnings; C only if the user accepts the risk.
|
||||
- A) Merge it — everything looks good (Completeness: 10/10)
|
||||
- B) Hold off — I want to fix the warnings first (Completeness: 10/10)
|
||||
- C) Merge anyway — I understand the warnings (Completeness: 3/10)
|
||||
|
||||
If B: **STOP** with specific next steps (run `/review`, run E2E, run `/document-release`, or fix the PR body).
|
||||
If A or C: "Merging now." Continue to Step 4.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Merge through the right regime
|
||||
|
||||
This is the heart of `/land`. The merge **command** depends on the regime, but the "did it land" **signal** is uniform — so a single helper, `bin/gstack-merge`, owns detection, submission, and the landing poll.
|
||||
|
||||
### 4.1: Resolve the merge regime
|
||||
|
||||
Resolution order (platform-agnostic rule — the project owns its config, gstack reads it):
|
||||
|
||||
1. **Explicit config** — read the `## Merge Configuration` section of CLAUDE.md for a `Merge queue: none|github|trunk` line.
|
||||
2. **Auto-detect** — if no config line, ask the helper:
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-merge detect --base <base> --json
|
||||
```
|
||||
It returns `{"regime":"none|github|trunk","source":"...","base":"..."}`. Detection uses the queue's own GitHub status check (`Trunk Merge Queue (<base>)` → trunk), branch-protection merge queue (→ github), and `.trunk/trunk.yaml` `merge:` as a secondary signal. A bare `.trunk/` directory is NOT treated as trunk (the `trunk check` linter uses the same dir).
|
||||
3. **Ask once, then persist** — if there is no config AND detection returns `none` but the user expected a queue (or detection is ambiguous), ask via AskUserQuestion which regime to use, then write a `## Merge Configuration` section to CLAUDE.md so we never ask again. (You can also point them at `/setup-deploy`, which writes this section.)
|
||||
|
||||
Tell the user which regime you'll use and why: e.g. "Your repo uses the trunk.io merge queue (detected from the `Trunk Merge Queue (main)` check). I'll enqueue the PR and watch the queue."
|
||||
|
||||
Record the start timestamp.
|
||||
|
||||
### 4.2: Submit
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-merge submit --regime <regime> --pr <NNN> --base <base>
|
||||
```
|
||||
|
||||
What the helper does per regime:
|
||||
- **none** → `gh pr merge <pr> --squash --delete-branch`
|
||||
- **github** → `gh pr merge <pr> --auto --delete-branch` (GitHub auto-merge / native queue; falls back to a direct squash if `--auto` is not enabled)
|
||||
- **trunk** → **comment-first**: `gh pr comment <pr> --body "/trunk merge"` (zero new auth — works the moment Trunk's GitHub App is installed), then the `trunk` CLI if installed, then the Trunk REST API if `$TRUNK_API_TOKEN` is set. NEVER `gh pr merge`, NEVER `--delete-branch` — Trunk owns the merge and branch cleanup.
|
||||
|
||||
If the user wants priority on a trunk queue, pass `--priority <urgent|high|medium|low|lowest>`.
|
||||
|
||||
### 4.2a: Post-failure PR-state check
|
||||
|
||||
**Universal invariant:** after ANY non-zero exit from `gh pr merge` (the `none`/`github`
|
||||
submit paths), query authoritative PR state before retrying or stopping. Do NOT retry
|
||||
`gh pr merge`. Related: cli/cli#3442, cli/cli#13380. (For the `trunk` path, the same
|
||||
no-blind-retry rule applies to `submit` per H4 — never resubmit a failed `/trunk merge`;
|
||||
check status first.)
|
||||
|
||||
```bash
|
||||
gh pr view --json state,mergeCommit,mergedAt,mergedBy
|
||||
```
|
||||
|
||||
**If `state == "MERGED"`:**
|
||||
|
||||
The server-side merge succeeded (it may have completed before the local cleanup phase failed, or a concurrent merge landed). Tell the user: "PR is merged on GitHub." (Do NOT say "the merge succeeded" — this also covers the concurrent-merge case.)
|
||||
|
||||
Capture the merge SHA:
|
||||
```bash
|
||||
gh pr view --json mergeCommit -q .mergeCommit.oid
|
||||
```
|
||||
|
||||
Worktree cleanup — non-destructive, candidate-based:
|
||||
```bash
|
||||
git worktree list --porcelain
|
||||
```
|
||||
A worktree is a stale candidate if (a) it is checked out on the base branch, AND (b) it is not the user's primary working tree, AND (c) `git status --porcelain` inside it is empty.
|
||||
- For each clean candidate: OFFER to remove it ("There's a stale worktree at `<path>` on `<branch>` with no uncommitted work. Remove it?"). Remove only on confirmation (`git worktree remove <path> && git worktree prune`).
|
||||
- If any candidate has uncommitted work: list the files, tell the user, and STOP worktree cleanup without removing anything.
|
||||
- Do NOT use `--force`. Do NOT remove the user's primary working tree.
|
||||
|
||||
Then continue to the landing confirmation (Step 5) — `write-state` will confirm the SHA.
|
||||
|
||||
**If `state == "OPEN"`:**
|
||||
|
||||
Check whether auto-merge / a queue is active:
|
||||
```bash
|
||||
gh pr view --json autoMergeRequest -q .autoMergeRequest
|
||||
```
|
||||
- If non-null: auto-merge is enabled or merge queue is in use. The open state is expected — continue to 4.3's wait.
|
||||
- If null: genuine failure. Surface both the `submit` stderr AND the current PR open state, then **STOP**.
|
||||
|
||||
**If `state == "CLOSED"`:** the PR was closed without merging. **STOP.**
|
||||
|
||||
**Hard rule: never call `gh pr merge` a second time** after a non-zero exit. Server state is authoritative.
|
||||
|
||||
### 4.3: Wait for it to land
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-merge wait --regime <regime> --pr <NNN> --base <base>
|
||||
```
|
||||
|
||||
The helper polls the uniform landing signal (`gh pr view state` + the merge-queue status check) and prints progress. For the trunk regime it first confirms the PR was actually picked up (the `Trunk Merge Queue (<base>)` check appears) — a posted `/trunk merge` comment is silently inert if the GitHub App isn't installed.
|
||||
|
||||
Handle the exit:
|
||||
- **exit 0 / `LAND_STATUS=landed`** — it merged. Continue to Step 5.
|
||||
- **`LAND_STATUS=ejected`** — the queue rejected the PR (a CI check failed on the merge candidate, or a conflict with another queued PR). **STOP.** "The merge queue ejected this PR: {reason}. Check the queue page — usually a check failed on the merge commit. Fix and run `/land` again."
|
||||
- **`LAND_STATUS=closed`** — **STOP.** "The PR was closed without merging."
|
||||
- **`TRUNK_ENQUEUE_TIMEOUT`** — **STOP.** "I posted `/trunk merge` but Trunk never picked it up. Confirm the Trunk GitHub App is installed on this repo and 'GitHub commands' is enabled, then run `/land` again."
|
||||
- **timeout** — **STOP.** "The merge has been pending for {duration}. Something may be stuck — check the GitHub Actions tab and the merge-queue page."
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Confirm landing and write the handoff
|
||||
|
||||
A merge isn't done until the commit is on the base branch with a known SHA. This is also the **handoff** the deploy half needs (its `git revert` and deploy-workflow match both need the merge SHA), so `/land` writes it as a file, not just a log line.
|
||||
|
||||
```bash
|
||||
{{SLUG_EVAL}}
|
||||
~/.claude/skills/gstack/bin/gstack-merge write-state --regime <regime> --pr <NNN> --base <base> --slug "$SLUG"
|
||||
```
|
||||
|
||||
The helper polls until the PR is `MERGED` with a non-null merge SHA (the SHA can lag the state flip on squash/queue merges), verifies the commit is actually on `origin/<base>`, then atomically writes `~/.gstack/projects/$SLUG/last-land.json`:
|
||||
|
||||
```json
|
||||
{"schema_version":1,"pr":NNN,"sha":"<oid>","headRefOid":"<oid>","base":"<branch>","head_branch":"<branch>","repo":"owner/name","regime":"<regime>","ts":"<ISO>"}
|
||||
```
|
||||
|
||||
and prints a human echo: `LANDED: pr=#NNN sha=<oid> regime=<regime> base=<branch>`.
|
||||
|
||||
- **If `write-state` exits non-zero:** landing could not be confirmed. **STOP** and do NOT report success: "The PR shows as merged but I couldn't confirm the commit on {base} / capture a merge SHA. Don't deploy off this until you verify on GitHub."
|
||||
- **If it succeeds:** the PR has truly landed and the handoff file is written.
|
||||
|
||||
> When this skill is composed by `/land-and-deploy`, that skill reads `last-land.json` after this step (validating it is for this exact PR + repo and recent) and uses the SHA for deploy matching and revert.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Land summary
|
||||
|
||||
When run standalone, print a short summary (skip the deploy framing — there is none here):
|
||||
|
||||
```
|
||||
LAND REPORT
|
||||
═══════════
|
||||
PR: #<number> — <title>
|
||||
Branch: <head> → <base>
|
||||
Regime: <none / github / trunk>
|
||||
Merged: <timestamp>
|
||||
Merge SHA: <sha>
|
||||
CI: <PASSED / SKIPPED>
|
||||
Reviews: <Eng: CURRENT/STALE/NOT RUN; inline fix: yes(N)/no/skipped>
|
||||
|
||||
VERDICT: LANDED
|
||||
```
|
||||
|
||||
Then suggest the natural next step: "Want to deploy and verify this in production? Run `/land-and-deploy` — it'll pick up this landing and take it through deploy + canary." (Skip this suggestion when `/land` was invoked by `/land-and-deploy` — it already continues to deploy.)
|
||||
|
||||
---
|
||||
|
||||
## Important Rules
|
||||
|
||||
- **Never force push.** Use `gh pr merge` / the queue — never a manual push to the base branch.
|
||||
- **Never skip CI.** Failing or pending checks gate the merge.
|
||||
- **Never call `gh pr merge` twice** after a non-zero exit — server state is authoritative (see 4.2). Related: cli/cli#3442, cli/cli#13380.
|
||||
- **Trunk owns the trunk path.** In the trunk regime, never run `gh pr merge` and never pass `--delete-branch`.
|
||||
- **Landing means a SHA on the base branch.** Don't report success until `write-state` confirms it (Step 5). A null SHA silently kills the deploy half's revert.
|
||||
- **Detect, don't assume.** Resolve the regime from config → live detection → ask-once-and-persist. The same `gstack-merge detect` is what `/land-and-deploy`'s dry-run uses, so the two never disagree.
|
||||
- **Narrate the journey.** The user should always know what just happened, what's happening now, and what's next.
|
||||
|
|
@ -143,6 +143,11 @@
|
|||
"routing": "Updates StateServer.swift, DebugOverlay.swift, Package.swift,\nand the typed @Observable state accessors. Use after you upgrade gstack\nor add new ViewModels/properties that need accessor coverage.\nUse when asked to \"resync the iOS debug bridge\", \"regenerate iOS\naccessors\", or \"update the gstack iOS instrumentation\".",
|
||||
"voice_line": "Voice triggers (speech-to-text aliases): \"resync the iOS debug bridge\", \"regenerate iOS accessors\", \"update the gstack iOS instrumentation\"."
|
||||
},
|
||||
"land": {
|
||||
"lead": "Land a PR through the right merge regime: pre-flight, CI wait, VERSION-drift check, pre-merge readiness gate, then merge via no-queue,",
|
||||
"routing": "GitHub native merge\nqueue, or trunk.io merge queue. This is the \"land\" half of /land-and-deploy,\nusable on its own when you want to merge but not deploy. Use when: \"land\",\n\"land the pr\", \"land it\", \"merge\", \"merge the pr\", \"merge it\", \"get it merged\".\nFor deploy + canary verification after landing, use /land-and-deploy.",
|
||||
"voice_line": null
|
||||
},
|
||||
"land-and-deploy": {
|
||||
"lead": "Land and deploy workflow.",
|
||||
"routing": "Merges the PR, waits for CI and deploy,\nverifies production health via canary checks. Takes over after /ship\ncreates the PR. Use when: \"merge\", \"land\", \"deploy\", \"merge and verify\",\n\"land it\", \"ship it to production\".",
|
||||
|
|
|
|||
Loading…
Reference in New Issue