kbrdn-docs

August 13, 2026 · View on GitHub

ci license docs bun

One Astro Starlight site per product, one shared design system, and not a single product page authored here.

Documentation is written next to the code it documents, in the product's own repository. This monorepo is the other half: it converts that tree, themes it, and ships it to Cloudflare Pages as a static site. main is the live "latest" of every product; a frozen version is a branch, not a directory.

Managed with Bun — the only supported package manager and runtime. npm / pnpm / yarn / npx / node are not used here.

What this repo is, precisely:

  • A renderer, not a source. sites/gwm/src/content/docs/<section>/ is wiped and regenerated by bun run sync:gwm from kbrdn1/gwm-cli, folder docs/. Everything hand-written for the site (the two landing index.mdx) is preserved across a sync.
  • A consistency layer. @kbrdn/ds-shared holds what must look identical on every product — theme tokens, the Starlight Footer override, reusable MDX components. Each site overrides its own accent, landing and nav on top of it.
  • A pipeline that runs itself. A delivery landing on gwm-cli's main that touches docs/, changelogs/ or Cargo.toml fires a repository_dispatch here; the sync replays the conversion, commits whatever drifted, and calls the deploy.
  • Four green gates or nothing. oxlint, prettier, bun test on the conversion core, then astro check + a full static build. The converter runs with no human in the loop, so a mapping regression would publish itself — that is what the test gate is for.

live

ProductAddressPage source
gwmhttps://gwm.kbrdn.devkbrdn1/gwm-cli, folder docs/

gwm-docs.pages.dev serves the same content and cannot be taken down — it is the Cloudflare project's own address. It is kept out of search indexes by sites/gwm/public/_headers, the only place in the chain that knows which host it is answering on: a robots.txt is static, therefore served identically on both domains and unable to exclude just one.

quick start

bun install
bun run dev          # gwm dev server on http://localhost:4321

With Nix: nix develop gives you bun, git, curl and jq. The shell ships no node — Bun is the only supported runtime, so every script goes through bun run <script>. The repo ships an .envrc, so direnv allow enters the same shell on cd.

commands

CommandEffect
bun run devgwm dev server (alias of dev:gwm)
bun run buildstatic build of every site
bun run preview:gwmserve the built gwm site
bun run checklint + format check + bun test + astro check
bun run testconversion-core tests (scripts/lib/)
bun run lintoxlint (lint:fix to autofix)
bun run formatprettier --write (format:check to verify)
bun run sync:gwmregenerate the gwm pages from a local gwm-cli checkout

sync:gwm reads ~/Projects/Perso/gwm-cli by default; point it elsewhere with GWM_REPO=/path/to/gwm-cli bun run sync:gwm.

layout

kbrdn-docs/
├── packages/
│   └── ds-shared/      # @kbrdn/ds-shared — theme, Starlight overrides, shared MDX components
├── sites/
│   └── gwm/            # @kbrdn/docs-gwm — Starlight site for gwm
└── scripts/
    ├── sync-gwm-docs.mjs   # disk I/O + orchestration
    └── lib/convert.mjs     # pure conversion core, covered by bun test

how the gwm site stays in sync

The conversion bridges two documentation formats, one difference at a time: Nuxt Content encodes sidebar order in the filename (1.getting-started/2.first-worktree.md) where Starlight reads it from frontmatter; the opening # Title is dropped because Starlight already renders title as the <h1>; screenshots move from docs/<section>/_assets/ to src/assets/captures/ with both link shapes rewritten; internal links get a /fr prefix inside the French pages.

The generated pages stay committed here. They could be built on the fly, but ci.yml builds the site on every push and PR — without them in the tree, CI would break. So the bot commits them, and the repository stays a faithful image of what is online.

⚠️ sync-gwm.yml reads gwm-cli's main, never its dev. A page whose source has not reached main yet is therefore deleted by the next sync, and that deletion is committed as legitimate drift.

deployment

main deploys the production ("latest") deployment to Cloudflare Pages, one Pages project per product. The deploy.yml workflow is gated by the DEPLOY_ENABLED repo variable and needs the CLOUDFLARE_API_TOKEN / CLOUDFLARE_ACCOUNT_ID secrets. All three have been in place since 2026-08-04.

Nothing needs to be triggered by hand: sync-gwm.yml calls deploy.yml explicitly once it has committed. That call is deliberate, not a side effect of the commit — a push signed by GITHUB_TOKEN triggers no workflow at all, which is GitHub's anti-recursion guard.

quality gates

Four gates, enforced by CI, and the merge contract for any change:

GateCommand
Lintbun run lint
Formatbun run format:check
Type & content checkbun --filter='*' run check
Build (no broken refs)bun run build

bun run check chains the first three plus the tests; .astro files are type-checked by astro check, not by oxlint. An opt-in pre-commit hook runs them on staged files only — git config core.hooksPath .githooks.

versioning

By branch, per product: release/<product>/v<MAJOR.MINOR> freezes one version's documentation and deploys as its own Cloudflare Pages deployment. There is no in-tree versioned_docs/ mechanism, and none is coming. See CONTRIBUTING.md and PLAN.md.

license

MIT © 2026 Kylian Bardini.