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:

  1. Install the GitHub App on the repository from GitHub. The check starts reporting right away.

  2. 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]
  3. 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:

.uploads.yml
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:

.uploads.yml
comment:
check:
require: [before-after, desktop, mobile]
ItemPasses when

uploads (or any)

The PR has at least one upload. Every other item implies it.
before-after

There’s a capture uploaded with –state before and one with –state after.

desktop, mobile

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 pass uploads, 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:

.uploads.yml
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 #

ResultWhen
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.

FailureOnly 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.sh isn’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 --frame for device chrome.
  • A PR is skipped by mistake. Remove the skip label and the check re-evaluates.