Skip to content

Commit ca01d80

Browse files
using-systemclaude
andcommitted
fix(skills): harden github-complete-pr force-delete and full recipe
Rule §4 now distinguishes MERGED from CLOSED (unmerged) when allowing force-delete, and calls out the data-loss tradeoff for closed branches whose commits live nowhere else. Full Recipe now enforces Rule §1 by capturing PR state and headRefName from `gh pr view` and aborting when state is OPEN, refuses to delete default/protected branches, and only force-deletes after state has been validated (no more unconditional `|| git branch -D`). Deriving FEATURE_BRANCH from the PR prevents a mistyped argument from targeting the wrong branch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1cf35e2 commit ca01d80

1 file changed

Lines changed: 44 additions & 9 deletions

File tree

skills/github-complete-pr/SKILL.md

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,20 @@ Use `--ff-only` to avoid accidental merge commits if the local default branch ha
5858
git branch -d <feature-branch>
5959
```
6060

61-
Use `-d` (safe delete) first. If Git refuses because the branch is not fully merged into the default branch (common when the PR was squash-merged), confirm the PR was actually merged via `gh pr view`, then force the delete:
61+
Use `-d` (safe delete) first. Git will refuse when the branch is not fully merged into the default branch — this is common and expected when the PR was squash-merged or rebase-merged (the tip commit of the feature branch is not an ancestor of `main`).
62+
63+
Force-delete with `-D` is acceptable **only** when one of the following is true, confirmed via `gh pr view`:
64+
65+
- `state` is `MERGED` — the changes exist on the default branch under a new commit hash; the original commits are redundant.
66+
- `state` is `CLOSED` **and** the branch was intentionally abandoned (the user confirms the work will not be reused).
6267

6368
```bash
6469
git branch -D <feature-branch>
6570
```
6671

67-
Never use `-D` without first confirming PR merge/close state.
72+
**Data-loss warning.** For a `CLOSED` (unmerged) PR, `-D` permanently discards any commits on that branch that live nowhere else — there is no remote copy to recover from once the remote branch is also deleted in step 5. Confirm with the user before force-deleting a closed-unmerged branch.
73+
74+
Never use `-D` without first confirming PR state via `gh pr view`.
6875

6976
### 5. Delete the remote branch if it still exists
7077

@@ -94,24 +101,52 @@ The feature branch must be absent from both.
94101

95102
## Full Recipe
96103

104+
The recipe below enforces Rule §1 (abort when `state` is `OPEN`) and Rule §4 (only force-delete after state validation), and derives `FEATURE_BRANCH` from the PR itself so a mistyped argument cannot cause the wrong branch to be deleted.
105+
97106
```bash
98-
# 1. Verify PR state
99-
gh pr view <branch-or-number> --json state,headRefName
107+
PR=<branch-or-number>
108+
109+
# 1. Verify PR state and capture the exact head branch from the PR
110+
PR_STATE=$(gh pr view "$PR" --json state -q .state)
111+
FEATURE_BRANCH=$(gh pr view "$PR" --json headRefName -q .headRefName)
112+
113+
if [ -z "$PR_STATE" ] || [ -z "$FEATURE_BRANCH" ]; then
114+
echo "Could not determine PR state or head branch; aborting."
115+
exit 1
116+
fi
100117

101-
# 2. Find default branch
118+
if [ "$PR_STATE" = "OPEN" ]; then
119+
echo "PR is still OPEN; refusing to delete anything."
120+
exit 1
121+
fi
122+
123+
# 2. Find default branch and refuse protected branches
102124
DEFAULT=$(gh repo view --json defaultBranchRef -q .defaultBranchRef.name)
103125

126+
if [ "$FEATURE_BRANCH" = "$DEFAULT" ] \
127+
|| [ "$FEATURE_BRANCH" = "main" ] \
128+
|| [ "$FEATURE_BRANCH" = "master" ] \
129+
|| [ "$FEATURE_BRANCH" = "develop" ] \
130+
|| [[ "$FEATURE_BRANCH" == release/* ]]; then
131+
echo "Refusing to delete protected or default branch: $FEATURE_BRANCH"
132+
exit 1
133+
fi
134+
104135
# 3. Switch + sync
105136
git checkout "$DEFAULT"
106137
git pull --ff-only origin "$DEFAULT"
107138

108-
# 4. Delete local branch (fallback to -D after confirming merge)
109-
git branch -d <feature-branch> || git branch -D <feature-branch>
139+
# 4. Delete local branch — safe delete first, force-delete only after state has been validated above
140+
if ! git branch -d "$FEATURE_BRANCH"; then
141+
# For CLOSED (unmerged) PRs this discards commits that live nowhere else — confirm with the user first.
142+
echo "Safe delete failed (branch not fully merged). Force-deleting validated branch: $FEATURE_BRANCH"
143+
git branch -D "$FEATURE_BRANCH"
144+
fi
110145

111146
# 5. Prune + delete remote branch if still present
112147
git fetch --prune origin
113-
if git ls-remote --exit-code --heads origin <feature-branch> >/dev/null 2>&1; then
114-
git push origin --delete <feature-branch>
148+
if git ls-remote --exit-code --heads origin "$FEATURE_BRANCH" >/dev/null 2>&1; then
149+
git push origin --delete "$FEATURE_BRANCH"
115150
else
116151
echo "Remote branch already deleted"
117152
fi

0 commit comments

Comments
 (0)