Validate PR Description
Check a PR title and body against the three things this repository's gates read:
the conventional-commit title, the issue-linking keyword, and the template
sections the spec-coverage job parses. Catch them before submission, where the
fix costs one edit instead of one CI round.
Migrated from .claude/commands/validate-pr-description.md under ADR-064, which
makes skills the single user-invocable surface. user-invocable: true is what
fires it as /validate-pr-description in both Claude Code and Copilot CLI.
Triggers
validate my PR description, check my PR title, is this PR body compliant,
validate PR metadata
Inputs
| Input |
Source |
Required |
| PR title |
$0, or stated in conversation |
Yes |
| PR body |
$1 as a path to a file, or pasted text |
Yes |
| Target repository |
current checkout |
Yes |
Process
Phase 1: Resolve the inputs
Read the title and body from the arguments when given. When they are not, ask for
both rather than inferring them from the branch or the last commit: a PR body is
usually longer than any commit message, and validating the wrong text reports a
pass that does not describe the PR.
When the body arrives as a path, read the file. When it arrives as pasted text,
work from the text directly.
Phase 2: Run the three checks
| # |
Check |
Rule |
Failure meaning |
| 1 |
Conventional commit title |
<type>(<scope>)?: <description>, type one of feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert |
Release tooling cannot classify the change |
| 2 |
Issue-linking keyword |
body holds at least one of Closes #N, Fixes #N, Resolves #N, or a past-tense variant |
The issue stays open after merge, and the PR fails the repository's issue-linkage rule |
| 3 |
Template compliance |
Summary non-empty, Changes has at least one item, Type of Change has at least one marked checkbox |
The spec-coverage job reports FAIL on an unpopulated section |
Prefer the repository's own validator when it ships one, because it is the same
code the gate runs. Resolve the script root through a plugin-root variable so the
invocation works in a plugin install and not only in the upstream checkout:
SCRIPTS_DIR="${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/github/scripts/pr"
uv run python "$SCRIPTS_DIR/validate_pr_description.py" \
--title "[title]" \
--body-file "[path-to-body.md]"
When that script is absent, validate manually against the table above. Say which
path you took: a manual pass and a validator pass are not the same evidence.
Phase 3: Report
Report per check, not as a single verdict. For each failure give the offending
text and the corrected form, so the author edits rather than re-derives.
A closing keyword is a claim that the PR closes the issue. Verify it against the
diff before recommending one. When the diff does not deliver what the issue asks,
recommend Refs #N and say why, rather than upgrading a reference the merge will
act on.
Verification
Anti-Patterns
| Avoid |
Why |
Instead |
| Inferring the title from the branch name or last commit |
Validates text that is not the PR's, and reports a pass the PR does not have |
Ask for the title and body |
| Reporting one aggregate verdict |
The author cannot tell which of the three checks failed |
Report per check |
Recommending Closes #N because the PR mentions the issue |
A closing keyword acts on merge; an unsupported one closes live work |
Verify against the diff, downgrade to Refs #N with a reason |
| Rewriting the body wholesale |
The author loses their own wording to a style preference |
Name the failing section and the minimal edit |
| Calling a manual pass a validator pass |
Two different levels of evidence read as one |
State which ran |
Extension Points
- New title types. The type list mirrors the repository's commit convention.
When that convention gains a type, update the Phase 2 table and the validator
together, not one of them.
- New template sections. The three checked sections are the ones the
spec-coverage job parses. A new parsed section is a new row, not new prose.
- Other forges. The keyword list is GitHub and GitLab behavior. On Azure
DevOps and Bitbucket the same keywords are traceability markers with no
auto-close, so a missing keyword is a weaker finding there.
1---2name: validate-pr-description3description: Validate a PR title and body against conventional commit format, issue-linking keywords, and template compliance before submission. Use when you say `validate my PR description`, `check my PR title`, or `is this PR body compliant`. Do NOT use to open, update, or push a PR (use the github skill or push-pr), and do NOT use to review the code diff (use review).4license: MIT5---67# Validate PR Description89Check a PR title and body against the three things this repository's gates read:10the conventional-commit title, the issue-linking keyword, and the template11sections the spec-coverage job parses. Catch them before submission, where the12fix costs one edit instead of one CI round.1314Migrated from `.claude/commands/validate-pr-description.md` under ADR-064, which15makes skills the single user-invocable surface. `user-invocable: true` is what16fires it as `/validate-pr-description` in both Claude Code and Copilot CLI.1718## Triggers1920`validate my PR description`, `check my PR title`, `is this PR body compliant`,21`validate PR metadata`2223## Inputs2425| Input | Source | Required |26|-------|--------|----------|27| PR title | `$0`, or stated in conversation | Yes |28| PR body | `$1` as a path to a file, or pasted text | Yes |29| Target repository | current checkout | Yes |3031## Process3233### Phase 1: Resolve the inputs3435Read the title and body from the arguments when given. When they are not, ask for36both rather than inferring them from the branch or the last commit: a PR body is37usually longer than any commit message, and validating the wrong text reports a38pass that does not describe the PR.3940When the body arrives as a path, read the file. When it arrives as pasted text,41work from the text directly.4243### Phase 2: Run the three checks4445| # | Check | Rule | Failure meaning |46|---|-------|------|-----------------|47| 1 | Conventional commit title | `<type>(<scope>)?: <description>`, type one of `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `build`, `revert` | Release tooling cannot classify the change |48| 2 | Issue-linking keyword | body holds at least one of `Closes #N`, `Fixes #N`, `Resolves #N`, or a past-tense variant | The issue stays open after merge, and the PR fails the repository's issue-linkage rule |49| 3 | Template compliance | Summary non-empty, Changes has at least one item, Type of Change has at least one marked checkbox | The spec-coverage job reports FAIL on an unpopulated section |5051Prefer the repository's own validator when it ships one, because it is the same52code the gate runs. Resolve the script root through a plugin-root variable so the53invocation works in a plugin install and not only in the upstream checkout:5455```bash56SCRIPTS_DIR="${COPILOT_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT:-.claude}}/skills/github/scripts/pr"57uv run python "$SCRIPTS_DIR/validate_pr_description.py" \58 --title "[title]" \59 --body-file "[path-to-body.md]"60```6162When that script is absent, validate manually against the table above. Say which63path you took: a manual pass and a validator pass are not the same evidence.6465### Phase 3: Report6667Report per check, not as a single verdict. For each failure give the offending68text and the corrected form, so the author edits rather than re-derives.6970A closing keyword is a claim that the PR closes the issue. Verify it against the71diff before recommending one. When the diff does not deliver what the issue asks,72recommend `Refs #N` and say why, rather than upgrading a reference the merge will73act on.7475## Verification7677- [ ] Title checked against the conventional-commit pattern, and the type named78- [ ] Body searched for every accepted issue-linking keyword, not just `Closes`79- [ ] All three template sections inspected, with the failing one named80- [ ] Validator path stated: the repository script ran, or the check was manual81- [ ] Every failure reported with its offending text and a corrected form82- [ ] Any recommended closing keyword checked against the diff first8384## Anti-Patterns8586| Avoid | Why | Instead |87|-------|-----|---------|88| Inferring the title from the branch name or last commit | Validates text that is not the PR's, and reports a pass the PR does not have | Ask for the title and body |89| Reporting one aggregate verdict | The author cannot tell which of the three checks failed | Report per check |90| Recommending `Closes #N` because the PR mentions the issue | A closing keyword acts on merge; an unsupported one closes live work | Verify against the diff, downgrade to `Refs #N` with a reason |91| Rewriting the body wholesale | The author loses their own wording to a style preference | Name the failing section and the minimal edit |92| Calling a manual pass a validator pass | Two different levels of evidence read as one | State which ran |9394## Extension Points9596- **New title types.** The type list mirrors the repository's commit convention.97 When that convention gains a type, update the Phase 2 table and the validator98 together, not one of them.99- **New template sections.** The three checked sections are the ones the100 spec-coverage job parses. A new parsed section is a new row, not new prose.101- **Other forges.** The keyword list is GitHub and GitLab behavior. On Azure102 DevOps and Bitbucket the same keywords are traceability markers with no103 auto-close, so a missing keyword is a weaker finding there.