From GitHub Actions

Attach screenshots to a PR from GitHub Actions

One comment per PR, updated on every run.

What you get #

Add one step to a workflow, and every run posts its screenshots and artifacts to the pull request. The CLI authenticates with its own token, so it doesn’t depend on which GitHub token the job holds. A CI job can:

  • Post screenshots from a Playwright or Cypress run to the pull request.
  • Attach non-image artifacts, like a test report or a log, to the same comment.
  • Update one comment on each run instead of adding a new one.

Set up the token #

Create a service token for the workflow. A service token belongs to the workspace, not to a member, so it keeps working when people join or leave. A workspace admin creates it in the workspace settings, under Settings → Service tokens:

  1. Give the token a label such as CI. Files the workflow uploads are attributed to this label.
  2. Set access to Read & write, and choose an expiry.
  3. Copy the token. It starts with ups_<workspace>_.

Save it as a repository secret named UPLOADS_TOKEN. The CLI reads the token and its workspace from that variable.

A service token can post to repos that are already linked to the workspace, but it cannot link a new repo. If this is the first time the workspace posts to the repo, link it once from your own machine with a personal login:

uploads github link --repo <owner>/<repo>

Any workspace admin can revoke a service token on the same settings page. A personal token from your account settings also works, but it belongs to the member who creates it: files are attributed to them, and the token stops working if they leave the workspace.

Add the workflow step #

Add a step after the job that writes your screenshots:

.github/workflows/screenshots.yml
on: pull_request
permissions:
contents: read
pull-requests: write
jobs:
screenshots:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# …build the app and write screenshots to ./shots
- run: npm install -g @buildinternet/uploads
- run: uploads put ./shots/*.png --pr ${{ github.event.pull_request.number }}
env:
UPLOADS_TOKEN: ${{ secrets.UPLOADS_TOKEN }}
GH_TOKEN: ${{ github.token }}

The files get stable URLs keyed to the pull request. The attachments comment posts as uploads-sh[bot] when the GitHub App is installed. Without the App, the CLI posts the comment with gh, which uses GH_TOKEN.

Want a screenshot of a URL without a test runner? Use uploads screenshot — see Capture & annotate →.

FAQ #

How do I post screenshots from GitHub Actions to a pull request?
Upload them with uploads put and the PR number in a workflow step. The files get stable URLs, and one attachments comment on the PR lists them. Later runs update that comment instead of adding new ones.
Can I attach test reports and logs, not only images?
Yes. PDFs, logs, CSV, JSON, Markdown, and zip files are listed in the same comment, with their type and size.
Does this work for pull requests from forks?
Only if the workflow can read the UPLOADS_TOKEN secret. GitHub does not pass repository secrets to workflows triggered by pull requests from forks.