Consumes pre-classified visual-diff JSON and a reviewer-signed acceptance log to produce a single go/no-go CI verdict for visual regression. Blocks when intentional baseline changes lack a non-author reviewer sign-off or when regressions are present, and emits a markdown + JSON artifact for the CI step. Use this skill when the gate's input is pre-classified diff data and the enforcement concern is reviewer approval, not when the goal is fanning out to multiple engines (use a multi-engine CI orchestrator for that).
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Low
Low-risk findings worth noting
A typical visual-regression CI run produces an engine-specific verdict (Chromatic exit code, Percy build status, Playwright snapshot pass/fail). That's not enough for a strict gate, because:
1
on changes (review needed), Percy returns success but flags the build
pending, Playwright fails the test outright.--update-snapshots
commit is a self-approval; for safety-relevant components it should
require a sign-off.This skill defines a build-an-X workflow that consumes both the diff classifier output and an explicit acceptance log to emit a single go/no-go verdict.
data-quality-gate in the
qa-data-quality plugin, for data quality).--update-snapshots commit by the PR author cannot
self-approve a baseline change to a critical component.If the project uses one engine only and trusts engine-native review (e.g. all changes go through Chromatic UI approval), prefer the engine's native CI integration - see the matching engine's "CI integration" section in its SKILL.md.
category in intentional | incidental | regression) and
the reviewer-committed .visual-acceptance.yml (see Inputs)..visual-acceptance.yml was authored by someone other than the PR
author (see Author cannot self-approve).The gate consumes two inputs:
Classification artifact - the pre-classified diff JSON, one record per snapshot:
{
"snapshot": "dashboard-mobile-375",
"engine": "playwright",
"category": "intentional|incidental|regression",
"pattern": "text-truncation|...|null",
"paired_change": true,
"diff_url": "playwright-report/data/dashboard-mobile-375-diff.png"
}Acceptance log - a YAML file at .visual-acceptance.yml
committed by reviewers (NOT the PR author) that explicitly accepts
each intentional baseline change for the current PR:
# .visual-acceptance.yml - committed by reviewers in the PR's review pass
pr: 1234
accepted_by: reviewer-handle
accepted_at: 2026-05-04T12:00:00Z
snapshots:
- snapshot: dashboard-mobile-375
reason: "Intentional CTA color change per design spec DS-456"
- snapshot: pricing-tablet-768
reason: "Intentional pricing tier rename"The acceptance file lives in the PR branch; merging the PR records the acceptance in git history.
def visual_gate(classifications, acceptance_log, *,
require_reviewer_acceptance=True):
accepted = {s["snapshot"] for s in acceptance_log.get("snapshots", [])}
blockers = []
for c in classifications:
if c["category"] == "regression":
blockers.append((c, "regression - blocks unconditionally"))
elif c["category"] == "intentional" and require_reviewer_acceptance:
if c["snapshot"] not in accepted:
blockers.append((c, "intentional - missing reviewer acceptance"))
elif c["category"] == "incidental":
# incidental requires investigation but does NOT block by default
pass
return {
"verdict": "no-go" if blockers else "go",
"blockers": blockers,
"incidentals": [c for c in classifications if c["category"] == "incidental"],
"intentional_accepted": [c for c in classifications
if c["category"] == "intentional"
and c["snapshot"] in accepted],
}Default behavior:
.visual-acceptance.yml.For low-risk projects, set require_reviewer_acceptance=False so
intentional changes pass without explicit acceptance - this collapses
the gate to "block on regressions only."
For a stricter gate, validate that the commit adding .visual-acceptance.yml
was authored by someone other than the PR author:
ACCEPTANCE_AUTHOR=$(git log --format='%ae' -1 .visual-acceptance.yml)
PR_AUTHOR=$(gh pr view --json author --jq '.author.login + "@..."')
if [[ "$ACCEPTANCE_AUTHOR" == "$PR_AUTHOR" ]]; then
echo "ERROR: PR author cannot self-approve baseline changes"
exit 1
fiThis is the visual-regression analog of GitHub's "require approval from someone other than the last committer" branch protection.
Run the gate on one build. The classification step emitted three records, and one reviewer committed an acceptance log:
classifications = [
{"snapshot": "dashboard-mobile-375", "engine": "playwright", "category": "regression"},
{"snapshot": "pricing-tablet-768", "engine": "percy", "category": "intentional"},
{"snapshot": "hero-desktop-1280", "engine": "chromatic", "category": "intentional"},
]
acceptance_log = {"snapshots": [{"snapshot": "hero-desktop-1280",
"reason": "Intentional hero copy update per DS-789"}]}
result = visual_gate(classifications, acceptance_log)Walking the rule per record:
| Snapshot | Category | In acceptance log? | Outcome |
|---|---|---|---|
| dashboard-mobile-375 | regression | n/a | block - regressions always block |
| pricing-tablet-768 | intentional | no | block - missing reviewer acceptance |
| hero-desktop-1280 | intentional | yes | pass - reviewer signed off |
result["verdict"] is "no-go" (two blockers), so the CI step exits
non-zero and the merge is held. The gate prints:
# Visual Baseline Gate - verdict: NO-GO
- playwright :: dashboard-mobile-375 :: regression - blocks unconditionally
- percy :: pricing-tablet-768 :: intentional - missing reviewer acceptanceTo turn this build green, a non-author reviewer either fixes the
dashboard-mobile-375 regression or, if the diff is actually intended,
adds both snapshots to .visual-acceptance.yml and re-runs the gate.
The full markdown + JSON artifact and the CI wiring that surfaces this
output are in
references/artifact-and-ci-wiring.md.
percy-visual-regression-testingchromatic-visual-regression-testingplaywright-snapshotsstorybook-visual-regression-testingvisual-baseline-conventions - the conventions this gate enforces.data-quality-gate - sibling gate skill for data-quality results, same artifact shape.