@@ -479,7 +479,8 @@ half of the pipeline and skips deploy-side assertions.
479479`make qualify` is the canonical pre-push command. It runs :
480480
481481- ` test-coverage` — `go test -race ./...` plus the 80% coverage floor.
482- - ` lint` — golangci-lint with `.golangci.yaml` plus yamllint.
482+ - ` lint` — golangci-lint with `.golangci.yaml`, yamllint, and the docs checks
483+ (filenames, MDX patterns, MDX parse — see [Docs MDX Gate](#docs-mdx-gate)).
483484- ` e2e` — the end-to-end pipeline runner.
484485- ` scan` — Grype vulnerability scan.
485486- ` license-check` — license header / dependency-license sweep.
@@ -501,6 +502,66 @@ This applies even to PRs labeled `documentation` when they include
501502incidental Go changes. Do not rely on CI to surface lint failures —
502503the pre-push gate is local.
503504
505+ # # Docs MDX Gate
506+
507+ Fern renders published docs through an MDX parser, so a construct that is valid
508+ CommonMark can still abort `fern generate --docs` at publish time. A bare `<=`
509+ in prose is the classic case — MDX reads the `<` as the start of a JSX tag and
510+ fails with `Unexpected character = (U+003D) before name`.
511+
512+ **Which files are checked.** Both checks derive their file list from
513+ ` docs/index.yml` via `tools/docs-published-files` — Fern's navigation manifest
514+ is the authoritative statement of what gets parsed. Globbing
515+ ` docs/user` /`docs/integrator`/`docs/contributor` instead was a denylist in
516+ disguise : it missed `docs/README.md`, the published landing page, so a hazard
517+ there passed both gates and still broke the publish. Add a page to
518+ ` docs/index.yml` and it is gated that day; a file that is not published is not
519+ gated at all.
520+
521+ Two checks cover this, both run by `make lint` :
522+
523+ | Check | What it is | Speed |
524+ |-------|-----------|-------|
525+ | `make check-docs-mdx` | Pattern-based bash approximation. Names the specific hazard, needs no dependencies. | Instant |
526+ | `make check-docs-mdx-parse` | The real MDX parser (`@mdx-js/mdx`, locked in `tools/mdx/package-lock.json`). Authoritative. | ~2 s + one `npm ci` |
527+
528+ The parser is the source of truth. The bash rules are deliberately kept as a
529+ strict **subset** of what it rejects : a miss is caught by the parse gate, but a
530+ false positive would force you to mangle prose the publish step would have
531+ accepted. That is why `< 500` and `< 10 s` are fine (MDX only enters tag mode
532+ when a name-ish character follows `<` immediately) while `<= 2,000` and `<30 s`
533+ are not.
534+
535+ Hazards only the parser sees : a stray closing tag (`</div>`), an unclosed
536+ fragment (`<>`), a placeholder sharing a line with well-formed JSX, unbalanced
537+ expression braces spanning lines, and any acorn-level syntax error.
538+
539+ Well-formed JSX is fine in both — `<Component />` and `<span>text</span>` parse,
540+ and the Fern component set is authored that way. So is YAML frontmatter : Fern
541+ strips it before MDX, so a `title : gate <= 2,000` is valid, and both checks skip
542+ it by line number so later diagnostics still cite the true line.
543+
544+ ` check-docs-mdx-parse` needs Node 20+. Without it the script prints a warning
545+ and exits 0 locally, but **hard-fails under CI** — the `docs-mdx` job in
546+ ` merge-gate.yaml` blocks on it, and the merge gate is the only required status
547+ check. This is the one place where a green local `make qualify` does not
548+ guarantee a green CI : if you have no Node, the MDX gate did not actually run.
549+
550+ Fixing a violation is usually one of :
551+
552+ ` ` ` markdown
553+ gate <= 2,000 → gate ` <= 2,000`
554+ <30 s → `<30 s` or <30 s
555+ <br> → <br />
556+ {template} → \{template\}
557+ ```
558+
559+ Note that ` fern check ` (the Fern Docs CI job) does ** not** parse MDX, and the
560+ job that does — ` fern generate --docs --preview ` — runs as a ` workflow_run `
561+ companion whose status never lands on the PR head SHA, so it cannot be a
562+ required check. ` make check-docs-mdx-parse ` exists to close that gap without a
563+ token or a dependency on Fern's service at merge time.
564+
504565## Common Gotchas
505566
506567- ** ` goreleaser ` fails when both ` GITLAB_TOKEN ` and ` GITHUB_TOKEN `
0 commit comments