Screenery

Docs / Testing and review

Pull-request comments and Markdown

After a push, Screenery can comment on the pull request with what changed. Every push also writes the gallery and README Markdown for you, so nobody assembles screenshot URLs by hand.

The preview comment

After the push, the CLI reads the build's gallery: every screenshot, each with a status against a base channel (added, removed, changed or unchanged). The base is the channel of the branch the pull request merges into. If that branch has no channel yet, because it has never pushed or its name cannot be a channel, the base is @latest. The comment is built from that gallery:

## šŸ“ø Screenery — 3 changed Ā· 1 added Ā· 12 unchanged

**[Open the gallery](…)** — 16 screenshots from `a1b2c3d`, [CI run](…)

| Payment form Ā· mobile | Cart | Home (added) | Settings |
| --- | --- | --- | --- |
| ![…](…@v_9f1c.png) | … | … | … |

_Compared with `latest`. Unchanged screenshots are byte-identical to it._

Compared at the merge base

Your branch grew from one commit of the base branch: the merge base. If the comment compared with the base channel as it is today, every change the base branch gained since you branched would show up as a change your pull request made. So when the push recorded its merge base, the comparison is with the base channel as it was at that commit, and the last line says so:

_Compared with `main` at `a1b2c3d`, the merge base. Unchanged screenshots are byte-identical to it._

A shallow checkout is the usual reason. actions/checkout fetches one commit by default; on a pull request the CLI can still read the base off GitHub's merge commit, but for the merge base itself and its history, fetch it all:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0

Possibly flaky screenshots

A flaky screenshot is one whose bytes change when nothing real changed: an animation caught mid-frame, a clock, a font that loaded late. Screenery spots two patterns from builds it already has, with no extra capture work:

When a changed or added screenshot is flaky, its cell in the strip says so and a note follows the pictures:

| Settings (flaky) | Pay (added) | … |

**Possibly flaky:** this screenshot has changed before when nothing real did, so the change here may be noise.

- `settings` — went back to an earlier version on `main` in recent builds

To fix a flaky screenshot, make the page hold still before the capture: wait for fonts and network to settle, freeze the clock, and turn off animations (Playwright's animations: 'disabled').

Who posts the comment

Each project has a setting for who posts the comment, on the console's Access screen:

SettingPosted byWhat the job needs
Screenery screenery[bot], after the build Nothing. No token and no pull-requests: write.
CI Your job, as github-actions[bot] pull-requests: write. The Action passes the job's own token through its github-token input, so there is no env: line.
Off Nobody Nothing. The gallery and the Markdown are still written.

Until you choose, Screenery posts when its GitHub App may write pull requests on the repository, and your job posts otherwise. So a workflow that covers both looks like this:

permissions:
  id-token: write
  contents: read
  pull-requests: write
steps:
  - uses: screenery/push@v1
    with:
      project: acme/dashboard
      path: test-results

When Screenery posted, the log prints the comment's URL and the job does not post a second one. Without pull-requests: write, GitHub refuses the job's post. That is a warning naming the permission, and the push still passes.

Switching a project from CI to Screenery starts a new comment. The old one from github-actions[bot] is left as it was, because Screenery never edits another author's comment. A push that uses SCREENERY_TOKEN instead of OIDC is always posted by the job.

When it posts

comment inputCLI flagBehaviour
auto(default) Posts on a pull request. Skips a pull request from a fork, where the token cannot write.
true--commentAlways tries to post.
false--no-commentNever posts.

To update its comment rather than add a new one, the CLI finds the comment by a hidden marker and checks that its own identity wrote it. It never edits anyone else's comment. If your job posts as its own GitHub App rather than github-actions[bot], set SCREENERY_COMMENT_AUTHOR to that App's login so the comment is updated in place.

Why it embeds version URLs

GitHub caches images in comments by their URL. A moving URL such as …@pr-12.png would keep showing the first push's pixels after a second push, and a reviewer could approve a screenshot the branch no longer produces. So the strip embeds the immutable version URL, …@v_9f1c….png, which always shows exactly what was pushed. The moving channel URL is in the gallery Markdown beside it. See URLs and asset names.

Gallery and README Markdown

Every push writes three pieces of Markdown, whether or not a comment is posted:

MarkdownWhat it holdsAction input / output
Gallery Every screenshot, grouped, with its status, both URLs, its size, and the README snippets at the end. Never shortened. gallery-out / gallery-markdown
README snippets An embed for each screenshot at its @latest URL, ready to paste once. readme-out / readme-markdown
PR comment The comment body, posted or not. comment-out / pr-comment-markdown

In GitHub Actions the three files land in $RUNNER_TEMP/screenery/ even when you set no path, and the comment body is added to the job's summary page. The Action also has the outputs channel, comment-url, gallery-file, readme-file and comment-file. With the CLI, the flags are --gallery-out, --readme-out and --comment-out, and the README snippets are also printed in the log:

README snippets (@latest) — paste once:
  ![checkout/payment-form/mobile](https://cdn.screenery.dev/acme/dashboard/checkout/payment-form/mobile@latest.png)

A video gets a link instead of an image.

The snippets point at @latest because a README shows what your default branch looks like. A pull request's URL would show unmerged work. See How @latest moves.

Any job, person or script can fetch the same gallery after the push:

npx screenery gallery acme/dashboard --pr 12
npx screenery gallery acme/dashboard --channel latest --format markdown

Pick exactly one of --pr, --build or --channel. --against <channel> chooses the base. The default output is JSON, which suits agents and jq. A public project needs no credential to read its gallery.

Verify receipts

screenery verify-publish is for a different job: after a browser check in a pull request, publish its screenshots and diffs as receipts, whether the check passed or failed. Product screenshots still go through screenery push.

npx screenery verify-publish ./verify-out --project acme/dashboard --verdict fail