-
Notifications
You must be signed in to change notification settings - Fork 0
Review 5611
#5611 fix(ci): bind recovery capture to merged SHA by cixzhang (bucket: the maintainer)
eb5c6a7fa06fda65c6b0ef6f0db26faf340bf6ef <- every claim below was verified at this commit
LOOP VERSION: 1.6.0 AUDIT RUBRIC: 1.13
LANE: full
WHY: the diff sits in a pull_request_target privileged promotion job carrying allow-unsafe-pr-checkout: true — a trust boundary, which fast lane excludes outright.
PRIOR REVIEW: none on the PR. pulls/5611/reviews → empty; pulls/5611/comments → empty; the only issue comments are the Vercel and PR-Analysis bots. Nothing to extend or contradict. The blocker being reconciled is an unposted loop draft from an earlier head, so it is checked by mutation (M1) rather than quoted.
RISK CLASS: low-risk, all four conditions answered — no new API surface (no prop, export, type or theme target); no behaviour regression (the only behaviour that moves is a lane that currently always refuses); no performance or resource regression (zero Effects, listeners, deps, bundle delta); nothing got bigger (nothing renders).
COMMIT TYPE: fix(ci): is honest. It claims a CI fix with nothing consumer-visible, and the diff is one CI env key plus tests — no package, component, story or style file.
WHY 1: The trusted recovery recapture stamped its manifest with the recovery run's own commit instead of the historical merge it had just checked out and built. WHY 2: Promotion compares that stamp to the resolved merge SHA and refuses on mismatch, so the recovery lane cannot finish. WHY 3: The visual gate exists so a human's "yes" to a frame is the thing that reaches the baseline. When the only recovery path always refuses, that "yes" is spent and discarded and the frames stay out of the baseline.
USER-FACING PROBLEM: A maintainer dispatching recovery for #5162 cannot complete it by any route, and the 124 frames a human already reviewed stay out of the baseline — so the next contributor whose PR touches those keys is shown 124 "added" frames that were already approved, and has to review them again.
PROBLEM SEVERITY: broken task — the dispatch has no path to success.
run 33102388128 workflow_dispatch conclusion: failure
failing step: "Verify and promote the baseline"
Error: visual acceptance refused: capture was produced for
e6507a95fb13cae83f94e2d4b58009eaf584f1c6 <- current main at the time
not e1079ca567ce5e3a32ad0d145776385e455ce7fc <- the resolved merge for #5162
VERDICT: clear
The recapture has to tell the gate which commit it photographed. It was reading the workflow run's own commit, which on a recovery dispatch is whatever main is today, not the historical merge being recovered. The workflow now states the resolved merge explicitly, through the override the PR-time capture already uses. Promotion's identity check is untouched, so a capture that still names the wrong commit is still refused.
SOLUTION (1 decision · 1 runtime line of 82 added)
1. recapture identity comes from ASTRYX_VISUAL_SHA, not a shadowed GITHUB_SHA [the fix]
visual-acceptance-promote.yml:317 — traces to the stated problem
The other 81 added lines are two tests and one fail-closed mutation case. All four deletions are mechanical — the env key, a helper split (workflowStep extracted from workflowStepScript), and one fixture destructure widened by capture — so no assertion is removed or loosened anywhere, and deletions: 4 accounts for every one.
BURDEN: low — one env: key in one step. No runtime state, Effect, listener, timer, shared coupling or public surface.
BURDEN MATCH: proportionate.
VERDICT: clear
OWNER: captureIdentity() (gate.mjs:50-55) owns capture identity; the workflow owns which commit
TIER 1: reuses the existing ASTRYX_VISUAL_SHA override — no new mechanism
TIER 2: none
SEAMS: the two lanes reaching this step — pull_request_target types:[closed], workflow_dispatch
BEHAVIOR UNIT: n/a — a workflow env binding, no component behaviour
The implementation lives with its owner: gate.mjs:51 already reads ASTRYX_VISUAL_SHA ?? GITHUB_SHA ?? null, the override predates this PR, and pr-comment.yml:302 already drives it — so the finding a first read invites, "this invents a private env contract", is dead on the grep.
Seams, proved two ways because Actions cannot be executed here:
SEAM RESULT
workflow_dispatch (recovery) driven in production — run 33102388128 failed on exactly this
identity check; the fix binds the value it wanted
pull_request_target closed both triggers present, and the recapture step occurs exactly
once, so the lanes share it (asserted at test:347)
fork PR merge .visual-merged-source is a separate path with
allow-unsafe-pr-checkout; control scripts run from the trusted
current-main checkout at :192-195. Unchanged by this PR
Is there a next place the same failure can happen? No. There are exactly two gate.mjs capture invocations in the whole workflow directory, and after this PR both name their own identity — pr-comment.yml:302 and visual-acceptance-promote.yml:317. The class is closed.
Sweep that ran clean: pr-comment.yml sets three identity vars and this step sets one. Deliberate, not an omission — the skipped-evidence path checks all three (visual-acceptance.mjs:204-210, guarded by fail('skipped evidence run identity mismatch')), the promote path checks only context.sha (:798), and the recapture really was produced by the run whose id it reports.
VERDICT: clear
Nobody's pixels move and no app changes. Two people are reached, both ours: the maintainer running recovery, whose dispatch currently cannot finish and after this reaches the pixel comparison; and the next contributor whose PR touches those 124 keys, who is shown frames a human already accepted on #5162 and asked to review them again.
verified read-only against gh-pages
acceptance record for #5162 124 keys · all kind: added · all with an AFTER hash
· all with no baseline preimage
derived plan 124 entries, key-for-key identical to the record
today's baseline 974 shots — 0 of the 124 present
What landing this newly exposes: nothing. The failure was fail-closed, and the production log proves it: promotion refused the mismatched capture rather than writing it. No baseline was corrupted; the bug cost a red run, not bad pixels.
VERDICT: clear
no API change. No prop, export, type, flag or CLI argument is added or altered.
Ossification: none. ASTRYX_VISUAL_SHA is an internal CI env var with no consumer outside this repo, already read at gate.mjs:51 and already driven at pr-comment.yml:302. This PR adds a second consumer of an existing contract — a new instance of an existing idea, which passes on citation, not a new concept.
VERDICT: clear
n/a — structural only.
grep -cE "#[0-9a-fA-F]{3,8}|rgba?\(|hsla?\(|boxShadow|light-dark\(|stylex\.|xstyle"
.github/scripts/visual-gate/visual-promotion-workflow.test.mjs
.github/workflows/visual-acceptance-promote.yml → 0
No new theme targets.
VERDICT: clear
Is there a consumer who can be broken? These are repository CI files: no published package, no installed consumer.
- API — no. Nothing exported changed.
-
Visual — no. Zero of the two changed paths match
^packages/|\.stories\.|\.tsx$|\.css, and the PR Analysis Report reports "No new or modified components detected" and "No component packages changed". - Theme — no, per the grep above.
Behaviour — the recovery lane goes from always-refused to able to proceed. The states this diff can reach, walked:
| state | walked |
|---|---|
| correct merge SHA | capture stamps the merge; context.sha === mergeSha passes |
| wrong merge SHA | still refused — driven, mutation M3 |
| empty/absent override |
?? does not swallow '', so context.sha is '' and fails the !== check; gate-context.test.mjs already covers "preserves invalid explicit identity for downstream rejection" |
| empty · loading · error · disabled · boundary | not reachable — the diff adds no state, no default, no prop |
The only bound here is the identity equality, and it is driven from both sides: equal (M0, 234 pass) and unequal (M3).
VERDICT: clear
Effects: zero added, zero changed, zero moved — no React code is in the diff. No listener, observer, timer, dependency or bundle delta; the PR Analysis Report reports no component packages changed. CI cost is unchanged: same steps, same two installs, same Storybook build, one env: key renamed.
VERDICT: clear
VISUAL CHECK: not applicable
WHY: both changed paths are non-rendering — visual-acceptance-promote.yml (one env key)
and visual-promotion-workflow.test.mjs (a test). Zero component/story/style files in
the diff; style and role/aria greps return 0 on both files; the PR Analysis Report
reports no components and no component packages changed.
No frames, and the reason is the acceptable one: nothing rendered changes because nothing rendering is touched. The evidence carrying this review is executable instead, banked so it can be re-run — one control and three mutations, one worktree at eb5c6a7, restored clean after each.
repro: cd ~/astryx/worktrees/visual-recovery-merged-capture-sha
npx pnpm@11.10.0 exec vitest run \
.github/scripts/visual-gate[/visual-promotion-workflow.test.mjs]
M0 control, unmutated PASS 18 files / 234 tests (matches the PR body)
M1 gate.mjs manifest.context: replace
`...captureIdentity(),` with
`sha: process.env.GITHUB_SHA ?? null,` FAIL "expected 'manifest.context = {…' to
contain '...captureIdentity(),'"
M2 workflow :317 ASTRYX_VISUAL_SHA
reverted to GITHUB_SHA FAIL both new tests, on the
ASTRYX_VISUAL_SHA assertion
M3 visual-acceptance.mjs:798 guard
replaced with `if (false)` FAIL "the wrong merged capture identity"
stops failing closed
M1 answers the earlier blocker. Scoping the source assertion to the manifest.context slice (test:323-327) means a deliberate raw-GITHUB_SHA there is now caught; under a whole-file assertion it would not have been, because captureIdentity() also appears at gate.mjs:206 and :245.
VERDICT: clear
REMEDY SEARCH: not triggered — no proven visual defect
Nothing rendered, no strings, no roles, no direction.
grep -cE "aria-|role=|useTranslator|t\('@astryx" <both changed files> → 0
-
§1 automated — the PR Analysis Report says "No accessibility violations detected". Auto-covered, and honestly it is not evidence either way here:
pr-a11yscopes to changed components and none changed. - §1 manual — not reachable. No interactive element, focus path, announcement or keyboard surface is in the diff.
-
§9 strings and direction — no user-visible or AT-visible string added;
.github/a11y-baseline.jsonuntouched, so nothing buys silence.
Checked by me: the greps above and the changed-file list.
VERDICT: clear
PROBLEM clear API clear VISUAL clear
SOLUTION clear THEMING clear A11Y & I18N clear
ARCHITECTURE clear BREAKING clear
IMPACT clear PERFORMANCE clear
Read against the bodies, not the labels: no slot's prose contains a sentence about a person who is stuck, cannot reach something, hears nothing, or loses their place. The one harm sentence anywhere — a contributor re-reviewing 124 already-approved frames — is harm this PR removes.
GOAL: met, for the binding, which is what the PR claims. The production failure named the exact mismatch; captureIdentity() resolves ASTRYX_VISUAL_SHA first, so context.sha will equal merge_sha and pass the guard at visual-acceptance.mjs:798; M2 shows reverting the line reintroduces the condition the tests catch. What it does not claim, correctly: the pixel comparison after the identity check is unchanged and still has to pass on the next dispatch.
DISPOSITION: 1. the identity test never captures → note, not blocking, no follow-up — the assertions beside it cover the behaviour (M1 and M2 both fail if either half breaks).
ADVICE: proven existing pattern — gate-context.test.mjs:77 is the landed home for runtime identity precedence. Cited, not invented.
AUTHOR CAN PROCEED: yes — nothing blocked, nothing withheld.
WORST OUTCOME: "the next contributor whose PR touches those keys is shown 124 'added' frames that were already approved" → that is main today and the state this PR ends. Compatible with approve.
JUDGEMENT NEEDED: none — defect fix against the existing contract. No capability, no new concept, no new surface.
Verdict: approve.
1. [not blocking] the test named `captures the resolved historical merge` runs
`gate.mjs check --max-shots 0`, which takes the skipped-verdict branch and reads
verdict.json — no capture manifest is ever written
→ whoever opens this test to ask "is the recapture manifest bound to the merge SHA?"
reads an executed proof and gets a string match, and gate-context.test.mjs:77
already proves the same precedence the same way
· visual-promotion-workflow.test.mjs:331
Good — reverting the line reproduces the failure the tests catch. Nit: the identity test runs
check, so no capture manifest is written.
-
.github/scripts/visual-gate/visual-promotion-workflow.test.mjs:331— Hmm, this runscheck, notcapture.gate-context.test.mjs:77probably already covers the precedence.
-
#5608 overlap: none. No shared file; it does not touch
captureIdentity()or themanifest.contextblock this test string-matches (its nearest hunk adds an unrelatedlegacyBaselinePackagesconst). Merge order is free either way. - CI on this head is 7 runs, not the ~19 a component PR gets, because path filters exclude everything but the
.githublane;visual-acceptancesits pending and theVisual acceptancerun was cancelled. Expected for a CI-only diff, and the suite that matters was run locally (M0). - The failed run's log line is the whole diagnosis; it is quoted in PROBLEM and does not need re-arguing to her.
TIME total 14m
setup 1m worktree already at the exact head, node_modules present (warm main reused: yes)
reading 4m brief + critic (~6,900 lines of kit), PR body, diff, workflow, gate.mjs
measuring 5m 1 control + 3 mutation runs, 124-record verification, failed-run log, capture-site grep
writing 4m draft, two critic passes, rewrite
waste 1m two greps mangled by shell quoting and re-run; one probe of the acceptance
record written against a guessed JSON shape before reading the real one
- Whether the
pull_request_target types:[closed]lane was also mis-binding before this PR. GitHub documentsGITHUB_SHAthere as the base branch's last commit, which for a just-merged PR is usually the merge commit — so the close lane was probably right by coincidence and drifted only whenmainadvanced first. Read from docs, not driven. The fix removes the coincidence either way. - Whether a step-level
env: GITHUB_SHA:is ignored by the runner as a general rule, or was ignored only here. The production log proves it did not take effect on run 33102388128, which is all this PR needs; the general rule is unverified.