Initial commit
This commit is contained in:
646
commands/merge_pr.md
Normal file
646
commands/merge_pr.md
Normal file
@@ -0,0 +1,646 @@
|
||||
---
|
||||
description: Safely merge PR with verification and Linear integration
|
||||
category: version-control-git
|
||||
tools: Bash(linearis *), Bash(git *), Bash(gh *), Read
|
||||
model: inherit
|
||||
version: 1.0.0
|
||||
---
|
||||
|
||||
# Merge Pull Request
|
||||
|
||||
Safely merges a PR after comprehensive verification, with Linear integration and automated cleanup.
|
||||
|
||||
## Configuration
|
||||
|
||||
Read team configuration from `.claude/config.json`:
|
||||
|
||||
```bash
|
||||
CONFIG_FILE=".claude/config.json"
|
||||
TEAM_KEY=$(jq -r '.catalyst.linear.teamKey // "PROJ"' "$CONFIG_FILE")
|
||||
TEST_CMD=$(jq -r '.catalyst.pr.testCommand // "make test"' "$CONFIG_FILE")
|
||||
```
|
||||
|
||||
## Process:
|
||||
|
||||
### 1. Identify PR to merge
|
||||
|
||||
**If argument provided:**
|
||||
|
||||
- Use that PR number: `/merge_pr 123`
|
||||
|
||||
**If no argument:**
|
||||
|
||||
```bash
|
||||
# Try current branch
|
||||
gh pr view --json number,url,title,state,mergeable 2>/dev/null
|
||||
```
|
||||
|
||||
If no PR on current branch:
|
||||
|
||||
```bash
|
||||
gh pr list --limit 10 --json number,title,headRefName,state
|
||||
```
|
||||
|
||||
Ask: "Which PR would you like to merge? (enter number)"
|
||||
|
||||
### 2. Get PR details
|
||||
|
||||
```bash
|
||||
gh pr view $pr_number --json \
|
||||
number,url,title,state,mergeable,mergeStateStatus,\
|
||||
baseRefName,headRefName,reviewDecision
|
||||
```
|
||||
|
||||
**Extract:**
|
||||
|
||||
- PR number, URL, title
|
||||
- Mergeable status
|
||||
- Base branch (usually main)
|
||||
- Head branch (feature branch)
|
||||
- Review decision (APPROVED, REVIEW_REQUIRED, etc.)
|
||||
|
||||
### 3. Verify PR is open and mergeable
|
||||
|
||||
```bash
|
||||
state=$(gh pr view $pr_number --json state -q .state)
|
||||
mergeable=$(gh pr view $pr_number --json mergeable -q .mergeable)
|
||||
```
|
||||
|
||||
**If PR not OPEN:**
|
||||
|
||||
```
|
||||
❌ PR #$pr_number is $state
|
||||
|
||||
Only open PRs can be merged.
|
||||
```
|
||||
|
||||
**If not mergeable (CONFLICTING):**
|
||||
|
||||
```
|
||||
❌ PR has merge conflicts
|
||||
|
||||
Resolve conflicts first:
|
||||
gh pr checkout $pr_number
|
||||
git fetch origin $base_branch
|
||||
git merge origin/$base_branch
|
||||
# ... resolve conflicts ...
|
||||
git push
|
||||
```
|
||||
|
||||
Exit with error.
|
||||
|
||||
### 4. Check if head branch is up-to-date with base
|
||||
|
||||
```bash
|
||||
# Checkout PR branch
|
||||
gh pr checkout $pr_number
|
||||
|
||||
# Fetch latest base
|
||||
base_branch=$(gh pr view $pr_number --json baseRefName -q .baseRefName)
|
||||
git fetch origin $base_branch
|
||||
|
||||
# Check if behind
|
||||
if git log HEAD..origin/$base_branch --oneline | grep -q .; then
|
||||
echo "Branch is behind $base_branch"
|
||||
fi
|
||||
```
|
||||
|
||||
**If behind:**
|
||||
|
||||
```bash
|
||||
# Auto-rebase
|
||||
git rebase origin/$base_branch
|
||||
|
||||
# Check for conflicts
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ Rebase conflicts"
|
||||
git rebase --abort
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Push rebased branch
|
||||
git push --force-with-lease
|
||||
```
|
||||
|
||||
**If conflicts during rebase:**
|
||||
|
||||
```
|
||||
❌ Rebase conflicts detected
|
||||
|
||||
Conflicting files:
|
||||
$(git diff --name-only --diff-filter=U)
|
||||
|
||||
Resolve manually:
|
||||
1. Fix conflicts in listed files
|
||||
2. git add <resolved-files>
|
||||
3. git rebase --continue
|
||||
4. git push --force-with-lease
|
||||
5. Run /catalyst-dev:merge_pr again
|
||||
```
|
||||
|
||||
Exit with error.
|
||||
|
||||
### 5. Run local tests
|
||||
|
||||
**Read test command from config:**
|
||||
|
||||
```bash
|
||||
test_cmd=$(jq -r '.catalyst.pr.testCommand // "make test"' .claude/config.json)
|
||||
```
|
||||
|
||||
**Execute tests:**
|
||||
|
||||
```bash
|
||||
echo "Running tests: $test_cmd"
|
||||
if ! $test_cmd; then
|
||||
echo "❌ Tests failed"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
**If tests fail:**
|
||||
|
||||
```
|
||||
❌ Local tests failed
|
||||
|
||||
Fix failing tests before merge:
|
||||
$test_cmd
|
||||
|
||||
Or skip tests (not recommended):
|
||||
/catalyst-dev:merge_pr $pr_number --skip-tests
|
||||
```
|
||||
|
||||
Exit with error (unless `--skip-tests` flag provided).
|
||||
|
||||
### 6. Check CI/CD status
|
||||
|
||||
```bash
|
||||
gh pr checks $pr_number
|
||||
```
|
||||
|
||||
**Parse output for failures:**
|
||||
|
||||
- If all checks pass: continue
|
||||
- If required checks fail: prompt user
|
||||
- If optional checks fail: warn but allow
|
||||
|
||||
**If required checks failing:**
|
||||
|
||||
```
|
||||
⚠️ Some required CI checks are failing
|
||||
|
||||
Failed checks:
|
||||
- build (required)
|
||||
- lint (required)
|
||||
|
||||
Passed checks:
|
||||
- test ✅
|
||||
- security ✅
|
||||
|
||||
Continue merge anyway? [y/N]:
|
||||
```
|
||||
|
||||
If user says no: exit. If user says yes: continue (user override).
|
||||
|
||||
### 7. Check approval status
|
||||
|
||||
```bash
|
||||
review_decision=$(gh pr view $pr_number --json reviewDecision -q .reviewDecision)
|
||||
```
|
||||
|
||||
**Review decisions:**
|
||||
|
||||
- `APPROVED` - proceed
|
||||
- `CHANGES_REQUESTED` - prompt user
|
||||
- `REVIEW_REQUIRED` - prompt user
|
||||
- `null` / empty - no reviews, prompt user
|
||||
|
||||
**If not approved:**
|
||||
|
||||
```
|
||||
⚠️ PR has not been approved
|
||||
|
||||
Review status: $review_decision
|
||||
|
||||
Continue merge anyway? [y/N]:
|
||||
```
|
||||
|
||||
If user says no: exit. If user says yes: continue (user override).
|
||||
|
||||
**Skip these prompts if** `requireApproval: false` in config.
|
||||
|
||||
### 8. Extract ticket reference
|
||||
|
||||
```bash
|
||||
branch=$(gh pr view $pr_number --json headRefName -q .headRefName)
|
||||
title=$(gh pr view $pr_number --json title -q .title)
|
||||
|
||||
# From branch using configured team key
|
||||
if [[ "$branch" =~ ($TEAM_KEY-[0-9]+) ]]; then
|
||||
ticket="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
|
||||
# From title if not in branch
|
||||
if [[ -z "$ticket" ]] && [[ "$title" =~ ($TEAM_KEY-[0-9]+) ]]; then
|
||||
ticket="${BASH_REMATCH[1]}"
|
||||
fi
|
||||
```
|
||||
|
||||
### 9. Show merge summary
|
||||
|
||||
```
|
||||
About to merge:
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
PR: #$pr_number - $title
|
||||
From: $head_branch
|
||||
To: $base_branch
|
||||
Commits: $commit_count
|
||||
Files: $file_count changed
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
Reviews: $review_status
|
||||
CI: $ci_status
|
||||
Tests: ✅ Passed locally
|
||||
Ticket: $ticket (will move to Done)
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Merge strategy: Squash and merge
|
||||
|
||||
Proceed? [Y/n]:
|
||||
```
|
||||
|
||||
### 10. Execute squash merge
|
||||
|
||||
```bash
|
||||
gh pr merge $pr_number --squash --delete-branch
|
||||
```
|
||||
|
||||
**Always:**
|
||||
|
||||
- Squash merge (combines all commits into one)
|
||||
- Delete remote branch automatically
|
||||
|
||||
**Capture merge commit SHA:**
|
||||
|
||||
```bash
|
||||
merge_sha=$(git rev-parse HEAD)
|
||||
```
|
||||
|
||||
### 11. Update Linear ticket
|
||||
|
||||
If ticket found and not using `--no-update`:
|
||||
|
||||
```bash
|
||||
# Verify linearis is available
|
||||
if ! command -v linearis &> /dev/null; then
|
||||
echo "⚠️ Linearis CLI not found - skipping Linear ticket update"
|
||||
echo "Install: npm install -g --install-links ryanrozich/linearis#feat/cycles-cli"
|
||||
else
|
||||
# Move to "Done"
|
||||
linearis issues update "$ticket" --state "Done"
|
||||
|
||||
# Add merge comment
|
||||
linearis comments create "$ticket" \
|
||||
--body "✅ PR merged!\n\n**PR**: #${prNumber} - ${prTitle}\n**Merge commit**: ${mergeSha}\n**Merged into**: ${baseBranch}\n\nView PR: ${prUrl}"
|
||||
fi
|
||||
```
|
||||
|
||||
### 12. Delete local branch and update base
|
||||
|
||||
```bash
|
||||
# Switch to base branch
|
||||
git checkout $base_branch
|
||||
|
||||
# Pull latest (includes merge commit)
|
||||
git pull origin $base_branch
|
||||
|
||||
# Delete local feature branch
|
||||
git branch -d $head_branch
|
||||
|
||||
# Confirm deletion
|
||||
echo "✅ Deleted local branch: $head_branch"
|
||||
```
|
||||
|
||||
**Always delete local branch** - no prompt (remote already deleted).
|
||||
|
||||
### 13. Extract post-merge tasks
|
||||
|
||||
**Read PR description:**
|
||||
|
||||
```bash
|
||||
desc_file="thoughts/shared/prs/${pr_number}_description.md"
|
||||
if [ -f "$desc_file" ]; then
|
||||
# Extract "Post-Merge Tasks" section
|
||||
tasks=$(sed -n '/## Post-Merge Tasks/,/^##/p' "$desc_file" | grep -E '^\- \[')
|
||||
fi
|
||||
```
|
||||
|
||||
**If tasks exist:**
|
||||
|
||||
```
|
||||
📋 Post-merge tasks from PR description:
|
||||
- [ ] Update documentation
|
||||
- [ ] Monitor error rates in production
|
||||
- [ ] Notify stakeholders
|
||||
|
||||
Save these tasks? [Y/n]:
|
||||
```
|
||||
|
||||
If yes:
|
||||
|
||||
```bash
|
||||
# Save to thoughts
|
||||
cat > "thoughts/shared/post_merge_tasks/${ticket}_tasks.md" <<EOF
|
||||
# Post-Merge Tasks: $ticket
|
||||
|
||||
Merged: $(date)
|
||||
PR: #$pr_number
|
||||
|
||||
$tasks
|
||||
EOF
|
||||
|
||||
humanlayer thoughts sync
|
||||
```
|
||||
|
||||
### 14. Report success summary
|
||||
|
||||
```
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
✅ PR #$pr_number merged successfully!
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
|
||||
Merge details:
|
||||
Strategy: Squash and merge
|
||||
Commit: $merge_sha
|
||||
Base branch: $base_branch (updated)
|
||||
Merged by: @$user
|
||||
|
||||
Cleanup:
|
||||
Remote branch: $head_branch (deleted)
|
||||
Local branch: $head_branch (deleted)
|
||||
|
||||
Linear:
|
||||
Ticket: $ticket → Done ✅
|
||||
Comment: Added with merge details
|
||||
|
||||
Post-merge tasks: $task_count saved to thoughts/
|
||||
|
||||
Next steps:
|
||||
- Monitor deployment
|
||||
- Check CI/CD pipeline
|
||||
- Verify in production
|
||||
|
||||
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
||||
```
|
||||
|
||||
## Flags
|
||||
|
||||
**`--skip-tests`** - Skip local test execution
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 123 --skip-tests
|
||||
```
|
||||
|
||||
**`--no-update`** - Don't update Linear ticket
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 123 --no-update
|
||||
```
|
||||
|
||||
**`--keep-branch`** - Don't delete local branch
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 123 --keep-branch
|
||||
```
|
||||
|
||||
**Combined:**
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 123 --skip-tests --no-update
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
**Rebase conflicts:**
|
||||
|
||||
```
|
||||
❌ Rebase conflicts detected
|
||||
|
||||
Conflicting files:
|
||||
- src/app.ts
|
||||
- tests/app.test.ts
|
||||
|
||||
Resolve manually:
|
||||
gh pr checkout $pr_number
|
||||
git fetch origin $base_branch
|
||||
git rebase origin/$base_branch
|
||||
# Fix conflicts
|
||||
git add <files>
|
||||
git rebase --continue
|
||||
git push --force-with-lease
|
||||
/catalyst-dev:merge_pr $pr_number
|
||||
```
|
||||
|
||||
**Tests failing:**
|
||||
|
||||
```
|
||||
❌ Tests failed (exit code 1)
|
||||
|
||||
Failed tests:
|
||||
- validation.test.ts:45 - Expected true but got false
|
||||
- auth.test.ts:12 - Timeout exceeded
|
||||
|
||||
Fix tests or skip (not recommended):
|
||||
/catalyst-dev:merge_pr $pr_number --skip-tests
|
||||
```
|
||||
|
||||
**CI checks failing:**
|
||||
|
||||
```
|
||||
⚠️ Required CI checks failing
|
||||
|
||||
Failed:
|
||||
- build: Compilation error in src/types.ts
|
||||
- security: Dependency vulnerability found
|
||||
|
||||
You can:
|
||||
1. Fix issues and try again
|
||||
2. Override and merge anyway (not recommended)
|
||||
|
||||
Override? [y/N]:
|
||||
```
|
||||
|
||||
**Linearis CLI not found:**
|
||||
|
||||
```
|
||||
⚠️ Linearis CLI not found
|
||||
|
||||
PR merged successfully, but Linear ticket not updated.
|
||||
|
||||
Install Linearis:
|
||||
npm install -g --install-links ryanrozich/linearis#feat/cycles-cli
|
||||
|
||||
Configure:
|
||||
export LINEAR_API_TOKEN=your_token
|
||||
|
||||
Then update ticket manually:
|
||||
linearis issues update $ticket --state "Done"
|
||||
```
|
||||
|
||||
**Linear API error:**
|
||||
|
||||
```
|
||||
⚠️ Could not update Linear ticket $ticket
|
||||
|
||||
Error: Ticket not found or API unavailable
|
||||
|
||||
PR merged successfully, but ticket status not updated.
|
||||
Update manually in Linear.
|
||||
```
|
||||
|
||||
**Branch deletion error:**
|
||||
|
||||
```
|
||||
⚠️ Could not delete local branch $head_branch
|
||||
|
||||
Error: Branch has unpushed commits
|
||||
|
||||
This won't affect the merge (already complete).
|
||||
Delete manually: git branch -D $head_branch
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Uses `.claude/config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"catalyst": {
|
||||
"project": {
|
||||
"ticketPrefix": "RCW"
|
||||
},
|
||||
"linear": {
|
||||
"teamKey": "RCW",
|
||||
"doneStatusName": "Done"
|
||||
},
|
||||
"pr": {
|
||||
"defaultMergeStrategy": "squash",
|
||||
"deleteRemoteBranch": true,
|
||||
"deleteLocalBranch": true,
|
||||
"updateLinearOnMerge": true,
|
||||
"requireApproval": false,
|
||||
"requireCI": false,
|
||||
"testCommand": "make test"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
**Happy path (all checks pass):**
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 123
|
||||
|
||||
Running tests: make test
|
||||
✅ All tests passed
|
||||
✅ CI checks passed
|
||||
✅ PR approved
|
||||
|
||||
About to merge PR #123...
|
||||
[shows summary]
|
||||
Proceed? Y
|
||||
|
||||
✅ Merged!
|
||||
✅ Linear ticket RCW-13 → Done
|
||||
✅ Branches deleted
|
||||
```
|
||||
|
||||
**With failing CI (user override):**
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 124
|
||||
|
||||
⚠️ Some CI checks failing
|
||||
Continue anyway? y
|
||||
|
||||
✅ Merged (with overrides)
|
||||
```
|
||||
|
||||
**Skip tests:**
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 125 --skip-tests
|
||||
|
||||
⚠️ Skipping tests (not recommended)
|
||||
✅ Merged!
|
||||
```
|
||||
|
||||
**Linearis not installed:**
|
||||
|
||||
```bash
|
||||
/catalyst-dev:merge_pr 126
|
||||
|
||||
✅ PR merged successfully!
|
||||
⚠️ Linearis CLI not found - Linear ticket not updated
|
||||
|
||||
Install Linearis to enable automatic ticket updates.
|
||||
```
|
||||
|
||||
## Safety Features
|
||||
|
||||
**Fail fast on:**
|
||||
|
||||
- Merge conflicts (can't auto-resolve)
|
||||
- Test failures (unless --skip-tests)
|
||||
- Rebase conflicts
|
||||
- PR not in mergeable state
|
||||
|
||||
**Prompt for confirmation on:**
|
||||
|
||||
- Missing required approvals
|
||||
- Failing CI checks
|
||||
- Any exceptional circumstance
|
||||
|
||||
**Always automated:**
|
||||
|
||||
- Rebase if behind (no conflicts)
|
||||
- Squash merge
|
||||
- Delete remote branch
|
||||
- Delete local branch
|
||||
- Update Linear to Done (if Linearis available)
|
||||
- Pull latest base branch
|
||||
|
||||
**Graceful degradation:**
|
||||
|
||||
- If Linearis not installed, warn but continue
|
||||
- Merge succeeds regardless of Linear integration
|
||||
|
||||
## Post-Merge Workflow
|
||||
|
||||
```
|
||||
PR merged
|
||||
↓
|
||||
Linear ticket → Done (if Linearis available)
|
||||
↓
|
||||
Branches deleted
|
||||
↓
|
||||
Base branch updated locally
|
||||
↓
|
||||
Post-merge tasks extracted
|
||||
↓
|
||||
Monitor deployment
|
||||
```
|
||||
|
||||
## Remember:
|
||||
|
||||
- **Always squash merge** - clean history
|
||||
- **Always delete branches** - no orphan branches
|
||||
- **Always run tests** - unless explicitly skipped
|
||||
- **Auto-rebase** - keep up-to-date with base
|
||||
- **Fail fast** - stop on conflicts or test failures
|
||||
- **Update Linear** - move ticket to Done automatically (if Linearis available)
|
||||
- **Extract tasks** - save post-merge checklist
|
||||
- **Clear summary** - show what happened
|
||||
- **Only prompt for exceptions** - approvals missing, CI failing
|
||||
- **Graceful degradation** - Work without Linearis if needed
|
||||
Reference in New Issue
Block a user