Subchapter 1.2
references/visual-evidence.mdMarkdown5 KBView on GitHub
Every PR that changes what a user sees ships a before/after pair per affected state. Screenshots live in GitHub’s user-attachments storage, never in the repository.
List every affected surface: page, route, dialog, component, responsive breakpoint, theme, locale. A shared primitive or global style change needs several consumers, not one representative page.
Give each state a stable id and readable title, for example settings-connection / Settings / Connection.
Check out the merge base in a detached temporary worktree so the working branch is untouched:
base=$(git merge-base origin/main HEAD)
git worktree add --detach /tmp/pr-base "$base"Capture the same states from both revisions with whatever the repo already has: Playwright, Storybook / Histoire stories, Vishot, agent-browser screenshot, or the app’s own dev server plus a browser. Prefer an existing story or e2e scenario over an ad hoc route.
Use identical viewport, theme, locale, fixture data, and readiness condition for both revisions. Use demo or sanitized data only, never real user content. Write outputs to an ignored directory such as .pr-shots/base/<id>.png and .pr-shots/head/<id>.png.
Look at every image. Agents can confidently capture the wrong page. Reject blank, loading, error, or unstable captures unless that state is the subject of the change. A failed capture blocks the PR; record the reason instead of dropping the state.
Record before: absent for a newly added state and after: removed for a deleted one.
Run the exact same capture command for both revisions. When the setup is not obvious from the images, add the command and a short manifest (id, title, viewport, before / after revisions) under the ## Visual changes table so a reviewer can reproduce it.
Remove the temporary worktree after the PR is created: git worktree remove /tmp/pr-base.
Newer GitHub CLI (2.99+) can attach files directly from the command line: it uploads local images and videos with --attach (docs (opens in a new tab)) and rewrites matching local paths in the body to the uploaded URL. Write the body with ordinary local paths, then pass each file:
## Visual changes
| Before | After |
| --- | --- |
|  |  |
| Settings / Connection, 1440x900, dark | Settings / Connection, 1440x900, dark |gh pr create \
--title "fix(settings): keep connection form visible on narrow widths" \
--body-file /tmp/pr-body.md \
--attach .pr-shots/base/settings-connection.png \
--attach .pr-shots/head/settings-connection.png--attach must match the paths in the body exactly; unmatched files are appended to the end of the body.gh pr edit --attach and gh pr comment --attach work the same way for follow-ups.) alone in its paragraph renders as a player.After creating the PR, open it and confirm every image renders and matches its label. Do not probe the asset URLs with GET/HEAD; anonymous requests can return 404 while the image still renders inside GitHub.
On a gh without --attach, upload each file with the bundled script and paste the returned URL into the body:
url=$(scripts/upload-github-attachment.sh .pr-shots/head/settings-connection.png)
# -> https://github.com/user-attachments/assets/...--repo OWNER/REPO when the current directory does not resolve to the target repository.gh auth token, or GH_TOKEN / GITHUB_TOKEN when no interactive session exists. The script never prints the token.