Docs / Start here
Getting started
From zero to an embedded, self-updating screenshot. Your tests already take the screenshots. Screenery gives each one a URL that stays the same while the image behind it follows your default branch.
Two decisions
Setup asks you two things, and neither has a default:
- Public or private. A public screenshot can be embedded anywhere with a plain URL. A private one needs a credential to read.
- The setup path: github_app or static_token.
| Path | What you get | What it costs |
|---|---|---|
| github_app | No secret in CI. Publishing a GitHub release promotes @latest for you. | One trip to GitHub to install the App. Your CI must be GitHub Actions. |
| static_token | Works with any CI. | A SCREENERY_TOKEN secret in CI, and you promote by hand. |
If your repository is on GitHub and your CI is GitHub Actions, take the GitHub App path. The rest of this page follows it. Without the GitHub App covers the other one.
Prefer to hand this to an agent? Add the MCP server with claude mcp add screenery -- npx -y screenery mcp (or your host's equivalent) and ask it to set up self-updating screenshots for the repository. It will ask you both questions above, together, and do the rest.
1. Install the GitHub App
Sign in to the console, follow install app, and pick the repository. Choose public or private. That creates the project, named {org}/{project}, for example acme/dashboard.
The install binds the project to the repository's numeric id, not its name. Renaming the repository changes nothing, and a fork cannot publish to your project. One repository can feed several projects: each push names the project it publishes to.
On a GitHub organization, the person who installs the App has to be the one who finishes setup in Screenery, signed in with the same GitHub account. Screenery checks that with GitHub. Other members of the organization cannot connect an installation they did not make. Right after the install, the console may say Finishing setup with GitHub for a few seconds while GitHub confirms who installed it.
2. Push screenshots from CI
After your Playwright step, in the same job, add the Screenery Action:
permissions:
id-token: write
contents: read
pull-requests: write
steps:
# … check out, install, run your Playwright tests …
- uses: screenery/push@v1
with:
project: acme/dashboard
path: test-results
There is no Screenery secret to store. Each line has a job:
- id-token: write lets the job ask GitHub for an OIDC token: a short, signed statement of which repository and branch the job is running for. The Action trades it for a publish credential that lives five minutes. Nothing is stored, and there is nothing to rotate.
- contents: read keeps your checkout step working once you list permissions.
- pull-requests: write lets the job post a preview comment on each pull request. The Action hands the CLI the job's own token, so there is no env: line to add. If Screenery posts the comment as screenery[bot] instead, the job needs no permission for it. Either way, a comment that cannot be posted never fails the push. See Who posts the comment.
Every input, output and option is on the GitHub Action reference.
How files become names
Each file's path under path, without its extension, is its name. Nothing is renamed for you.
| File | Name |
|---|---|
| test-results/checkout/payment-form.png | checkout/payment-form |
| test-results/checkout/payment-form/mobile.png | checkout/payment-form/mobile |
A path that cannot be a legal name stops the push with an error that names the file. PNG, JPEG, WebP, AVIF, GIF, WebM and MP4 files are pushed. Other files, such as traces, are skipped and listed in the log.
3. The first push creates @latest
Every push lands on a channel: a name that points at one build. A pull request's push lands on @pr-12; a branch's lands on @main, @feature-x and so on. @latest is the channel you embed, and it follows your default branch.
The first push to a new project creates @latest, whatever branch it came from.
You usually add Screenery in a pull request, so this is what makes your first URLs work straight away. It happens once. After that, only a push on the default branch, or a promotion, moves @latest. A pull request never does.
With the GitHub App, publishing a GitHub release also promotes your default branch to @latest. The full rules are in How @latest moves.
4. Embed it
A public screenshot's @latest URL is ordinary Markdown. Paste it once:

The next push on your default branch changes the image behind that URL. There is no cache to purge. The extension is the format you pushed: a PNG is served as .png.
The push log prints a ready-to-paste snippet for every screenshot it published. Unlisted and private screenshots need a signed URL or a read token, so they have no paste-once snippet. The URL grammar, channels and versions are on Screenshot URLs.
Without the GitHub App
On the static_token path, CI publishes with a long-lived publish token instead of OIDC. Create the project through the MCP server and choose static_token when it asks. A publish token comes from the agent, or from new publish token on the project's Access screen in the console. Store it as a CI secret named SCREENERY_TOKEN.
In GitHub Actions, pass the secret to the Action instead of asking for an OIDC token:
- uses: screenery/push@v1
env:
SCREENERY_TOKEN: ${{ secrets.SCREENERY_TOKEN }}
with:
project: acme/dashboard
path: test-results
In any other CI, run the CLI from npm with SCREENERY_TOKEN in the environment. Outside GitHub Actions it cannot read the branch, commit or run number for itself, so pass them:
npx screenery push ./test-results --project acme/dashboard \ --branch main \ --commit "$(git rev-parse HEAD)" \ --commit-timestamp "$(git show -s --format=%cI HEAD)"
The first push still creates @latest. After that, a project with no repository bound has no default branch to follow, so you move @latest yourself, again with SCREENERY_TOKEN set:
npx screenery promote acme/dashboard --from main --to latest
Next
- GitHub Action reference: every input, outputs, fork pull requests, pinning.
- How @latest moves: channels, promotion, and which jobs may move the default branch.
- Screenshot URLs: the URL grammar, channels and versions.
- README screenshots that update themselves: this setup, from Playwright test to README.
- Markdown image syntax: size, alignment and dark mode for the image you embed.