Git PR Report
v2.0.0Every pull request ships with proof that it works.
A Claude Code skill that refuses to open a pull request until the body carries real evidence — screenshots of anything a reviewer can see, captured payloads, actual check output, and mermaid ER diagrams for every change that touches data.
git-pr-reportghcurljqfilenodeInstall
skill.tar.gz/plugin marketplace add mitflex/git-pr-report-skills/plugin install git-pr-report@mitflex-skillsRecommended for teams. Resolves straight from git — no extra CLI, and it keeps working even if this site is down.
Configuration
PR_REPORT_DIAGRAM_VIEWERRequiredOrigin of your self-hosted Mermaid Live Editor. The script refuses to emit a link without it, so a schema is never sent to a third party by default.
These are gates, not suggestions — the skill blocks the PR until each one is satisfied or explicitly waived with a stated reason.
Screenshots are a gate
No PR for a user-visible change goes up without a screenshot in the body. Includes an escalation ladder for seeding data rather than giving up — and a hard rule against production or customer data.
ER diagrams on every data change
Backend PRs carry a mermaid erDiagram of the entities they touch, derived from the live database catalog rather than ORM models, because models drift and nullability is where the bugs hide.
Migration chains, caught early
Migration PRs lead with the migration id and its parent, verified against the tool's own graph. A base branch that already holds a sibling migration means two heads on merge — the heading makes that obvious before it breaks a deploy.
Zoomable diagram links
GitHub renders mermaid inline, small, with no pan or zoom. Every diagram gets a one-click full-screen permalink encoded in the URL fragment — nothing uploaded, nothing to expire.
Images that actually render
Repo-path images silently break in private-repo PR bodies because GitHub's proxy fetches them anonymously. The bundled uploader uses the attachments endpoint, so images inherit repo visibility.
No fabricated evidence
Only screenshots actually captured and output actually seen. Anything unverified is stated as an explicit gap — an honest gap is worth more to a reviewer than a confident guess.
Everything the archive contains — 4 files, 15.7 KB compressed. Read it before you install it.
---
name: git-pr-report
description: >-
Build an evidence-backed pull request body before the PR is opened —
screenshots of anything a reviewer can see, captured request/response
payloads, real check output, and mermaid ER diagrams for schema and data
changes. Use whenever opening, drafting, updating or rewriting a PR in any
repository (gh pr create, gh pr edit, "open a PR", "raise a PR", "put up a
PR", "update the PR description"), and whenever asked to add screenshots,
testing proof, verification notes or schema diagrams to a PR that already
exists. Enforces: a screenshot for every user-visible change, an ER diagram
for every backend change that touches data, before/after ER diagrams plus a
migration-id heading for every migration, and a zoomable full-screen link
under every diagram.
---
# PR Report
A pull request is a claim: *this change works*. The body is where the claim is
backed up. This skill turns that from a habit into a checklist — every PR ships
with proof that the change was run, not merely written.
Two questions the body must answer before `gh pr create` runs:
1. **What did you run?** — environment, account, data, commands.
2. **How do you know it worked?** — the screenshot, the payload, the query
result, the check output.
If you cannot answer both, the PR is not ready.
---
## Evidence matrix
Match the evidence to the kind of change. Screenshots stack *on top of* these
rows whenever the change is visible — they never replace a row.
| Kind of change | Required evidence |
|---|---|
| User-visible UI | Screenshots — see [Visible changes](#visible-changes-screenshots-are-required) |
| API / payload shape | The actual request and response body you captured |
| Backend reads or writes data | Query output, job result or log line, **plus an ER diagram** |
| Migration / schema change | **Before and after ER diagrams**, plus the migration-id heading |
| Refactor, types, tooling | The checks proving behaviour is unchanged (lint, typecheck, tests) |
| Docs, comments, README | One line saying so. Nothing else. |
### Never invent evidence
Include only screenshots you actually took and output you actually saw. Do not
predict what a test *would* print, do not reconstruct a payload from the code,
and do not describe a check you did not run.
When something could not be verified, write one line naming the gap and the
reason. A stated gap is useful to a reviewer. A confident guess is a trap.
---
## Visible changes: screenshots are required
**Do not open a PR for a change a person can see without a screenshot in the
body.** Without one the reviewer has to check out the branch to review it — and
in practice most will not, so the PR simply stalls.
"Visible" is broader than it first sounds:
- **Front-end change** → a before/after pair for a modified surface; the new
states (default, filled, empty, error) for a new one.
- **Back-end change with a user-facing surface** → screenshot the surface the
change exists *for*, driven against a local server on the branch. The list
whose filter you widened, the total you corrected, the response body in the
network panel. A passing test suite does not show a reviewer what moved.
- **Genuinely invisible** → CI config, dependency bumps, a pure refactor, an
internal library with no surface. Say so in one line and move on.
That last exemption is narrow. "There was no data locally" does not qualify.
### Capture targets
- **Web** — a browser tool that writes image *files* (see [Capture to disk](#step-2--capture-to-disk)).
- **iOS** — `xcrun simctl io booted screenshot shots/01-<state>.png`
- **Android** — `adb exec-out screencap -p > shots/01-<state>.png`
### When the screenshot seems impossible
Before concluding you cannot capture one, work the list:
1. **Seed the data yourself** through the app's own API or UI against your local
environment. This solves it the large majority of the time.
2. **Look for the repo's fixtures** — a seed script, `fixtures/`, `seeds/`,
`scripts/`, or a demo account documented in the README.
3. **Rule out gating first.** A 403, a redirect or an empty list is far more
often a feature flag, a role or a permission than an empty table.
4. **Shoot the control, not the rows.** Table components usually keep headers
and filters rendered when empty, so an opened filter showing the new options
still proves the change.
Only after all four fail, **stop before opening the PR** and ask the requester
for exactly what you need — "I need an account with at least one active
subscription to show the billing filter." One question costs far less than a PR
nobody can review.
### Never capture production or customer data
Seed synthetic records instead. Screenshots, PR bodies and skill files all get
forwarded, quoted and indexed; a real customer name, record or login inside one
is a leak regardless of intent. The same applies to credentials of any kind,
including throwaway ones for a local environment — do not put them in a
screenshot, a payload dump or the body.
---
## The body describes what ships — nothing else
The body explains the change being merged. It is not a status report on the base
branch and not a log of how you figured the change out.
**Do not list work already merged to the base.** No "already on main" column, no
"the range contains 30 PRs but most are promoted", no "main moved since this was
opened". The reader wants to know what merging *this* does; everything else is
noise they must read past.
This matters most on **release / promotion PRs** (`staging → main`), where the
commit range legitimately includes work already on the base. Compute the real
delta with `git diff --stat <base>...<head>` over source paths — not the commit
log. A squashed or cherry-picked commit reads as unmerged while its content is
already there, so the commit count is not the delta.
| Don't write | Write |
|---|---|
| "6 PRs in range; #41, #43, #44 already on main, only #42 ships" | "Promotes #42." |
| "Range has 18 PRs, most already promoted; 10 files differ" | "10 files changed: …" |
| "Body refreshed — base was `a1b2c3d`, now `e4f5a6b`" | *(nothing — describe today's delta)* |
Two exceptions, both about the merge rather than the history:
- **A prerequisite that must land first** — another repo's PR, a migration to
run, a flag to flip. It changes what the reader does, so it belongs.
- **A conflict and its resolution.** Name the file, both sides, and which side
won. Someone has to trust that call.
Apply the same rule to your own process: state the resolution, not the three
approaches you tried before it.
---
## Backend changes: an ER diagram is required
**Every** backend PR that reads or writes data carries a mermaid `erDiagram` of
the entities involved — not only the ones with a migration. Backend changes are
almost always claims about relationships (this join, this foreign key, this
nullable column), and a reviewer forced to rebuild four tables from prose ends
up reviewing the prose.
Pick the flavour by whether the schema moved:
- **Schema moved** (a migration is in the diff) → before/after pair. See
[Migrations](#migrations-beforeafter-diagrams-are-required); those rules are
stricter.
- **Schema unchanged** → exactly one diagram under
`## Entities this change reasons over`, opening with an explicit line:
*"No schema change — no migration and no model file touched, so there is no
before/after to draw."* Never imply structure moved when it did not.
### Scope
Include every table named in the queries you added or changed, plus the
foreign-key hops those queries traverse. Deliberately include a neighbour the
change does *not* touch when its absence is the point — drawing `refunds` beside
`invoice_lines` makes "these two surfaces are independent" checkable instead of
merely asserted.
Then add a short bullet per claim, tied to the diagram: which branch the new
query walks, which column filters it, which constraint makes an edge case
impossible, which plausible-looking column is deliberately unused and why.
### Derive from the live catalog, not the models
ORM models drift from the database. The catalog is what the query actually runs
against, and nullability — usually the half carrying the bug — is frequently
wrong in the models.
```sql
-- PostgreSQL / MySQL: relationships
SELECT tc.table_name, kcu.column_name, ccu.table_name AS refs_table, ccu.column_name AS refs_column
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu ON tc.constraint_name = ccu.constraint_name
WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_name IN ('orders','customers');
-- nullability and types
SELECT table_name, column_name, data_type, is_nullable
FROM information_schema.columns WHERE table_name IN ('orders','customers');
```
The before/after case is the exception — it reads models at two refs, because
one side of the pair does not exist in any live database yet.
**Exempt**: a backend PR touching no data at all — docs, CI config, a dependency
bump, a log-line change. Say that in one line rather than omitting silently.
---
## Every diagram carries a full-screen link
GitHub renders a mermaid fence inline, small, with no pan and no zoom. Past
roughly six tables it stops being readable, and a reviewer who cannot read a
diagram skips it — which wastes both previous sections. So **every `erDiagram`
fence is followed immediately by a link that opens the same diagram full
screen**.
Generate it with the script that ships beside this file, from the **exact source
you put in the fence** — never a retyped copy, or the two drift apart silently:
```bash
node "${CLAUDE_SKILL_DIR}/scripts/diagram-link.mjs" schema.mmd
```
It prints a permalink to the viewer configured for this installation. The
diagram is encoded in the URL fragment, so nothing is uploaded, nothing expires,
and the link still renders years later.
Configure the viewer origin once, via environment variable:
```bash
export PR_REPORT_DIAGRAM_VIEWER="https://www.nishit-portfolio.com/git-pr-report-skills/mermaid"
```
If it is unset the script refuses to emit a link rather than quietly sending
your schema to a third-party service. See `scripts/diagram-link.mjs --help`.
### Rules
- **One link per fence**, on the line directly after the closing fence.
- **A before/after pair gets two links**, labelled `Open "Before" full screen ↗`
and `Open "After" full screen ↗`. A single link covering two diagrams is worse
than none — it looks like it covers both and does not.
- **The fragment is readable by anyone holding the URL, and cannot be revoked.**
If the table or column names are themselves sensitive — a customer-specific
schema, an unannounced feature — omit the link, leave the fence alone, and
write one line saying "full-screen link omitted — schema is not public."
- **Do not screenshot a diagram instead.** A PNG cannot be zoomed either, goes
stale as soon as the schema moves, and has to be uploaded. The fence stays the
rendered artifact; the link is the escape hatch.
- **Open the link once before posting.** A malformed fence produces a link that
renders an error page — and the fence on GitHub will fail identically.
---
## Migrations: before/after diagrams are required
If the diff adds or edits a migration — Alembic, Django, Rails, Prisma, Flyway,
Liquibase, Knex, node-pg-migrate, TypeORM, or hand-written DDL — the body MUST
carry a `## Schema changes (ER)` section containing:
1. a **What changed** bullet list, one line per operation (new table, new column
and its type, new foreign key, index, drop);
2. two fenced `mermaid` erDiagram blocks headed **Before** and **After**;
3. a full-screen link under each fence.
GitHub renders mermaid natively in PR bodies — no image capture, no upload.
### Lead with the migration id, as a heading
Parallel branches each add migrations, so the first thing a reviewer needs is
which migration this is and what it sits on:
```markdown
## Migration `0042_add_order_status` → parent `0041_create_orders`
```
- Use the id **the migration tool reads**, which is often not the filename.
Alembic follows the `revision` / `down_revision` variables inside the file.
- **Several migrations in one PR** → list the whole chain, oldest first:
`` `0040_a` → `0041_b` → `0042_c` (parent `0039_x`) ``. Order inside a PR is as
load-bearing as order between PRs.
- Directly beneath the heading, state in one line whether the parent still
matches the base branch's current head. **If the base has moved, say so
loudly** — for a tool expecting one linear head that is two heads after merge,
and a service that migrates on boot will fail to start.
- **A PR whose base already contains a sibling migration must be rebased, not
merged.** The heading is where that becomes obvious.
Verify against the tool's own graph, never by eyeballing filenames — numbers in
filenames are a convention, the dependency graph is the truth:
```bash
alembic heads # Alembic
python manage.py showmigrations # Django
rails db:migrate:status # Rails
npx prisma migrate status # Prisma
```
Also check `scripts/` for a repo-specific migration-chain check.
### Scope and derivation
- Include every table the migration creates, alters or drops, **plus every table
those reference by foreign key in either direction**. Collapse unchanged
neighbours to their primary key and the linking foreign-key column only.
- Derive **Before** from the models or schema file at the merge base:
```bash
git show "$(git merge-base origin/<base> HEAD):path/to/models.py"
```
and **After** from the head. Then cross-check both against the migration's own
operations, which are canonical for DDL. **If the models and the migration
disagree, say so in the section** rather than papering over it — that
disagreement is usually the most valuable thing on the page.
- **Hub-table exception**: when a touched table has 10+ foreign-key neighbours
(`users`, `accounts`, `organizations`), drawing all of them buries the change.
Show only the relevant neighbours and name every omitted one in prose.
- **Placement**: append the section at the end of the body — unless the body ends
in a marker some bot regenerates (typically an HTML comment like
`<!-- bot:footer -->`), in which case insert it immediately *before* that
marker. Replace an existing `## Schema changes (ER)` section rather than
appending a second one.
### Keep the syntax renderable
Plain erDiagram only. An unrenderable fence is worse than no fence.
```mermaid
erDiagram
customers ||--o{ orders : places
orders {
bigint id PK
bigint customer_id FK
varchar status
timestamp created_at
}
```
Columns are `type name PK` / `type name FK`, and `PK, FK` for a column that is
both. Keep attribute comments to plain words — no punctuation that the parser
will choke on.
### Delegate the derivation when you can
If your agent harness supports subagents, hand one the repo path, the base and
head refs and the list of migration files, and have it return the finished
section. It keeps large model-file dumps out of the main context.
---
## Step 1 — reuse a capture host, then shut it down
Simulators, emulators, browsers and dev servers all hold memory until something
stops them. Reuse what is already running, and stop whatever you started once
the shots are uploaded.
```bash
xcrun simctl list devices booted # iOS: is a simulator already up?
adb devices # Android: emulator or device already attached?
```
- **iOS Simulator** — boot only if none is running (`xcrun simctl boot <udid>`),
and `xcrun simctl shutdown <udid>` when done.
- **Android emulator** — boot only if you must; `adb emu kill` when done. Leave
an emulator someone else started exactly as you found it.
- **Browser** — drive the flow with the cheapest tool available, open a
file-writing browser only for the shots themselves, close it after the last
one, and stop any dev server you started.
- **Files** — write to a scratch directory such as `/tmp/pr-shots/`, never into
the repo. Delete it after upload, along with any stray `*.png`, `*.log`,
`*.yml` or tool cache directories your browser tool dropped in the working
directory. **Delete only files you created** — images already in the repo may
be committed assets.
---
## Step 2 — capture to disk
Screenshots must exist as files before they can be uploaded, and browser tools
differ here in a way that matters:
- **Some return the image inline to the agent only** — an in-app browser pane,
for instance. Useful for checking your own work; useless for a PR body,
because there is no file to upload.
- **Some write a real image file** — for example Playwright MCP's
`browser_take_screenshot` with a `filename`. Use one of these for report
images. If it rejects a path, the error usually names the directories it is
allowed to write to; write there and move the file afterwards.
**So order the work deliberately:** click through the flow, fix what is broken
and confirm the state with the cheapest tool you have. Only once the surface is
correct, open the file-writing browser, take the shots, and close it. Its
lifetime should be a few screenshots, not the whole session.
Practical notes:
- **Fix the viewport first** (1440×900 is a good default) so the set looks like
one set.
- **Name files so the sequence reads on its own**: `01-save-disabled.png`,
`02-save-enabled-after-edit.png`.
- **Capture the claim, not the page.** If the fix is "the button stays disabled
until the form is dirty", the image must show that button in that state.
- **Assert the state programmatically alongside the image** — evaluate a script
returning `{disabled: true, title: "..."}`. The screenshot shows it; the
assertion proves you did not misread it.
- For a flow rather than a state, capture the two or three frames that carry the
argument, not every step.
### Rasterising a PDF: flatten onto white and set the DPI
A PDF page has no background — the white you see is the viewer's canvas. Some
converters (macOS `sips -s format png` among them) preserve that as transparency
and render at the PDF's point size, producing a transparent 595×842 image. It
looks correct in a light-themed preview and is **unreadable on GitHub's dark
theme**, where the "paper" turns navy and black body text all but vanishes.
Use a rasteriser that composites onto an opaque background at a real resolution:
```bash
pdftoppm -png -r 200 -f 1 -l 1 doc.pdf out # writes out-1.png, RGB, ~1654x2339
python3 -c "from PIL import Image; im=Image.open('out-1.png'); print(im.mode, im.size)"
```
`RGB` — not `RGBA` — is the check that it survives dark mode. Open the image and
look at it before attaching; this failure is invisible from the command that
produced it.
---
## Step 3 — upload so the images actually render
**Images referenced by repo path do not render in a PR body on a private
repo.** GitHub's image proxy fetches the URL anonymously, receives a 404, and
the reader sees a broken image. No branch, path or URL form fixes it — this is
the single most common way a report silently fails.
Use the attachments endpoint instead, via the script beside this file. Run it
from inside the target repository:
```bash
"${CLAUDE_SKILL_DIR}/scripts/attach-assets.sh" /tmp/pr-shots/*.png
```
It prints `<img …>` tags ready to paste into the body. The URLs inherit the
repository's visibility — readable by anyone who can see the repo, 404 for
everyone else.
Requires `gh`, `curl`, `jq` and `file`. A 404 from the upload is almost always
the wrong `gh` account rather than a broken endpoint; see the header comment in
the script for pinning `GH_TOKEN`.
If the repo has a convention for committing images too (`docs/qa/<slug>/`, for
example), follow it — that is the durable record. The upload is only what makes
the body render. Check for an existing folder before inventing one.
---
## Step 4 — structure the body
Lead with the problem, then the fix, then the evidence. A reviewer should be able
to stop reading after the first paragraph and still know what changed.
```markdown
## Problem
What was broken, described as what a user hits.
## Fix
What changed, and why this approach. Name the non-obvious constraint that shaped
it — that is the part a reviewer cannot reconstruct from the diff.
## Verification
Where it ran: environment, account role, data used.
### <the state being proven>
<img width="1440" alt="save button disabled until form is dirty" src="https://github.com/user-attachments/assets/..." />
Checks: <lint / typecheck / tests — the actual result>
## Notes for review
Known edge cases, deliberate omissions, follow-ups.
```
Put each screenshot under a heading stating what it proves, so the images read in
sequence without hunting back into the prose.
Assemble and post the body from a file — long bodies passed inline with `--body`
get mangled by shell quoting:
```bash
gh pr create --title "..." --body-file /tmp/pr-body.md
gh pr edit <number> --body-file /tmp/pr-body.md
```
To patch a section into an existing PR body without clobbering the rest:
```bash
node "${CLAUDE_SKILL_DIR}/scripts/pr-body.mjs" --pr 123 --section "Schema changes (ER)" --file schema-section.md
```
---
## Step 5 — report what actually happened
- If a check failed, say so and paste the output. Never describe a PR as verified
when part of it was not.
- If part of the change could not be exercised — no test data, a backend gap, an
environment limit — name which part and why.
- If you pushed a fix for review feedback, verify it the same way and say what
you ran. A "fixed!" comment with no evidence restarts the review from scratch.
---
## Pre-flight checklist
Run this before `gh pr create`:
- [ ] Body written to a file and passed with `--body-file`
- [ ] Every user-visible change has a screenshot, or a one-line exemption
- [ ] Screenshots uploaded via `attach-assets.sh` (not repo paths)
- [ ] Every screenshot sits under a heading saying what it proves
- [ ] Backend change touching data has an ER diagram
- [ ] Migration present → `## Schema changes (ER)` with before/after, plus the
migration-id heading and its parent
- [ ] Migration parent checked against the base branch head
- [ ] Every `erDiagram` fence followed by its own full-screen link
- [ ] Every full-screen link opened once and confirmed rendering
- [ ] No production or customer data, and no credentials, anywhere in the body
- [ ] No already-merged work described
- [ ] Failed or skipped checks stated honestly
- [ ] Scratch directory and stray tool output deleted
- [ ] Capture hosts you started shut down
Integrity
The installer verifies this digest against the archive it downloads and refuses a mismatch. Builds are byte-reproducible, so the same source always yields this hash.
sha256:f12a0b5d333aab81160f743174ee069d70e9ff17d3d7a4b173a92b10f5b2123b