Skip to content

ci(docs): Build Docs runs the production build command, read from vercel.json - #11265

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-10642-build-docs-gate-divergence
Aug 23, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/issue-10642-build-docs-gate-divergence

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #10642

The divergence, measured

Not the path filter. The docs filter already lists content/** and is fail-open, so the greens on epic #10206 were real runs — that candidate was already struck in the card's unblock note and I did not re-derive it.

The divergence is that Build Docs never ran the production build at all. It ran pnpm --filter @objectstack/docs build, a second, hand-maintained spelling of it. Production runs whatever apps/docs/vercel.json declares, and nothing in this repo read that file — a grep -rl vercel across every workflow, script and config returns apps/docs/vercel.json itself, apps/docs/next.config.mjs (a comment naming it), apps/docs/redirects.mjs, and four unrelated files about the Vercel AI SDK. No reader. So the gate's command and the shipped command were free to drift apart with nothing to notice, which is the general form of the specific failure the card recorded.

A gate that re-implements the thing it guards can only ever test the re-implementation.

What this changes

.github/workflows/ci.yml, the Build Docs job:

  1. The command is read from apps/docs/vercel.json instead of copied. The step runs with working-directory: apps/docs, because Vercel runs buildCommand from the project's Root Directory — which is why the stored command opens with cd ../... Changing how production builds now changes this gate in the same commit, by construction. That is the part that survives the next divergence rather than pinning today's.
  2. .next/BUILD_ID is asserted afterwards. Exit code 0 is not evidence the app was built: pnpm --filter <name> with a filter matching no package prints No projects matched the filters and exits 0 (measured below). Vercel's symptom for that is a deploy-time No Output Directory found, i.e. after the gate has already reported green.
  3. TURBO_FORCE: 'true' — the one deliberate difference from production, and it is load-bearing. @objectstack/docs#build inherits turbo's generic build task, whose inputs are the package's own 35 files, none under content/. Appending a line to content/docs/index.mdx left the task hash at 4e0c51cb3db203a8 — a cache HIT. A content-only PR is the normal shape of a docs change and precisely the shape this gate must be able to fail, so the gate must not be satisfiable by a replayed artifact. The same hole on the production side is a different defect and is filed separately as @objectstack/docs#build declares no input under content/, so a content-only commit is a turbo cache HIT — the production build command can replay a stale .next #11264; it is not addressed here.

The acceptance test: manufactured breakages, and what the gate did

The card's suggested lever — re-introducing output: 'standalone' — is gone (PR #10652 removed it and next.config.mjs now explains why it must not return), so it was not used. Instead the breakage is applied to the file that defines the production build. Both fixtures are ephemeral: applied locally, proven on disk, reverted by an EXIT/INT/TERM trap. Nothing in this diff is a fixture. The legs execute the actual run: bodies extracted from ci.yml with the YAML parser, not a paraphrase of them.

Breakage A — the production build command names a package that no longer exists (a rename or typo in vercel.json; production build fails):

gate exit evidence
old gate (pnpm --filter @objectstack/docs build) 0 — GREEN built .next/BUILD_ID=1b78aPQIaZ8tdciC3Avb7 and reported success while production is broken
new gate, build step 1 — RED x No package found with name '@objectstack/docs-renamed' in workspace

That pair is the whole card in two rows: same tree, same broken production config, gate green before and red after.

Breakage B — the production build command exits 0 and builds nothing (cd ../.. && pnpm --filter @objectstack/docs-renamed build), run against a simulated fresh checkout where only apps/docs/.next/cache is restored, as actions/cache does:

step exit evidence
new gate, build step 0 No projects matched the filters — the build step alone cannot catch this
new gate, assert step 1 — RED The production build command exited 0 but produced no apps/docs/.next/BUILD_ID - nothing was built.

So the assertion is load-bearing rather than decoration; it is the only thing that fails on B.

Healthy tree (the same extracted steps, clean committed tree): build step exit 0 in 190s, Tasks: 2 successful, Cached: 0 cached, 2 total — confirming TURBO_FORCE really defeated the cache — and assert step exit 0, BUILD_ID=0_cmVtBk3cHW3hhSrb-Yl.

The cost trade, stated rather than shipped quietly

The job now also builds @objectstack/spec (turbo's ^build) and forces execution instead of accepting a cache hit. Measured in this container: 190s vs 87s, about +1.7 min per Build Docs run, against the job's 30-minute timeout. No secrets and no new runner are needed — the production command is a plain workspace build. I think that is the right trade for a gate whose entire job is to be the thing that goes red, but it is the PM's call and it is reversible: dropping TURBO_FORCE recovers most of it and gives back exactly the cache-replay hole described above.

Also in this diff, named because it is a scope exception

apps/docs/next.config.mjs: one clause of an existing comment claimed "Vercel builds this app with Turbopack via vercel.json's pnpm turbo run build". That conflates turborepo with Turbopack, and it is measured false in the direction that matters — next build on Next 16 uses Turbopack in CI too: both this repo's CI command and the production command print ▲ Next.js 16.3.1 (Turbopack). The bundler is not what differs between them. Corrected in place (comment only, no behaviour), because a false explanation of this exact divergence sitting in the config would contradict this PR. The comment's conclusion — do not reintroduce output: 'standalone' — is untouched.

What this does NOT cover

The card's strongest surviving candidate was a Vercel-injected Next build adapter. The mechanism is real in the installed Next 16.3.1 — config-shared.js:190 resolves adapterPath from NEXT_ADAPTER_PATH, and build/index.js:2783 carries the in-source note that standalone output "not be allowed if an adapter with onBuildComplete is configured". But it is unreachable for any CI gate: the adapter is Vercel's, is not public, and no runner can install it. It is also moot on this tree, since output: 'standalone' — the only thing that interacts with it — is gone. Closing that class needs a check on the deployment itself, not a build job; it is not claimed here.

Verification

All gates below ran on d4382b3 with a clean tree, each exit code captured before any pipe.

  • check:node-version, check:pnpm-filter-targets, check:published-files, check:required-contexts, check:shard-attestation, check:test-source-alias, check:type-source-resolution, check:workflow-status-functions, check:nul-bytes — all exit 0.
  • check-aggregator-roster.mjs, check-ci-filter-parity.mjs, check-required-contexts.mjs, check-shard-attestation.mjs, check-step-collectors.mjs — all exit 0. The two that judge this file say so themselves: OK: all 88 declared cross-package glob(s) (76 unique) are covered by 'core' or 'crosspkg' … and ✓ check-step-collectors: 355 'run:' steps across 26 workflow(s).
  • Family list re-derived from the actual diff with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no hand-built path list); it returned the 13 families above and no others.
  • Declared lint narrowing. Repo-wide pnpm lint was attempted and died exit 134 — V8 heap OOM under container contention, not a lint failure. Narrowed to the one changed JS file with three proofs: the population is eslint.config.mjs's own ignores/files and it includes this file (eslint reported on it rather than skipping it); --format json reports exactly 1 file, 0 errors, 0 warnings; and the config configures no parserOptions.project / projectService, so with type-aware linting off a comment-only edit in one file cannot move any untouched file's verdict. The other changed file is YAML and outside eslint's population entirely.
  • No changeset: nothing published changes. @objectstack/docs is private: true; the rest is workflow config. Labelled skip-changeset.

Generated by Claude Code


Generated by Claude Code

…cel.json

`Build Docs` ran `pnpm --filter @objectstack/docs build` — a second,
hand-maintained spelling of the production build. Production runs whatever
`apps/docs/vercel.json` declares, and nothing in this repo read that file, so
the two were free to drift silently and did: the gate was green on nine PRs
while the production build of the same app was failing.

Read the command from `vercel.json` instead of copying it, run it from
`apps/docs` the way Vercel does, and assert `.next/BUILD_ID` afterwards because
a filter matching no package exits 0 having built nothing. `TURBO_FORCE` is the
one deliberate difference: the docs build task declares no input under
`content/`, so a content-only commit is a turbo cache hit and would let the
gate pass on a replayed artifact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SPBxLsqQGCVL5z5UXvgipH
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026
@github-actions github-actions Bot added size/s ci/cd documentation Improvements or additions to documentation labels Aug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 07:57
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 23, 2026
Merged via the queue into main with commit c91d7fe Aug 23, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10642-build-docs-gate-divergence branch August 23, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants