Screenshots in CI
Require screenshots on pull requests
Add a screenshot step to your pull request checks, so a UI change can't merge without showing what it looks like.
How it works #
Once the uploads.sh GitHub App is installed, a check called uploads.sh shows up
in a pull request’s checks whenever the PR changes UI files. It sits next to your tests and builds,
and looks for screenshots or recordings on the PR from your coding agent, the
uploads CLI, or images dragged into the PR on GitHub.
Out of the box the check only reports. It goes neutral (“UI files changed, no uploads attached”), so nobody is blocked or notified. Turn on a requirement and it fails until the screenshots are there, then passes on its own when they arrive. Make it a required check in GitHub and it works like a linter for UI changes.
No CI workflow or GitHub Actions step is needed. The check comes from the GitHub App, the same way other GitHub integrations post theirs, and it re-runs on each push, each new upload, and each approval.
The check is built around screenshots and screen recordings today. Other file types, like test reports and logs, may get their own checks later.
Basic setup #
Make sure every pull request that changes your UI has at least one screenshot or recording:
-
Install the GitHub App on the repository from GitHub. The check starts reporting right away.
-
Turn on the requirement. A workspace admin can switch on Require screenshots on UI changes under Settings → GitHub in uploads.sh, for every repo in the workspace. Or commit this to one repo’s
.uploads.yml:.uploads.yml comment:check:require: [uploads] -
Make it required in GitHub. In the repo’s Settings → Branches (or Rules), require status checks to pass before merging and add
uploads.sh. GitHub only lists a check after it has run once, so open or update a pull request first.
With a requirement on, every pull request gets the check, and one that changes no UI files passes straight away.
Which files count as UI #
By default, changes to .astro, .tsx, .jsx, .vue, .svelte, .html, .css, .scss and
.less files, and anything in an email/ folder. It’s the same list the CLI’s
pre-PR reminder uses. Deleted files, and files moved without edits,
don’t count.
To choose the files yourself for every repo in a workspace, list them under UI file paths in
Settings → GitHub. One repo can set its own list under uiPaths, which replaces the workspace
list:
comment: uiPaths: - "apps/web/**" - "*.css"A pattern without a slash matches a file name anywhere. One with a slash starts at the repo root.
uiPaths: [] turns the check off for that repo.
Ask for more than one screenshot #
List what every UI change should include. The PR comment shows a checklist that ticks itself as files arrive, the check’s summary repeats it, and the check passes once every item is ticked:
comment: check: require: [before-after, desktop, mobile]| Item | Passes when |
|---|---|
| The PR has at least one upload. Every other item implies it. |
before-after | There’s a capture uploaded with |
| There’s a capture of each. The AI label decides when it’s set, otherwise the frame’s shape (tall reads as mobile, wide as desktop). |
A repo’s check.require replaces the workspace’s Require screenshots on UI changes switch.
What counts #
- Files uploaded with uploads.sh, from the CLI, the MCP server, or an agent, including ones staged before the PR existed.
- Images and videos dragged into the PR on GitHub, in the description or a comment. Links to
images committed to the same repo (
blob/…/shot.png?raw=true) count too. These passuploads, so contributors without uploads.sh are never stuck. The other items need files uploaded with uploads.sh. - Held files. A file held for review counts as attached, so a hold never blocks a merge.
Skip a pull request #
A PR that needs no screenshots can skip the requirement with a label: uploads:skip-verify,
skip-uploads, or uploads-skip, in any case. The check turns neutral right away, and the comment
says the requirement was skipped and by which label. A repo can list its own labels instead:
comment: check: require: [uploads] skipLabels: ["no-screenshots"]The how-to comment #
Once UI file paths are set for the workspace or repo, a pull request that changes one of those files with nothing attached also gets a short comment. It names the changed files and the ways to add screenshots. It’s the same managed comment the App keeps on every PR, so the first upload replaces it with the normal screenshots.
To keep the check but skip the comment, a workspace admin can turn off Comment when a PR has no
uploads under Settings → GitHub, or a repo can set noUploadsComment: false.
Manage it from uploads.sh #
Workspace admins can change these under Settings → GitHub without touching any repo:
- Require screenshots on UI changes: fail the check on UI changes with nothing attached.
- UI file paths: which files count as UI changes.
- Comment when a PR has no uploads: the how-to comment.
Each applies to every repo in the workspace. A repo’s .uploads.yml wins
where it sets the same thing, and the settings page shows which repo values are in effect.
Check results #
| Result | When |
|---|---|
| Success | Every attached file is published, e.g. “3 files attached”, or every required item is there. |
| Neutral | A file is held for review, everything was removed from a PR that had uploads, UI files changed with nothing attached and nothing required, or a skip label is on. |
| Failure | Only when a requirement is on and the PR is missing something. |
Without a requirement, a PR that never had uploads and changes no UI files gets no check, so backend-only PRs stay quiet.
Troubleshooting #
- No check on the PR. The App isn’t installed on the repo, or the PR changes no UI files and no requirement is on. If your org installed the App before checks existed, an org admin has to approve the new Checks permission first. See Permissions & events.
uploads.shisn’t listed in branch protection. GitHub lists a check only after it has run on the repo once. Open or update a pull request, then add it.- A mobile capture reads as desktop. Without an AI label, the shape decides. Crop a phone capture
to its viewport, or use
uploads screenshot --framefor device chrome. - A PR is skipped by mistake. Remove the skip label and the check re-evaluates.