Readers stop trusting the page
When the screen in front of them doesn't match the picture, people start doubting the words around it too. Then they open a support ticket, or they give up on step three.
Self-updating screenshots for docs and READMEs
Every release makes a few of your screenshots wrong, and nothing tells you which. Your users find out first, when their screen doesn't match your docs. Screenery plugs into the CI you already run and publishes the screenshots your tests take, so the image in your README changes on the merge that changed the screen. Turn on the visual check, and a screen that broke by accident fails CI before it reaches your docs.
https://cdn.screenery.dev/{org}/{project}/{asset}@latest.png
Not published yet. cdn.screenery.dev/jfreal/screenery/projects-cards@latest.png answers 404 until a push to the default branch puts this screenshot on the CDN.
In production
Pheidi builds training plans for runners. Its homepage used to show a PNG exported in May and a calendar drawn by hand in CSS. Both drifted from the app. Now its tests capture 15 screens on every merge, and the homepage and 13 doc pages show them by name at @latest.
The live screenshot did not load. cdn.screenery.dev/jfreal/pheidi/desktop/plan/overview@latest.png
Why screenshots rot
A renamed button or a moved menu is a one-line diff. The PNG in your README never shows up in that diff, so no test fails and no reviewer sees it. The page looks fine to everyone except the reader trying to follow it.
When the screen in front of them doesn't match the picture, people start doubting the words around it too. Then they open a support ticket, or they give up on step three.
Coding agents and AI assistants read your docs, pictures included. A stale screenshot teaches them the old screen, and they walk your users through it with full confidence.
Open the app, seed the right data, capture it at two sizes, crop it, upload it, then hunt down every page that pasted the old one. Most teams wait until someone complains.
A redesign, a new menu or a new logo means updating the same screenshots in 20 places. So teams put off the change, or ship it and leave the docs behind. Pairings Up rebranded without retaking one. Read how.
Screenery adds one step to the CI you already run. Your tests take the screenshot, the merge publishes it, and every page that embeds it shows the new one.
Five minutes of setup. After that, you never touch a screenshot by hand again.
One step after your tests publishes every PNG. No token to store. id-token: write proves the job. screenery[bot] posts the preview, or the job does; pull-requests: write covers that case.
permissions:
id-token: write
pull-requests: write
steps:
- uses: screenery/push@v1
with:
project: acme/dashboard
path: test-results
A push on the default branch moves @latest. The first build on a new project does too, from any branch. After that, a pull request keeps @pr-n and does not move @latest. With no repository bound, screenery promote moves it.
# no repository bound
npx screenery promote \
acme/dashboard \
--from main --to latest
Paste the Markdown once. The next push on the default branch updates the image, with nothing to purge.

The docs walk through each step: add a self-updating screenshot to your README, how @latest moves and the screenshot URL format.
Visual testing and approval, built in
A screenshot that updates itself also publishes its mistakes: a spinner, an error toast, a test account's data. So Screenery checks before your docs change. It keeps every screenshot your CI has ever pushed, so it always knows what @latest looked like. A screen that changed by accident fails the job. A reviewer, or an agent, approves each change before the merge that moves @latest.
screenery_diff from: latest to: pr-142
changed checkout/payment-form/mobile
changed settings/billing
added onboarding/welcome
unchanged 12 assets, byte-identical
Each changed asset comes with a URL for both sides, so the agent can look at the pixels itself.
Point the Action at a baseline and it compares every screenshot pixel by pixel before publishing. A change past your threshold, or a new finding, fails the job, and nothing is pushed.
permissions:
id-token: write
pull-requests: write
steps:
- uses: screenery/push@v1
with:
project: acme/dashboard
diff-baseline: ./baseline
Approve or reject each screenshot a pull request changed, by keyboard in the console or through an agent over MCP. Screenery reports the result to GitHub as a check named screenery. Make it required, and a pull request with an unreviewed or rejected change cannot merge. Approving records a decision; the merge is what moves @latest.
On a pull request the job posts one comment showing what changed against @latest, with immutable images reviewers can trust. screenery/push@v1 posts it with the job's own token, not as screenery[bot]. Run screenery verify-publish after a browser check, pass or fail, to publish the screenshots and diffs as receipts onto @pr-N. That does not move @latest.
Read the docs on visual regression testing in CI, reviewing a pull request's screenshots, screenshot previews in pull request comments and publishing UI eval results, which keeps a judge model's report next to each screenshot it scored.
Screenery ships an MCP server that carries its own runbook. Add it, say one sentence, and the agent creates the project, pushes a first screenshot, wires the workflow and pastes the snippet.
Three things stay yours: approve a sign-in code once, say whether the screenshots are public or private, and choose the setup path (github_app or static_token). On the GitHub App path, install the App and pick the repository.
Setup for each host is in the MCP server docs.
$ claude mcp add screenery -- npx -y screenery mcp
“Add the Screenery MCP server and set up self-updating screenshots for this repo.”
Works with Claude Code, Codex, Cursor, VS Code and any MCP host. Other hosts →
Set it up once, then stop thinking about it
The Action trades a GitHub OIDC token for a publish credential that lives five minutes. Nothing stored, nothing to rotate, nothing to leak.
Projects bind to a repository's numeric id. Renames change nothing, and a fork's pull request cannot publish to your project.
GitHub Actions needs no secret at all. Every other CI pushes with one static publish token, and the same diff gate runs anywhere the CLI does.
Public assets need no credential. Unlisted ones need a signed URL, private ones a read token — and an unauthorised request gets a 404, never a hint.
Images are served from the edge, independently of the control plane. If the dashboard is down, every embedded screenshot keeps serving.
Keep toHaveScreenshot as your pixel gate. Screenery publishes the files it already accepted, or an Argos upload folder, as-is.
Read the docs on CI credentials and GitHub OIDC, publishing Playwright screenshots and the screenery/push GitHub Action.
Screenery is a package registry for product screenshots. CI pushes screenshots on every build. Each one gets a stable URL ending in @latest.png, and the image behind it follows your default branch.
Push screenshots from CI with the screenery/push@v1 GitHub Action, then paste the @latest URL into your README once. The next push on the default branch changes the image behind it within a minute. There is no cache to purge. Getting started has the full walkthrough.
Yes, when you ask for it. Set diff-baseline to a committed folder of approved screenshots, and the Action compares every screenshot with it pixel by pixel before publishing. A change past your threshold fails the job, and nothing is pushed. See visual regression testing in CI.
Not on GitHub Actions. The Action trades a GitHub OIDC token for a publish credential that lives five minutes. Other CI systems push with one static publish token. See Credentials.
Yes. Keep toHaveScreenshot as your pixel gate. Screenery publishes the goldens it already accepted, or an Argos upload folder, as they are. See Playwright goldens.
Yes. Each screenshot is public, unlisted or private. An unlisted one opens with a signed URL that expires, and a private one with a read token. A refused request gets a 404, so nobody learns that a private screenshot exists.
Yes. Screenery ships an MCP server. Add it to Claude Code, Codex, Cursor or VS Code and ask the agent to set up self-updating screenshots. You still approve the sign-in, choose public or private, and pick the setup path.
After the first build, no. A pull request gets its own @pr-n channel and does not move @latest, so the image in your README stays on the default branch. The first build on a new project does move @latest, from whichever branch it came from. How @latest moves.
Set it up once. Point it at a baseline and every build is checked before it publishes, and a push on the default branch updates every image, everywhere you embedded it.