Skip to content

feat: GitHub App that reports and approves visual baselines from CI artifacts - #423

Open
FRSgit wants to merge 3 commits into
feat/manifestfrom
feat/github-app
Open

FRSgit wants to merge 3 commits into
feat/manifestfrom
feat/github-app

Conversation

@FRSgit

@FRSgit FRSgit commented Sep 22, 2026

Copy link
Copy Markdown
Member

This is the CI side of the story that #414 set up. The manifest tells you what happened in a run; this app reads it and puts the result where people actually look: on the pull request.

It's a Probot app living in packages/github-app (private package, not published). The flow:

  • CI runs Cypress and uploads the manifest plus the snapshot folders as an artifact (with if: always(), since the interesting runs are the failing ones).
  • When the workflow finishes, the app downloads the artifact, merges every manifest in it (several machines, e2e + component, re-run attempts) and posts a Visual regression check run plus one PR comment with old / diff / new thumbnails for each screenshot that needs a look.
  • Every failed screenshot also gets its own check run with an Approve button (capped, 10 by default). The summary check has Approve all and Refresh report. Or you comment /approve-visuals (everything) or /approve-visuals `home page renders_#0` (some).
  • Approving copies the .actual.png bytes from the artifact over the baseline and pushes a single commit to the PR branch. No re-render, what you saw is what lands in git. It refuses when the branch moved on since the report, when you don't have write access, or when the PR comes from a fork (can't push there).

A few things I want to call out:

  • Images need a URL, and GitHub gives apps no way to upload them, so the app serves them itself from the downloaded artifact through signed links that expire (14 days by default, capped server-side). GitHub fetches them through camo. If that's a problem for a repo, images: false in .github/visual-regression.yml turns the report text-only. There's a "why a server" section in the package README.
  • The check runs are found by external_id and the comment by a hidden marker, so a redelivered webhook or a second click updates instead of duplicating. Approvals skip files whose baseline already has the same bytes.
  • Artifacts are streamed to disk with size caps and a zip-slip guard, and anything whose path would land outside the repo is reported as "can't approve from here" instead of being committed.
  • Manifest paths are relative to the Cypress project, not the repo. The app uses ci.workspace (added in feat: write a run manifest for CI consumers #414) to figure out the prefix, with projectRoot in the config as a fallback. That's why .github/visual-regression.yml in this repo says projectRoot: examples/next.
  • Deploy mirrors frschool: image built on the runner, pushed to ghcr.io, then docker compose up on the VPS over an SSH docker context, joining the nginx-proxy network. Manual workflow_dispatch only.

Tests run the three handlers end to end through probot.receive with nock, plus unit tests for the pure bits (manifest merging, path safety, tokens, report rendering, the Git Data commit with retry). Coverage is around 94% lines.

What I still have to do by hand before this does anything:

  1. Point APP_HOST (e.g. visual-regression.frsource.org) at the VPS so the cert can be issued.
  2. Register the GitHub App, either through Probot's manifest flow (pnpm --filter @frsource/cpvrd-github-app dev without APP_ID, open /probot) or manually with the permissions from packages/github-app/app.yml and webhook https://<APP_HOST>/api/github/webhooks. Download the PEM.
  3. Add repo secrets APP_ID, PRIVATE_KEY, WEBHOOK_SECRET, IMAGE_URL_SECRET (openssl rand -hex 32), FRSCHOOL_SSH_HOST, FRSCHOOL_SSH_PRIVATE_KEY, and the variable APP_HOST.
  4. Run the "Deploy GitHub App" workflow, check https://<APP_HOST>/healthz.
  5. Install the app on this repo and open a PR that changes a screenshot in examples/next.

I couldn't run docker compose build locally (no Docker daemon here), so the Dockerfile is untested until the first deploy.

Based on feat/manifest; GitHub should retarget it to feat/major-5 once #414 lands.

Refs: #324

🤖 Generated with Claude Code

FRSgit and others added 3 commits September 22, 2026 11:20
…aselines

New private workspace package `packages/github-app`: a Probot app that
consumes the plugin's run manifest from workflow artifacts.

- `workflow_run.completed`: resolves the pull request (also for forks),
  reads `.github/visual-regression.yml`, streams the matching artifacts to
  a disk cache (size-capped, zip-slip guarded), merges every manifest of
  the run and publishes a summary check run, one check run per failed
  screenshot (capped) and a single PR comment with old/diff/new thumbnails
- `check_run.requested_action` and `issue_comment.created`
  (`/approve-visuals [names…]`): permission check, then one Git Data commit
  copying the run's `.actual.png` files over the baselines on the PR
  branch; compare-and-swap on the ref, blob-sha idempotency, stale-report
  detection, per-branch lock
- images are served from the cache through HMAC-signed expiring links;
  `images: false` gives a text-only report
- check runs are keyed by `external_id`, comments by a hidden marker, so
  redelivered webhooks update instead of duplicating

Tests run the handlers end to end through `probot.receive` with nock.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- multi-stage `node:24-alpine` Dockerfile (repo root as context, pnpm
  filtered install, `pnpm deploy --prod`), compose files for local runs and
  for the server (external `nginx-proxy` network, `VIRTUAL_*` /
  `LETSENCRYPT_*` routing, named cache volume, restart: always), committed
  `.env.production` with secret placeholders
- `deploy-github-app.yml`: manual workflow that builds and pushes the image
  to ghcr.io, then pulls and restarts it on the VPS over an SSH docker
  context (same shape as the frschool deploys)
- `.github/visual-regression.yml` so the app can report on this
  repository's own pull requests (artifact `test`, project `examples/next`)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant