"""Check a drafted PR body matches the real diff and has a test plan.
Usage: check_pr_body.py <pr-body.md> [base-ref]
Cross-checks every file path mentioned in the PR body against
`git diff <base-ref>...HEAD --name-only` (default base: origin/main), and
requires a "Test plan" (or "Testing") section to be present and non-empty.
def git_changed_files(base_ref: str):
["git", "diff", f"{base_ref}...HEAD", "--name-only"],
capture_output=True, text=True, check=True,
return set(line.strip() for line in out.splitlines() if line.strip())
except subprocess.CalledProcessError as e:
print(f"⚠️ git diff failed ({e}) — skipped diff cross-check", file=sys.stderr)
if len(sys.argv) not in (2, 3):
print("Usage: check_pr_body.py <pr-body.md> [base-ref]", file=sys.stderr)
body = open(sys.argv[1], encoding="utf-8").read()
base_ref = sys.argv[2] if len(sys.argv) == 3 else "origin/main"
changed = git_changed_files(base_ref)
mentioned = set(re.findall(r"`([\w./-]+\.\w+)`", body))
stale = mentioned - changed
print(f"❌ PR body mentions file(s) not in the actual diff: {sorted(stale)}")
print(f"✅ every file path mentioned in the body appears in the diff ({len(changed)} files changed)")
if re.search(r"#+\s*(Test [Pp]lan|Testing)\b", body):
section = re.split(r"#+\s*(?:Test [Pp]lan|Testing)\b", body, maxsplit=1)[-1]
next_heading = re.search(r"\n#+\s", section)
section_body = section[:next_heading.start()] if next_heading else section
print("✅ Test plan / Testing section present and non-empty")
print("❌ Test plan / Testing section header found but body is empty")
print("❌ no 'Test plan' or 'Testing' section found")
if __name__ == "__main__":