npm.io
0.0.11 • Published yesterdayCLI

pr-doctor

Licence
SEE LICENSE IN LICENSE
Version
0.0.11
Deps
5
Size
328 kB
Vulns
0
Weekly
0

pr-doctor

CLI + thin GitHub Action that gates PRs by pixel-comparing a running app against Figma design baselines.

Unlike visual-regression tools (Lost Pixel, Percy, Chromatic) that compare implementation now vs implementation before and need humans to approve screenshot baselines, pr-doctor treats Figma as the source of truth. A designer edit updates the baseline; there is no “approve this PNG” ritual.

v1 scope: pull → capture → compare → report; optional AI fix loop; optional E2E session video. No SaaS, no review web UI, no cloud baseline store.

Requirements

  • Node.js ≥ 20
  • Chromium (via Playwright; install with npx playwright install chromium if needed)
  • Figma access token with file_content:read (and ideally file_metadata:read for cheap version polling)

Install

npm install -D pr-doctor
# or run without install:
npx pr-doctor@latest check

Quickstart

  1. Configprdoctor.config.json at the repo root (or .mjs / .js with a default export):
{
  "figma": {
    "fileKey": "YOUR_FIGMA_FILE_KEY"
  },
  "capture": {
    "serve": {
      "command": "npm run preview",
      "url": "http://127.0.0.1:4173",
      "readyTimeoutMs": 60000
    }
  },
  "regions": [
    {
      "name": "primary-cta",
      "figmaNode": "12:34",
      "path": "/",
      "selector": "[data-testid=primary-cta]",
      "viewport": { "width": 1280, "height": 720 }
    }
  ],
  "compare": {
    "threshold": 0.1,
    "maxDiffPixelRatio": 0.01,
    "scale": 2,
    "background": "#ffffff"
  },
  "report": {
    "comment": true,
    "media": "auto"
  }
}

Secrets never go in config. figmaNode accepts a bare node id (12:34 / 12-34), fileKey:nodeId, or a full Figma URL.

  1. Env
Variable Used by Notes
FIGMA_ACCESS_TOKEN pull / check PAT or plan access token; Dev or Full seat on the plan that owns the files (View/Collab seats hit ~6 image exports/month and are unusable for CI)
GITHUB_TOKEN report / CI comment default in Actions
PRDOCTOR_API_KEY fix OpenRouter-compatible key
PRDOCTOR_MODEL fix model id for the agent

Figma rate limits (Tier 1 image export): on the order of 10–20/min depending on seat. pr-doctor caches baselines by file version (cheap meta call each run) and only re-exports when the version changes. On 429 it honors Retry-After once, then falls back to cache (warn) or exits 3.

  1. Run
export FIGMA_ACCESS_TOKEN=…
prdoctor check

Artifacts land in .prdoctor/run/<region>/{design,actual,diff}.png and .prdoctor/run/score.json.

Commands

Command Does Exit codes
prdoctor pull Fetch Figma baselines for all (or --region) regions; version cache 0 · 2 config · 3 Figma/API
prdoctor check pull (unless --no-pull) → serve/attach → capture → compare → artifacts (+ PR comment in CI) 0 pass · 1 over tolerance · 2 config · 3 env/infra
prdoctor fix check; on fail, agent edits within fix.boundary, recheck ≤ maxRounds 0 fixed/pass · 1 still failing · 2/3
prdoctor e2e Run configured Playwright specs with video; stage under .prdoctor/run/e2e/ 0 · 1 test fail · 3 env
prdoctor report (Re)post PR comment from existing artifacts 0 · 3

Flags of note:

  • check --no-pull — use cached .prdoctor/baseline/ only
  • check --region <name> / pull --region <name>
  • check --at <versionId> / pull --at <versionId> — pin Figma file version
  • fix --push — push fix commits (CI)
  • report --dry-run — print markdown without posting

Global: --config <path>, --cwd <path>.

