← About Visual Review

Set up Visual Review

Visual Review renders the affected screens before and after a pull request and diffs them, so a reviewer sees what changed on screen — not just what changed in the diff. It works from whichever of two sources you already have: CI-uploaded renders (works for everyone, and where we recommend starting), or a preview deployment LGTY can discover automatically. LGTY does not require Vercel, or any specific host — neither source assumes how, or whether, you deploy previews.

This guide covers what's live today. It does not cover automatic route discovery outside lgty.yml, glob matching, native mobile review, or cross-browser matrices.

Recommended: upload renders from your CI

This is the one setup that works regardless of how — or whether — you deploy previews. Your CI already produces screenshots for its own tests; add one step that uploads them.

Run this after whatever step already writes screenshots to disk:

jobs:
  lgty-visual-review:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # mints a short-lived OIDC token; no long-lived secret
      contents: read
    steps:
      - uses: actions/checkout@v5
      # ... your own steps that render screenshots to visual-review-captures/,
      # writing visual-review-captures/manifest.json alongside them
      - uses: trulayer/lgty-action@v1.0.0
        with:
          command: renders
          renders-dir: visual-review-captures/

manifest.json is a small JSON array naming each PNG and the state it shows:

[
  {
    "file": "dashboard.png",
    "state_id": "dashboard",
    "capture_key": {
      "viewport_width": 1280,
      "viewport_height": 720,
      "device_scale_factor": 1,
      "color_scheme": "light",
      "browser_engine": "chromium",
      "browser_version": "128.0.6613.137"
    }
  }
]

No lgty.yml entry is required for this path — the upload step's arrival is the whole configuration. A few things worth knowing:

  • No tool coupling. Playwright, Cypress, Storybook's own test-runner, Puppeteer, or a hand-rolled script — whatever already writes a PNG and a state_id satisfies the contract identically. LGTY parses no tool-specific config or report format.
  • State names are whatever your tool already calls them — a Playwright snapshot name, a Cypress screenshot label, a Storybook story ID. Two uploads compare only when their state_id matches exactly on both sides; a naming mismatch means no comparison, never a wrong one.
  • Keyed by commit, not branch. Base and head are resolved from the pull request's own SHAs. A base render is cached the first time it's uploaded and reused by every later PR that shares that base commit — no separate "render the base" job.
  • The brief posts immediately and updates once your images land. LGTY's webhook fires in about a second; your CI takes longer. The first comment posts without this facet, then updates in place when the images for both commits arrive. If your CI hasn't produced them yet, the brief shows a pending note, never a false "no visual change."
  • The receipt says "from your CI run," not "rendered by LGTY." On this path your own pipeline produced the pixels — LGTY compares them; it doesn't drive the render, and it doesn't independently confirm the page loaded without error the way it does on the two paths below. A comparison with no pixel difference here shows as a coverage note ("checked — no difference reported by your CI"), not the stronger "no visual change" claim reserved for the paths where LGTY's own render fleet witnessed the page load.

Full manifest field reference, versioning, and release-verification guidance live in trulayer/lgty-action.

Also available: a preview deployment LGTY can discover

If your CI already publishes a deployment for each commit through GitHub's own Deployments API, LGTY can find it and render against it directly, with no upload step. This works for a preview deployment on any host that registers a deployment status with a public HTTPS environment URL — LGTY never assumes Vercel, Netlify, or any other provider specifically.

Declare the states worth checking in lgty.yml:

visual_review:
  top_k: 3 # optional; defaults to 3, maximum 20
  states:
    - name: Dashboard
      route: /dashboard
      readiness_selector: '[data-lgty-ready="dashboard"]'
      code_paths:
        - app/dashboard/page.tsx
        - components/ui/zero-state.tsx
    - name: Settings
      route: /settings
      readiness_selector: '[data-lgty-ready="settings"]'
      code_paths:
        - app/settings/page.tsx
        - components/ui/zero-state.tsx

Each state requires:

  • name: the page or state name shown to the reviewer.
  • route: an absolute route available on both preview deployments.
  • readiness_selector: a visible, state-specific element that appears only when the page is ready. Generic selectors such as body, main, and #root are rejected.
  • code_paths: exact repository-relative files that can affect the state. Globs and framework-specific component discovery are not supported.

Declare one state per route; duplicate routes are rejected. LGTY intersects changed paths and grounded caller paths with code_paths, ranks the affected states, and renders at most top_k. A diff-affected state beyond that limit is still named in the coverage count, not silently dropped.

Today, this path reaches public states only. There is currently no token, JWT, or credential flow that lets LGTY stand in for a signed-in session on a preview, and no deployment-protection bypass for any hosting provider, including Vercel. If your preview requires a credential to open, LGTY can't reach it — that state renders as couldn't reach this state this run, our gap, never a change.

Check the comparison

Open the base and head receipts from the pull request comment and check:

  1. Both receipts name the intended state.
  2. Both pages show the real application state, not a loading or error fallback. (A CI-uploaded receipt reflects whatever your own CI captured — LGTY confirms the images arrived and compares them, not that your app rendered cleanly.)
  3. Shared-component changes select every configured page whose code_paths contains that component, without duplicate route comparisons.

If either side couldn't reach the declared state, LGTY names that state as unavailable. That's missing visual coverage, not evidence the page is unchanged.

Troubleshooting

No page was selected (preview-discovery path)

Confirm the changed filename exactly matches a code_paths entry in the pull request branch's lgty.yml. Globs are not expanded.

A named route is unavailable (preview-discovery path)

Confirm your CI actually published a deployment for that commit, and that its environment URL is a public HTTPS address with no credential required. A protected or missing deployment renders as unreached, never as "no visual change."

Two states aren't pairing up (CI-upload path)

Compare the state_id your tool wrote for the base commit's upload against the head commit's. They must match exactly, or LGTY won't treat them as the same state — check for a timestamp, random id, or environment-specific value that snuck into the name.

A comparison is stuck "pending" (CI-upload path)

LGTY posts the brief before your CI has finished and updates the same comment once images for both commits arrive. If the upload for this commit hasn't run yet, the note stays until a bounded wait expires, then converts to a declared "CI upload not received this run."

No base render is available (CI-upload path)

The base commit was never uploaded — usually because the upload step was added after that commit landed, or history isn't linear. This resolves itself once an upload exists for that SHA; it's declared, not guessed.

Current boundaries

This setup covers CI-uploaded renders from any tool that emits a PNG and a manifest, repository-owned affected-state selection and exact changed-path mapping for the preview-discovery path, route deduplication, and honest unavailable-state diagnostics across both.

It does not currently provide automatic route discovery, glob matching, an authenticated preview flow (no token, JWT, or bypass mechanism exists for any hosting provider today, including Vercel), native iOS or Android review, or cross-browser matrices.