Add PR description readiness check (#1179)

* Add PR description readiness check
This commit is contained in:
Engel Nyst
2026-06-09 22:26:08 +00:00
committed by GitHub
parent b969162027
commit 77a8decca9
4 changed files with 222 additions and 2 deletions
+13 -2
View File
@@ -1,14 +1,25 @@
<!-- Keep this PR as draft until it is ready for review. -->
<!-- AI/LLM agents: be concise and specific. Do not check the box below. -->
HUMAN:
<!-- Human contributors: add a short note about your testing before checking the box. -->
- [ ] A human has tested these changes.
AGENT:
<!-- AI/LLM agents:
Do not edit the HUMAN section or human-tested checkbox.
In this AGENT section and the template fields below, provide evidence that the
code runs properly end-to-end. Just running unit tests is NOT sufficient. Explain
exactly what command you ran and include logs, screenshots, or reproduction notes.
-->
---
## Why
<!-- Describe problem, motivation, etc.-->
<!-- Describe problem, motivation, etc. -->
## Summary
+172
View File
@@ -0,0 +1,172 @@
"""Validate PR description readiness before a PR is reviewed.
Required template fields: Why, Summary, and How to Test.
Local usage example:
python .github/scripts/check_pr_description.py --body-file /tmp/pr-body.md
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from pathlib import Path
HUMAN_TESTED_TEXT = "A human has tested these changes."
# Reject placeholders while allowing a concise human-written sentence.
MIN_HUMAN_NOTE_CHARS = 20
# These are the only PR-template sections that must remain and contain content.
REQUIRED_TEMPLATE_FIELDS: tuple[str, ...] = ("Why", "Summary", "How to Test")
HTML_COMMENT_RE = re.compile(r"<!--[\s\S]*?-->")
HEADING_RE = re.compile(r"(?m)^##\s+(.+?)\s*$")
HUMAN_HEADING_RE = re.compile(r"(?im)^\s*HUMAN:\s*$")
AGENT_HEADING_RE = re.compile(r"(?im)^\s*AGENT:\s*$")
def checkbox_re(label: str) -> re.Pattern[str]:
return re.compile(rf"(?im)^\s*[-*]\s+\[(?P<mark>[ xX])]\s+{re.escape(label)}\s*$")
def checkbox_is_checked(pattern: re.Pattern[str], text: str) -> bool:
match = pattern.search(text)
return match is not None and match.group("mark").lower() == "x"
HUMAN_TESTED_RE = checkbox_re(HUMAN_TESTED_TEXT)
def visible_text(text: str) -> str:
"""Return PR body content that should count as author-provided text."""
lines = []
for line in HTML_COMMENT_RE.sub("", text).splitlines():
stripped = line.strip()
if stripped and stripped != "-":
lines.append(stripped)
return "\n".join(lines).strip()
def first_visible_line(text: str) -> str:
for line in HTML_COMMENT_RE.sub("", text).splitlines():
stripped = line.strip()
if stripped:
return stripped
return ""
def extract_sections(body: str) -> dict[str, str]:
matches = list(HEADING_RE.finditer(body))
sections: dict[str, str] = {}
for index, match in enumerate(matches):
start = match.end()
end = matches[index + 1].start() if index + 1 < len(matches) else len(body)
sections[match.group(1).strip()] = body[start:end]
return sections
def extract_human_note(body: str) -> str:
"""Return human-written text in the required location before the checkbox."""
human_match = HUMAN_HEADING_RE.search(body)
if human_match is None:
return ""
checkbox_match = HUMAN_TESTED_RE.search(body, human_match.end())
if checkbox_match is None:
return ""
return visible_text(body[human_match.end() : checkbox_match.start()])
def validate_pr_body(body: str) -> list[str]:
errors: list[str] = []
if first_visible_line(body) != "HUMAN:":
errors.append("The first visible line of the PR description must be `HUMAN:`.")
human_note = extract_human_note(body)
if len(human_note) < MIN_HUMAN_NOTE_CHARS:
errors.append(
"Add a short human-written note between `HUMAN:` and "
"the human-tested checkbox."
)
human_tested = HUMAN_TESTED_RE.search(body)
if human_tested is None:
errors.append(
f"Keep the `- [ ] {HUMAN_TESTED_TEXT}` checkbox in the PR description."
)
elif not checkbox_is_checked(HUMAN_TESTED_RE, body):
errors.append(
"A human must check `A human has tested these changes.` before review."
)
if AGENT_HEADING_RE.search(body) is None:
errors.append("Keep the `AGENT:` marker from the PR template.")
sections = extract_sections(body)
for section in REQUIRED_TEMPLATE_FIELDS:
if section not in sections:
errors.append(f"Keep the `## {section}` section from the PR template.")
elif not visible_text(sections[section]):
errors.append(f"Fill in the `## {section}` section of the PR template.")
return errors
def body_from_event(event_path: Path) -> str:
payload = json.loads(event_path.read_text())
pull_request = payload.get("pull_request")
if not isinstance(pull_request, dict):
raise ValueError("GitHub event payload does not contain a pull_request object")
body = pull_request.get("body")
return body if isinstance(body, str) else ""
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=(
"Validate pull request description readiness from --body-file "
"or a GitHub event payload."
)
)
parser.add_argument(
"--body-file", type=Path, help="Read a PR description body from a file."
)
parser.add_argument(
"--event-path",
type=Path,
default=Path(os.environ["GITHUB_EVENT_PATH"])
if "GITHUB_EVENT_PATH" in os.environ
else None,
help="Read the PR description body from a GitHub event payload.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
if args.body_file is not None:
body = args.body_file.read_text()
elif args.event_path is not None:
body = body_from_event(args.event_path)
else:
raise SystemExit("Pass --body-file or set GITHUB_EVENT_PATH.")
errors = validate_pr_body(body)
for error in errors:
print(f"::error::{error}")
if errors:
print(f"PR description validation failed with {len(errors)} error(s).")
return 1
print("PR description validation passed.")
return 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,28 @@
---
name: PR Description Check
# Use pull_request_target so fork PR descriptions can be checked, but only run
# trusted validation code from the base branch checkout below.
on:
pull_request_target:
types: [opened, edited, reopened, ready_for_review]
permissions:
contents: read
pull-requests: read
jobs:
validate-pr-description:
name: Validate PR description
# Draft PRs may still have incomplete descriptions; validate when review starts.
if: github.event.pull_request.draft == false
runs-on: ubuntu-24.04
steps:
- name: Checkout trusted workflow scripts
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Validate HUMAN note and PR template
run: python .github/scripts/check_pr_description.py
+9
View File
@@ -19,6 +19,15 @@
- Verification command: `npm run typecheck && npm run build`.
- GitHub automation now includes `.github/workflows/ci.yml` for `npm ci`, `npm test`, and `npm run build`, plus `.github/dependabot.yml` with weekly npm/github-actions updates gated by a 7-day cooldown.
## PR Description Human Check
The `HUMAN:` section and the `A human has tested these changes.` checkbox in
PR descriptions are reserved for human contributors only. AI agents
MUST NOT add to, edit, move, remove, or check these fields. If the PR description
CI fails because these fields are missing, empty, or unchecked, stop and ask the
human user to update them in their own words. If the fields were already updated
by a human, report the exact validator error rather than editing them yourself.
## Tracking / Analytics Architecture
Two distinct PostHog systems exist. **Never mix them at a call site.**