Exit codes (uniform)
Code Meaning
0 Pass
1 Quality gate failed (diff over tolerance, or e2e test fail)
2 User/config error (bad config, size-mismatch design vs capture, missing token when required as config-class)
3 Environment / infra (Figma API, browser, network, serve timeout)

A Figma outage or 429 is exit 3, not a red visual gate. Size mismatch is exit 2 — no silent resize.

score.json shape: { regions: [{ name, diffPixelRatio, pass, dims, figmaFileVersion? }], pass } — real ratios only, no display inflation.

Media modes (PR comments)

Controlled by report.media (default auto):

Mode Behavior When
auto public repo → branch; private → artifact default
branch push media to orphan branch prdoctor-media; inline via raw URLs public repos only for reliable inline images
artifact scores table + link to Actions artifact (no true inline images) private repos, zero setup
s3 upload to S3/R2; embed signed URLs (default TTL 7d) private repos wanting inline

Private-repo honesty: GitHub’s image proxy fetches raw.githubusercontent.com unauthenticated, so branch-hosted images break in private repos. There is no supported bot API for PR-comment image attachments. Use artifact (default via auto) or s3 for private repos.

Fix loop

{
  "fix": {
    "enabled": true,
    "maxRounds": 2,
    "boundary": ["src/components/**"]
  }
}
export PRDOCTOR_API_KEY=… PRDOCTOR_MODEL=…
prdoctor fix          # local
prdoctor fix --push   # CI: push commits with PR-Doctor-Fix: trailer
  • Edits outside boundary are rejected.
  • Hard gate: a push that still fails tolerance exits 1 (not “soft green”).
  • Re-entry: workflows should skip the fix stage when the head commit message contains PR-Doctor-Fix: (the composite action does this).

E2E video

{
  "e2e": {
    "enabled": true,
    "specs": "e2e/**/*.spec.ts"
  }
}

Runs the repo’s Playwright specs with video on; stages webm (+ gif/mp4 when ffmpeg is available). E2E failure is exit 1. Teams without specs leave e2e unset / disabled.

GitHub Action

Thin wrapper: npx pr-doctor@<version> — pin version for reproducibility.

# .github/workflows/pr-doctor.yml
name: PR Doctor
on:
  pull_request:
    types: [opened, synchronize, reopened]

concurrency:
  group: pr-doctor-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

jobs:
  visual-gate:
    runs-on: ubuntu-latest
    permissions:
      contents: write   # fix --push / media branch
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: 1infra/1doctor/action@main   # or path to this action/
        with:
          figma-token: ${{ secrets.FIGMA_ACCESS_TOKEN }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          # config: prdoctor.config.json
          fix: "false"
          e2e: "false"
          version: latest
          # api-key / model — only if fix: "true"

Action steps: re-entry guard → check → optional fix --push on exit 1 → optional e2e on success → best-effort report → always upload .prdoctor/run artifact.

Troubleshooting

Symptom Cause / fix
403 / “token expired or invalid” PATs expire ≤90 days. Regenerate FIGMA_ACCESS_TOKEN. On Org/Enterprise prefer a plan access token not tied to one employee.
429 rate limit Dev/Full seat required; View/Collab is unusable. Cache by file version reduces Tier-1 spend. Wait / Retry-After, or commit .prdoctor/baseline/ as offline fallback (warn path).
size-mismatch (exit 2) Design PNG dims ≠ capture dims. Align Figma frame size, scale / deviceScaleFactor, and region selector/viewport. Silent crop/resize is refused so layout bugs stay visible.
Gate red, tiny AA/text diffs Expected Figma-vs-Chromium differences; tune compare.threshold and maxDiffPixelRatio. Bit-identity is a non-goal.
Dynamic content noise Use region mask selectors and waitFor; capture already sets reducedMotion: reduce.
Preview never ready (exit 3) Raise capture.serve.readyTimeoutMs; check serve logs (timeout includes last output lines).
Private PR images broken media: branch cannot inline in private repos. Use auto/artifact or s3.

License

MIT

Keywords