ci(storybook): report docs index changes on PRs - #5257
Conversation
Storybook's sidebar comes from a curated directory allowlist in .storybook/main.ts plus per-entry tags, so a docs page can go missing without anything turning red: move a component to an unlisted directory, drop `autodocs`, or add `no-sidebar`, and the page is simply gone. Snapshot the Storybook index on both sides of a PR and comment the diff. `storybook index` only runs the indexers -- no Vite, no bundling, no core build -- so a snapshot takes ~6s and this can run on every PR. Two failure modes get named explicitly: - A page still indexed but no longer reaching the sidebar (it lost `dev` or gained `no-sidebar`). Tests keep passing; nobody can find the page. - A docs page id is one slot that two mechanisms compete for. Attaching an .mdx to a component silently replaces its autodocs page: same id, same title, same URL, different content, props table and story previews gone. Nothing is added or removed, so an id-only diff sees no change at all -- comparing each entry's importPath is what catches it. Comparison runs against the PR merge commit's first parent, so pages that landed on main after the branch point are not reported as removed. Both sides are normalized by the same copy of the script, restored onto the base checkout, so a change to the tool cannot masquerade as a change to the docs. Non-blocking: deleting a page is often deliberate, and a gate that blocked merges on it would be routinely overridden until ignored. It reports via a PR comment and a workflow warning annotation instead. The check finishes long before a Chromatic build exists, so links start out pointing at the public Storybook and chromatic.yml rewrites them in place once a build for the PR is published -- the comment records its own link base, so nothing has to be carried between workflow runs. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔍 Review policy: Code changeDefault rule: any other change needs one approval from f0-devs (rule 4). Required approvals
How this was decided
Policy source: |
✅ No New Circular DependenciesNo new circular dependencies detected. Current count: 0 |
✅ Storybook docs — no pages lostEvery page reachable on Links point at this PR's Storybook build — browse the full Storybook. Snapshot of the Storybook index (docs pages + stories) compared against |
📦 Alpha Package Version PublishedUse Use |
🔍 Visual review for your branch is published 🔍Here are the links to: |
♿ Accessibility (axe) — components changed in this PR✅ No a11y issues in the stories this PR changed. Scope: only stories in the files/component folders this PR changed. It can't yet flag downstream ripple from shared-code/token changes, or diff against |
✅ No breaking public API changesNo public exports were removed, renamed, or had existing props/types changed in a breaking way compared to Comparing
|
Coverage Report for packages/react
File CoverageNo changed files found. |
Description
Storybook's sidebar is assembled from a curated directory allowlist in
.storybook/main.tsplus per-entry tags, which means a docs page can stop being exposed without anything turning red — move a component to a directory that isn't listed, dropautodocs, or addno-sidebar, and the page is simply gone. This snapshots the Storybook index on both sides of a PR and comments the difference, so losing a page is something a reviewer sees rather than something someone notices weeks later.Type of change
What gets flagged, and how it reads
Every example below is real rendered output, produced by running this check against actual commits in this repo — not mock-ups. Each is labelled with the comparison that generated it.
1. A docs page disappeared
From #5095 at its pre-fix commit (
9a6ac810, beforefix(F0Select): restore canonical docs route) against its merge-base. That PR addedtags: ["!autodocs"]plus an unattached MDX (<Meta title="Select/Overview" />), which moved the canonical Select docs page to a sub-route:The diagnosis on that first bullet is the literal root cause, and the misplaced replacement sits a few lines below it under Added — which is what makes the wrong-route conclusion quick to reach.
2. A page is still indexed but no longer reachable
From
mainagainstmain~150. These stories gainedno-sidebar, so their tests kept passing while no human could find them:3. A page survives but something else generates it now
From #5095 at its current head (
d9c11db7, after the fix) against its merge-base. The page's URL is intact, so an id-only diff sees nothing — only the source file changed:This one is worth reading closely, because it is a live example of real content loss: that MDX renders 3 story previews where autodocs rendered 45, and it has no
<Description />, so the component's description no longer appears. See the known limitation below.4. Informational categories, collapsed by default
A directory rename moves every story in it, so these group by file pair rather than listing 16 findings. Same mechanism on both sides, URLs unchanged, so it does not raise the warning heading. From
mainagainstmain~150:✏️ Updated,🏷️ Maturity changedand🔇 Added but not in the sidebarare collapsed the same way.5. Nothing lost
The state this PR itself is in — see the live comment below:
Reported on every run rather than staying silent, so a previous⚠️ resolves back to ✅ once fixed instead of leaving a stale warning.
The one-line summary
The counts line is always complete even when a section's list is capped:
Known limitation
Case 3 tells a reviewer where to look, not how much was lost. The index records that a page exists and which file backs it, never which stories that file renders — so the check cannot currently say "3 previews where autodocs rendered 45". Closing that means parsing the MDX for
<Canvas of={…}>/<Stories />and comparing against the stories the autodocs page covered. Deliberately left out of this PR; #5095 is a ready-made regression test for it.The check also verifies a page is indexed and reachable, not that it renders. A page whose MDX throws at runtime would pass.
Implementation details
ci: add
Storybook Docs Indexworkflow — snapshot the index for the PR and formainin parallel, diff them, comment the resultWhy this is cheap enough to run on every PR
storybook indexonly runs the indexers — no Vite, no bundling, and (verified) nof0-corebuild. A full snapshot of ~2,750 entries takes about 6 seconds, so the job cost is essentiallypnpm install. This does not build Storybook. Measured in CI on this PR: 1m8s and 1m15s for the two snapshot legs in parallel, 1m16s for the diff.feat: add
check-docs-index.ts— normalizesstorybook indexoutput into a snapshot (identity, sidebar visibility, source-file hash) and classifies the difffeat: report a page that is still indexed but no longer reaches the sidebar
Why this needs its own category
An entry reaches the sidebar only if it is tagged
devand not taggedno-sidebar(this repo's filter, applied in.storybook/manager.ts). Losing either leaves the entry in the index — so its tests keep passing and nothing looks broken — while no human can find the page. Presence alone can't detect this, so visibility is tracked per entry.feat: detect a docs page whose source was replaced rather than edited
The case an id-only diff cannot see
A docs page id is a single slot that two mechanisms compete for:
autodocsgenerates the page, and an.mdxattached to the same component replaces it. Adding an MDX file therefore silently overwrites the auto-generated page — same id, same title, same URL, entirely different content. Nothing is added and nothing is removed, so a diff keyed on ids sees no change at all. Comparing each entry'simportPathacross both sides is what catches it.A same-kind relocation (
ResourceHeader→F0ResourceHeader) is classified separately and does not raise the warning.feat: name a dropped
autodocstag as the likely cause when a docs page vanishesHow the cause is inferred
Verified against the real index by temporarily setting
!autodocsonF0ActionBar: the--documentationentry disappears from the index outright, so it already surfaces as a removal. What the entry can't say is why — a deleted component takes its stories with it, whereas a dropped tag leaves them behind. That second shape is flagged explicitly, and it fired correctly on feat(F0Select): add inline variant #5095, whose root cause was exactly anautodocs→!autodocschange.fix: compare against the merge commit's first parent, not the branch point
Why
Both sides derive from the PR merge commit, so the PR is synced with
mainbefore either snapshot is taken. Snapshotting the branch as-is would report every page that landed onmainafter the branch point as removed. Same approach as the Public API Surface check.fix: normalize both sides with the same copy of the script
Why
The base leg restores
check-docs-index.tsfrom the merge commit before running. Otherwise the base side would use whatever version exists onmain— or none at all, on this PR — and any change to the tool would masquerade as a change to the docs. Confirmed working in CI on this PR, which is the bootstrap case.ci: retarget the comment's links at the PR's Storybook once Chromatic publishes one
How, and why not a computed permalink
This check finishes in about a minute; a Chromatic build takes several. So the comment is posted immediately against the public Storybook, and
chromatic.ymlrewrites every link in place once a build for the PR exists. The comment records its own link base in an HTML comment, so nothing has to be carried between workflow runs — no artifacts, noworkflow_runplumbing.Computing the Chromatic permalink from the branch name was rejected: branch slugs are truncated at 37 characters and collapse repeated dashes, and this repo's branch names exceed that. The published URL also turns out to be
<appId>-<buildHash>.chromatic.com, a different shape from the documented branch permalink — so a computed URL would have pointed somewhere else entirely. The URL comes from the Chromatic action's own output instead.Best-effort by design. If Chromatic wins the race, the comment keeps its public links and self-corrects on the next push.
chore: add
check:docs-indexscript for running the comparison locallyNon-blocking on purpose
Losing a page does not fail the gate. Deleting a page is often deliberate, and a check that blocked merges on it would be routinely overridden until it was ignored. It reports through a PR comment and a workflow warning annotation.
Verification
HEADvsmaincorrectly reports no changes;mainvsmain~150surfaces real losses, hidden pages and overwrites; and feat(F0Select): add inline variant #5095's pre-fix commit reproduces the exact regression it was opened to fixRetargeted docs comment links at https://66a7a8d7d124220c363457cc-dzcelszubf.chromatic.compackages/react, its own check runs on it — the comment below is this tooling reporting on itself🤖 Generated with Claude Code