Claude-skills open-pr
Create a GitHub PR with smart defaults, detailed body generation, and user confirmation
git clone https://github.com/mosamaasif/claude-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/mosamaasif/claude-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/open-pr" ~/.claude/skills/mosamaasif-claude-skills-open-pr && rm -rf "$T"
skills/open-pr/SKILL.md/open-pr — GitHub Pull Request Creator
Create a GitHub PR with inferred defaults, a detailed generated body, and mandatory user confirmation before creation. Project-agnostic — works with any repo. Supports PR templates, CODEOWNERS, conventional commits, monorepos, and multiple ticket systems out of the box.
Argument Parsing
Parse arguments from the skill invocation string. All are optional,
--flag value format:
| Arg | Alias | Default | Description |
|---|---|---|---|
| | Inferred (see Phase 2) | Target branch |
| | Current user | PR assignee |
| | Ask user | Reviewer(s), comma-separated |
| | Inferred from paths | Label(s), comma-separated |
| | None | Image paths for screenshots |
| | Ask user | Create as draft |
| Create as ready for review | ||
| | Auto-generated | Override title |
| | Extracted from branch | Override ticket ID |
Execution: 5 Phases
You MUST execute all 5 phases in order. Do NOT skip Phase 4 (confirmation).
Phase 1 — Preflight Checks
Run these checks in parallel where possible:
- Auth check: Run
. If not authenticated, stop and tell the user to rungh auth status
.gh auth login - Uncommitted changes: Run
. If output is non-empty, warn the user about uncommitted changes and ask whether to proceed or abort.git status --porcelain - Existing PR: Run
. If a PR already exists for this branch:gh pr view --json url,state 2>/dev/null- Show the existing PR URL and state
- Ask user: "A PR already exists. Would you like to update it with
instead?"gh pr edit - If yes, switch to edit flow (re-generate body, run
)gh pr edit - If no, abort
- Commits ahead: Run
(after base is determined in Phase 2). If empty, stop — nothing to open a PR for.git log {base}..HEAD --oneline
Optional Config File
Check for configuration files (strictly optional — skill works perfectly without them):
- Check
(repo-level config).github/open-pr.yml - Check
(user-level config)~/.config/open-pr.yml - Repo config overrides user config. CLI flags override both.
Supported config keys:
base: develop # default base branch draft: true # default draft status labels: path_mappings: # custom path→label mappings "packages/payments/": "payments" "libs/auth/": "auth" reviewers: default: ["alice"] # default reviewer suggestions use_codeowners: true # enable CODEOWNERS parsing (default: true) template: ".github/PULL_REQUEST_TEMPLATE/feature.md" # force a specific template body_footer: "QA: https://staging.example.com" # append to PR body
If config files are not found, proceed silently with built-in defaults. Never error on missing config.
CI Status Warning
After confirming the branch has been pushed (or will be pushed), run:
gh run list --branch {branch} --limit 3 --json status,conclusion,name 2>/dev/null
If any recent runs have
conclusion: "failure", warn: "Recent CI runs have failures: {names}. This won't block PR creation, but you may want to investigate."
This is advisory only — never block on CI status.
Phase 2 — Gather & Infer Parameters
Ticket ID
- If
provided, use it directly.--ticket - Otherwise extract from branch name using priority-ordered patterns:
- Jira/Linear:
— e.g.,([A-Z][A-Z0-9]+-\d+)
,DP-1067ENG-42 - Shortcut:
(case-insensitive) — e.g.,(sc-\d+)sc-12345 - Azure DevOps:
— e.g.,(AB#\d+)AB#7890 - GitHub issue:
in branch name, or bare number prefix like#(\d+)
→123-fix-bug#123 - HOTFIX:
(case-insensitive)(HOTFIX)
- Jira/Linear:
- Stop at the first match. If no match, ticket ID is empty — that's fine.
Also scan commit messages for issue references (
#\d+) to collect related issues for the body.
Base Branch
- If
provided, use it.--base - If config file specifies
, use that (unlessbase
was given).--base - Otherwise detect:
- Get the repo default branch:
gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name' - Check common candidates:
,main
,master
, plus anydevelop
branchesrelease/* - For each candidate that exists, compute
distancegit merge-base - Pick the closest ancestor (fewest commits between merge-base and HEAD)
- Fallback: repo default branch
- Get the repo default branch:
- Verify the base branch exists on the remote. If not, warn and fall back to the repo default branch.
Stacked PR detection: If the resolved base branch is not
main, master, develop, or release/*, note: "This appears to be a stacked PR targeting {base}." Include this note in the Phase 4 preview.
Assignee
- If
provided, use it.--assignee - Otherwise:
gh api user --jq '.login'
Reviewer
- If
provided, use it.--reviewer - Otherwise use a layered strategy, combining results from all available sources:
- CODEOWNERS (top priority): Check for
,.github/CODEOWNERS
, orCODEOWNERS
. If found, parse it and match changed file paths against patterns. Extract usernames/teams. CODEOWNERS format: each line isdocs/CODEOWNERS
. Match using glob patterns (last matching pattern wins per file, same as GitHub). If config haspattern @owner1 @owner2
, skip this step.reviewers.use_codeowners: false - Recent file authors: For the top 5 most-changed files (by diff stat), run
, deduplicate, exclude self.git log --format='%aN' -5 -- {file} - Recent PR reviewers (fallback):
, deduplicate, exclude self.gh pr list --limit 15 --state merged --json author,reviews --jq '.[].reviews[].author.login'
- Combine all results. Label each suggestion with its source:
- "alice (CODEOWNERS for server/)"
- "bob (recent author of auth.ts)"
- "carol (recent PR reviewer)"
- If config specifies
, include those with label "(default from config)".reviewers.default - Deduplicate across sources (keep the highest-priority label).
- Present the combined list to the user via AskUserQuestion (up to 6 options + "Other" + "Skip"). Let them pick one or more.
Labels
-
If
provided, use those directly.--label -
Otherwise:
- Fetch available labels:
gh label list --json name --jq '.[].name' - If no labels exist in the repo, skip label inference entirely.
- Get changed file paths:
git diff --name-only {base}..HEAD - Infer labels using a dynamic matching strategy:
Config-based mappings (if config has
):labels.path_mappings- Match changed paths against configured path prefixes. Use the mapped label name if it exists in the repo.
Dynamic directory-to-label matching:
- Extract directory names from changed file paths (e.g.,
→packages/payments/src/foo.ts
,packages
,payments
)src - For each repo label, check if its name (case-insensitive) is a substring of any directory name in the changed paths, or vice versa
- E.g., label
matches paths underpayments
; labelpackages/payments/
matches paths underauthlibs/auth/
Universal fallback heuristics (always applied):
files or paths under*.md
→ labels containing "doc"docs/- Paths under
,test/
,tests/
, or files matching__tests__/
,*test*
→ labels containing "test"*spec* - Paths under
,.github/
,.circleci/
,ci/
,.gitlab-ci*
,Jenkinsfile
,Dockerfile
,docker-compose*
,terraform/
→ labels containing "ci", "infra", or "devops"infra/
Size labels: Calculate total lines changed from
. If the repo has labels matchinggit diff --stat
,size/XS
,size/S
,size/M
,size/L
(case-insensitive):size/XL- XS: <10 lines, S: <50, M: <200, L: <500, XL: 500+
- Auto-suggest the matching size label.
Monorepo package matching: If monorepo detected (see Phase 3), match package/workspace names against available labels.
- Present inferred labels to user for confirmation. Let them add/remove before proceeding.
- If no labels match, proceed without labels.
- Fetch available labels:
Draft Status
- If
provided → draft = true--draft - If
provided → draft = false--no-draft - If config specifies
, use that as default.draft - Otherwise: ask the user via AskUserQuestion: "Create as draft?" with options "Yes (draft)" and "No (ready for review)"
Phase 3 — Analyze Changes & Generate PR Content
Collect Change Data (run in parallel)
— commit listgit log {base}..HEAD --pretty=format:'%h %s'
— file change statsgit diff --stat {base}..HEAD
— full diff (only if <5000 lines; check withgit diff {base}..HEAD
first)git diff {base}..HEAD | wc -l- If diff is >5000 lines, warn the user and rely on commit messages + stats only.
- Count changed files: if >50, warn about large PR.
Size Classification
Calculate total lines changed (additions + deletions from
git diff --stat). Classify:
- XS: <10 lines
- S: <50 lines
- M: <200 lines
- L: <500 lines
- XL: 500+ lines
Include this in the Phase 4 preview. If XL, add advisory: "This is a very large PR. Consider splitting into smaller, focused PRs for easier review."
Detect Monorepo Structure
Check for monorepo indicators (in parallel with diff collection):
,pnpm-workspace.yaml
,lerna.json
,nx.json
,rush.jsonturbo.json- Presence of
orpackages/
directories at repo rootapps/
If detected, identify which packages/workspaces have changes by mapping changed file paths to package directories. This information is used for:
- Grouping changes in the PR body by package
- Package-based label matching (Phase 2)
- Noting in the summary if changes are scoped to a single package
Detect Conventional Commits
Parse the commit list. Check if >50% of commits follow the pattern
type(scope): description or type: description (where type is one of: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert).
If conventional commits are detected:
- Use the predominant type to inform the title prefix (e.g.,
→ feature PR,feat:
→ bugfix PR)fix: - Group changes by type in the body (Features, Bug Fixes, Refactoring, etc.)
- If any commit contains
in its body/footer or usesBREAKING CHANGE:
after the type (e.g.,!
), add a "Breaking Changes" section to the bodyfeat!: - Still include the ticket ID prefix if present:
[TICKET-ID] feat: description
If conventional commits are NOT detected (<=50% match), fall back to the current diff analysis behavior.
Detect PR Template
Before generating the body, check for repository PR templates in this order:
.github/PULL_REQUEST_TEMPLATE.md.github/pull_request_template.md
(repo root)PULL_REQUEST_TEMPLATE.md
(repo root)pull_request_template.md- Multiple templates in
— if found, list them and ask the user to pick one via AskUserQuestion.github/PULL_REQUEST_TEMPLATE/*.md
If config specifies
template, use that path directly instead of searching.
If a template is found:
- Read the template file
- Preserve its structure: headings, checkboxes, sections, formatting
- Remove HTML comments (
) and replace them with actual content generated from the diff/commits<!-- ... --> - Fill in each section intelligently:
- Summary/description sections → summarize from diff analysis
- Changes/what sections → list actual changes
- Testing sections → infer test steps from changes
- Checklist items → check off items that are clearly satisfied by the diff (e.g., "Tests added" if test files are in the diff)
- Leave unknown/inapplicable sections with a brief note or empty
- Append the
footer*Generated with Claude Code*
If no template is found, use the built-in template below.
Generate Title
- If
provided, use it.--title - Otherwise:
- Analyze the commits and diff to determine the primary change.
- If conventional commits detected: use the predominant type as part of the title format.
- Format:
if ticket exists, or just[TICKET-ID] Imperative description
.Imperative description - For HOTFIX branches:
.[HOTFIX] Imperative description - Keep under 70 characters.
Generate PR Body (built-in template — used only when no repo template is found)
## Summary {2-5 bullets: what changed and why, written from analyzing the actual diff and commits} ## Changes {If monorepo: group by package with package name as sub-heading} {If conventional commits: group by type — Features, Bug Fixes, Refactoring, etc.} {Otherwise: group by area} ### {Area/Package/Type 1} - {specific change} ### {Area/Package/Type 2} - {specific change} (use a flat bullet list instead of grouped headings if changes are simple/few) {If breaking changes detected:} ## Breaking Changes - {description of breaking change and migration path} ## Related Issues {List any GitHub issue references found in commits or branch name} {Use "Closes #123" for issues this PR fixes} {Use "Relates to #123" for referenced but not fixed issues} (OMIT this section if no issue references found) ## Screenshots {embedded images if URLs were provided} {TODO: drag-and-drop images in browser — if local file paths were provided} (OMIT this entire section if no images) ## Testing - [ ] {test item inferred from the changes} - [ ] {build/compile verification relevant to the project} - [ ] {manual verification step if applicable} --- *Generated with [Claude Code](https://claude.com/claude-code)*
If config specifies
body_footer, append it before the "Generated with Claude Code" line.
Fill in each section by actually reading and understanding the diff/commits — do not use generic placeholders. The summary and changes sections should reflect what actually changed.
Phase 4 — Confirm with User (MANDATORY — never skip)
Present a full preview to the user. Format it clearly:
=== PR Preview === Title: [DP-1067] Add safe area support Base: main Assignee: mosamaasif Reviewer: alice (CODEOWNERS), bob (recent author) Labels: client, mobile, size/M Draft: Yes Size: M (142 lines changed) Template: .github/PULL_REQUEST_TEMPLATE.md {If stacked PR: "Note: Stacked PR targeting `feature/base-branch`"} {If monorepo: "Packages: @app/payments, @app/auth"} --- Body --- (full PR body from Phase 3) --- End Body ---
Then ask via AskUserQuestion:
- "Create this PR?" with options:
- "Yes, create it"
- "Edit details" (loop back — ask what to change)
- "Abort"
If user picks "Edit details", ask what they want to change, apply the change, and show the preview again. Loop until they approve or abort.
Phase 5 — Create PR
-
Push branch if not already pushed:
- Check:
git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null - If no upstream:
git push -u origin HEAD - If upstream exists but local is ahead:
git push
- Check:
-
Create the PR using
:gh pr creategh pr create \ --base "{base}" \ --title "{title}" \ --assignee "{assignee}" \ --reviewer "{reviewer1},{reviewer2}" \ --label "{label1},{label2}" \ {--draft if draft} \ --body "$(cat <<'PRBODYEOF' {generated body} PRBODYEOF )"- Omit
if none selected--reviewer - Omit
if none selected--label - Omit
if not draft--draft
- Omit
-
Display the result:
- Show the PR URL returned by
gh pr create - If local image files were provided via
, remind: "Local images can't be uploaded via CLI. Open the PR in your browser and drag-and-drop the images into the Screenshots section."--images - Post-creation hint: "Monitor CI checks with
."gh pr checks
- Show the PR URL returned by
Edge Case Handling
- No ticket ID in branch: Omit
prefix from title and omit ticket-related content from body.[TASK-ID] - HOTFIX branches: Use
prefix. No ticket link.[HOTFIX] - PR already exists: Detected in Phase 1. Offer
flow.gh pr edit - Very large diffs (>5000 lines or 50+ files): Warn user. Rely on commit messages and
for body generation.--stat - Local image files: Remind user to manually upload after PR creation.
- Base branch missing on remote: Fall back to repo default branch with a warning.
- No labels in repo: Skip label inference silently.
- No reviewers found: Skip reviewer suggestion, allow manual entry.
- gh CLI not installed: Stop immediately with install instructions.
- Not a git repo: Stop immediately.
- No PR template found: Use built-in template — never error on missing template.
- No CODEOWNERS file: Skip CODEOWNERS step, fall through to other reviewer strategies.
- No conventional commits: Fall back to diff-based analysis for title and body grouping.
- Not a monorepo: Use flat file grouping — no package-level grouping.
- No config file: Proceed with built-in defaults — config is strictly optional.
- Config file has unknown keys: Ignore unknown keys silently.
- Multiple PR templates directory: Ask user to pick one.
- Stacked PR (non-standard base): Show advisory note, don't change behavior.
- CI failures on branch: Show warning, don't block PR creation.