Skip to content

feat(ui-visual): human-only approval for golden baseline changes; auto-merge refuses without it (warren-4780) - #1356

Merged
jayminwest merged 5 commits into
mainfrom
ui-visual/golden-approval-4780
Oct 9, 2026
Merged

jayminwest merged 5 commits into
mainfrom
ui-visual/golden-approval-4780

Conversation

@jayminwest

Copy link
Copy Markdown
Owner

Important

Article IX: this PR needs a human merge. It modifies .github/workflows/auto-merge.yml, a constitution-protected path, so the Article IX check refuses to arm auto-merge on it. That is expected and correct. Please review and merge by hand. Nothing in this PR works around that.

Plan pl-10db step 11 (seed warren-4780). Builds on the golden baselines from #1354 (warren-a132).

Why

#1354 changed 61 baseline paths, and auto-merge.yml armed it. It merged on green CI, and no gate asked a human to look at the pixels. Any agent PR can do the same thing: break a page, regenerate the goldens in the same PR, and pass. This PR closes that path.

What changes

auto-merge.yml: a UI baseline approval check step (id baseline) next to the Article IX check. It uses the same fail-closed contract: the token mint and the arm require hit == 'false' from both gates.

  • Scope. The gate covers scripts/ui-visual/__golden__/** plus the files that decide how a render compares: golden-cases.ts, golden.pw.ts, golden-manifest.ts, goldens.ts, playwright.config.ts, and ui-visual.yml. A looser tolerance or a skipped comparison launders a regression just as a new PNG does.
  • The approval signal. The approval is the ui-baseline-approved label from a human approver. The gate never trusts label presence. It reads three facts from sources no bot can write:
    1. The labeler comes from the issue events API: the latest labeled or unlabeled event for that label. The actor must be type: User, must not end in [bot], and must not have acted through a GitHub App (performed_via_github_app). The actor must not be in AUTO_MERGE_BOT_LOGIN, and must be in UI_BASELINE_APPROVERS (a repo variable, which defaults to the repo owner). An empty approver list, or one that names a bot, refuses everything.
    2. The head the approver saw comes from the repository activity API. That API records pushes with server timestamps. The gate takes the after of the last push strictly before the label event. Commit dates are pusher-controlled, so they are never used.
    3. The gate compares the git object ids of every baseline path at that head with the ids at the current head. A later push that changes the baselines voids the approval. A push that changes only other files keeps it.
  • Tamper resistance. The step runs git show origin/<base>:scripts/ui-visual/baseline-approval.ts, the base-branch copy of the gate, so a PR cannot rewrite the gate that judges it. A PR that changes the gate script never auto-merges.
  • Disarm. On refusal, the step Disarm auto-merge on an unapproved baseline change turns off an earlier arm.
  • New triggers. labeled and unlabeled now trigger the workflow, filtered to this label, so approving arms right away and removing the label disarms.
  • Head check. The arm step now arms only the head the gates judged: it compares the current head and passes --match-head-commit. This closes the race between a label-event run and a newer push.

Why a label and not the alternatives (full reasoning in the design record):

  • Code-owner review approval. GitHub blocks self-approval. The workflow arms owner-authored PRs, and this repo has one maintainer, so the owner could never approve baselines in their own PRs. Required code-owner review in branch protection would block those PRs permanently.
  • An environment with required reviewers. It lives in settings outside git, where no test can check it. Each push queues a waiting deployment. The trust is the same, because an approver's token can approve a deployment through the API.
  • author_association. The events API does not report it, and MEMBER would admit an org-member machine account.
  • Against bot identities. The warren GitHub App (it has pull-requests write, so it can add the label) and github-actions[bot] fail the User-type and [bot] checks. The AUTO_MERGE_BOT_LOGIN machine account is a User, so the gate denies it explicitly.

The seed's label-triggered regenerate-and-commit workflow is deliberately not built. That workflow would put the approval before the human sees the diff, and it would add a second writer to the PR branch. The human path uses the existing dispatch mode from #1354 instead:

  1. gh workflow run ui-visual.yml --ref <branch> -f update_goldens=true, then download ui-goldens-<sha> into __golden__/, commit, and push. An agent may do this step.
  2. Review the diff: GitHub's rich PNG diff, plus the golden-diff/ images in ui-screenshots-<sha>. The warren-70d9 crops will appear here once that step lands.
  3. Apply ui-baseline-approved. The label event re-runs auto-merge, which arms.

Other files.

  • scripts/ui-visual/baseline-approval.ts holds the pure logic plus thin IO, under 400 lines, with no non-node: imports.
  • baseline-approval.test.ts covers classification, the actor checks, head placement, and the decision.
  • baseline-approval.workflow.test.ts runs the embedded step shell against a scratch repo, as article-ix-gate.test.ts does. It covers the base copy versus the PR copy, a crash after hit=false, and a base that has no gate.
  • .github/labels.yml adds the label. sync-labels creates it on merge.
  • .github/CODEOWNERS routes review requests only and enforces nothing.
  • docs/design/ui-visual-gate.md is the new design record, with its catalog row. scripts/ui-visual/README.md gains an "Approve the baselines" section.

Names for warren-dbef (stable, documented in the record)

  • The status check ui-visual.
  • The job enable-auto-merge.
  • The step ids protected (Article IX) and baseline. Each writes hit=true|false.
  • A new gate adds a step and joins the if: of the mint and arm steps.

Proposed Article IX amendment (for a human to apply; docs/CONSTITUTION.md is not edited here)

Add after the "Executable form" paragraph:

The golden screenshot baselines in scripts/ui-visual/__golden__/ and the files that compare against them change only with a human's approval. The "UI baseline approval check" step in the same workflow refuses auto-merge on such a PR unless a human approver applied the ui-baseline-approved label to the baseline content the PR carries. An agent does not redraw the picture it is measured against.

Known limits

  • An agent that holds an approver's own personal token is that approver to GitHub. The gate stops the identities warren hands to agents: the App installation token and the machine account. Local sessions that run gh as the owner must not apply the label.
  • A pull_request workflow runs from the PR's merge commit, so a PR that edits auto-merge.yml can drop the step for itself. Article IX already forces a human merge there.
  • Warren-armed pr.autoMerge (reap-time arming) does not run this gate. This repo does not opt in today. If it ever does, add the baseline paths to protectedPaths.

Validation

Follow-ups

  • Article IX arms already made are not disarmed when a PR later gains a protected path. That is the same hole this PR closes for baselines, and it is worth a seed.
  • warren-598d should name this gate in the AGENTS.md UI conventions.

…o-merge refuses without it (warren-4780)

auto-merge.yml gains a UI baseline approval check next to Article IX. A PR
that changes scripts/ui-visual/__golden__/ or the comparison machinery arms
only when a human approver applied ui-baseline-approved to the exact baseline
content the head carries. The gate reads the labeler from the issue events
API (never label presence), the head the approver saw from the repository
activity API, and compares baseline object ids. It runs the base branch's
copy of scripts/ui-visual/baseline-approval.ts, fails closed, and disarms on
refusal. The arm step now also refuses to arm a head the gates did not judge.

Adds the label, CODEOWNERS routing, and docs/design/ui-visual-gate.md.
@jayminwest
jayminwest enabled auto-merge (squash) October 8, 2026 16:38
…e (warren-598d) (#1361)

* feat(ui-visual): design-review workflow runs the evaluator on UI PRs and reports a design-review check (warren-a694) (#1359)

.github/workflows/ui-design-review.yml runs the ui-design-review skill
through claude-code-action (pinned by commit) after each ui-visual run,
validates the verdict with scripts/design-review/verdict.ts, and reports
the result as the `design-review` check run on the PR head plus one sticky
comment keyed by <!-- design-review -->.

- gate (pull_request_target): every PR head into main gets the check at
  once, success "Skipped: no src/ui changes" or queued, so the check
  warren-dbef makes required never hangs.
- prepare (workflow_run): trusts the artifact's PR number only when that
  PR's head is the run's head sha, maps changed files to manifest pages,
  stages only in-scope screenshots and the src/ui diff.
- evaluate: contents: read token, scoped tools, no web tools, fixed
  prompt with no PR text, model/turn/budget caps from repo variables.
- report: re-validates in a fresh checkout and fails closed on a missing
  or invalid verdict.

Logic lives in pure, unit-tested modules under scripts/design-review/.

* feat(auto-merge): require ui-visual and design-review success for UI PRs before arming (warren-dbef)

* docs(ui): AGENTS.md UI conventions section and a UI definition of done (warren-598d)

Adds a UI conventions section to AGENTS.md that pairs each rule with the
check that holds it: the Tailwind arbitrary-value ratchet, the GritQL
primitive and inline-style bans, check:ui-visual, the golden baseline
approval label, and the design-review rubric. Folds the UI merge-gate
paragraph out of Protected paths into it and moves the repro-first
section next to it. The PR template gains a collapsed UI definition of
done, and src/ui/README.md links both UI design records.
@jayminwest
jayminwest enabled auto-merge (squash) October 9, 2026 14:51
@github-actions
github-actions Bot disabled auto-merge October 9, 2026 14:52
@jayminwest
jayminwest merged commit 9595340 into main Oct 9, 2026
13 checks passed
@jayminwest
jayminwest deleted the ui-visual/golden-approval-4780 branch October 9, 2026 15:15
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