Skip to content

ci(visual-gate): build the theme dists the gate needs instead of crashing - #5514

Merged
cixzhang merged 1 commit into
mainfrom
ci-visual-gate-selfheal
Aug 26, 2026
Merged

ci(visual-gate): build the theme dists the gate needs instead of crashing#5514
cixzhang merged 1 commit into
mainfrom
ci-visual-gate-selfheal

Conversation

@cixzhang

Copy link
Copy Markdown
Contributor

Problem

pr-visual is red on 15 of the 21 open PRs where it ran (71%), and none of
the failures are about anything those PRs changed. Two different errors, both
naming a file the contributor never touched:

Error: Theme butter is not built (packages/themes/butter/dist/source.mjs missing) — run pnpm build before the visual gate.
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '.../@astryxdesign/core/dist/theme/index.js'

It is a soft gate, so people merge through it — which is worse than the gate not
existing, because a red check nobody reads is a red check nobody reads when it is
right.

Why, exactly

The gate needs two build products it does not produce: each theme's
dist/source.mjs (a theme's component map is what defineTheme returns, not a
literal in its source) and packages/core/dist, which a built theme's own
import '@astryxdesign/core/theme' resolves into through the workspace link.
They reach the runner as an artifact from build-storybook, and how that
artifact is named and filled changed three times in twenty-four hours
(#5477,
#5481,
#5482). Each intermediate shape
left the gate with nothing to read, and the gate's response to that was to
throw.

A PR does not run the ci.yml on main. It runs the one on its own merge
commit, and that merge commit is frozen when the pull_request event fires.

Re-running the job replays the same payload, so a PR whose last push predates a
fix re-runs the pre-fix CI forever, and the error tells you which snapshot it is
stuck on. This is not base staleness:
#5455 is 25 commits behind main
and green, because it was pushed after the last fix landed; the failing PRs are
10–14 behind. Any push refreshes the snapshot — merging main is just one kind of
push.

So the backlog drains itself as people push. The defect worth fixing is that a
wiring change to a CI artifact can redden a gate at all.

The two digest-mismatch: error lines are a red herring

They are actions/download-artifact@v8 echoing its own digest-mismatch input
(default error) in the with: block it prints before every download. Nothing
mismatched — both digests verified. The lines are worth exactly one thing:
count them. Two means the run is on the pre-#5477 workflow; three means
post-#5477. Not ours to fix, and nothing in this PR touches it.

Solution

Four decisions:

  • The gate builds what it cannot import, instead of throwing. Core first,
    since every built theme imports it; then only the themes actually missing.
  • Per-package, not a root pnpm build. The gate needs eight small packages,
    not the whole workspace, and it says which one it is building and why.
  • The error, when a rebuild does not help, says it tried — and distinguishes
    a broken build from a missing one, which the old message could not.
  • The artifact download is continue-on-error. A crash there happens before
    the gate runs, so the recovery would never get its turn — that is precisely how
    a rename reddened every open component PR.

The artifact stays: it is the fast path. It is now an optimisation rather than a
correctness dependency.

Impact

Every component PR, contributor and maintainer alike. The gate stops being able
to fail for a reason that is not about the PR.

Already-red PRs are not retroactively fixed — nothing landed on main can
reach a run frozen to an older merge commit. They go green on their next push.

API

None. loadThemeOverrides gained an optional third parameter as a test seam;
it is an internal CI script with two call sites, both in gate.mjs.

Usage

// production — unchanged
const overrides = await loadThemeOverrides(REPO_ROOT, config.probeTheme);

// tests inject the builder instead of spawning pnpm
await loadThemeOverrides(root, 'probe', (_root, pkg) => builds.push(pkg));

Theme targets

None.

Ossification

The build seam is a third positional parameter on a module under
.github/scripts/, imported by one file in this repo and published nowhere. If
it is wrong, changing it costs one call site and a test. Not a class decision —
"inject the expensive side effect" is what the existing ensureCoreBuilt helper
does for the same build, and I deliberately did not import that one: it is
declared a test helper and locks against parallel Vitest workers, a hazard the
gate does not have.

Breaking

  • API — none.
  • Visual — none. The gate's verdict is computed from the same theme data.
  • Theme — none.
  • Behaviour — one: a gate run that used to abort with "not built" now builds
    and continues. That is the point of the change.

Performance & resources

Effects: one pnpm -F <pkg> build per missing package, on the failure path only.

Measured on this branch, from a fully bare tree (a state CI never actually
reaches, since build-storybook's artifact or the job's own core build normally
covers it):

starting state what the gate builds wall clock
everything present nothing 0s
no theme dists 8 themes 9.5s
no theme dists, no core dist core + 8 themes 20.8s

No cost on the happy path — the first import succeeds and nothing is spawned.

Visual evidence

None; this is CI plumbing and changes no rendered pixel. The evidence is the job
log, in a comment below.

Judgement

Slot Verdict
Problem clear
Solution clear
Impact clear
API clear
Theme targets none
Ossification note
Breaking clear
Performance clear
Visual evidence none

What I could not verify

  • Whether build-storybook's core dist in the artifact is now dead weight.
    #5482 rebuilds core in
    pr-visual unconditionally, which makes
    #5481's core half redundant —
    ~12s and an artifact's worth of bytes on every component PR. Flagging rather
    than removing: dropping a safety step in a PR about not depending on that step
    is the wrong trade, and it is a clean follow-up.
  • Nothing retroactive. The 15 red PRs stay red until each is pushed.

@vercel

vercel Bot commented Aug 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 25, 2026 9:40pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

…hing

The gate reads each theme's built source, which reaches the runner as an
artifact from the job that already built it. Three times in twenty-four
hours a change to how that artifact is named or filled left the gate with
nothing to read, and it died on a missing-file error naming a path no PR
had touched — on 15 of the 21 open PRs where it ran.

It builds what it cannot import now: core first, since every built theme
imports its dist, then only the themes that are actually missing. A bare
tree recovers in 21s and an intact one pays nothing. The download step is
non-fatal for the same reason, so an artifact rename can no longer kill
the job before the gate is reached.
@cixzhang
cixzhang force-pushed the ci-visual-gate-selfheal branch from 8106c2d to 765b432 Compare August 25, 2026 21:37
@cixzhang

Copy link
Copy Markdown
Contributor Author

Proved on CI, not locally

pr-visual skips a workflow-only PR by design, so this branch carried two
throwaway commits — a one-line touch on Kbd to arm the gate — which are now
dropped. Both runs are on the real job.

1. The recovery path, run
32900359861 on
f1aa4e4. That commit additionally pointed the download at an artifact name
nothing produces and deleted the unconditional core build, so the gate started
from nothing:

##[error]Unable to download artifact(s): Artifact not found for name: dists-THROWAWAY-FORCE-RECOVERY-f1aa4e4
...
Components: Kbd
visual gate: packages/core/dist/theme/index.js is missing and every built theme imports it; building @astryxdesign/core here rather than failing.
visual gate: packages/themes/butter/dist/source.mjs is missing; building @astryxdesign/theme-butter here rather than failing.
   … chocolate, gothic, matcha, neutral, probe, stone, y2k
Visual gate: 16 shots (component)
  captured 16/16
## Visual gate: pass

On main today that same job dies with Theme butter is not built. Recovery
cost: 48s on the 2-core runner (core ~28s, eight themes ~20s), then a normal
gate run.

2. The normal path, run
32901113865 on
8106c2d — artifact wiring restored, Kbd still touched. Three artifacts
downloaded, no visual gate: lines at all (the first import succeeds and
nothing is spawned), ## Visual gate: pass, 16/16.

One thing the diagnosis changed

The failures are not about a stale base — they are about a stale event. A
pull_request run is frozen to the merge commit computed when the event fired,
and re-running replays it, so a PR runs whatever ci.yml its own last push
merged with.
#5455 is 25 commits behind
main and green
; the red ones are 10–14 behind. Any push refreshes it; merging
main is just one kind of push, and re-running the job never does.

github-actions Bot added a commit that referenced this pull request Aug 25, 2026
github-actions Bot added a commit that referenced this pull request Aug 25, 2026
@cixzhang
cixzhang merged commit fb17bfe into main Aug 26, 2026
21 checks passed
@github-actions
github-actions Bot deleted the ci-visual-gate-selfheal branch August 27, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant