Contributing to kbrdn-docs
August 14, 2026 · View on GitHub
A multi-product documentation monorepo: one Astro Starlight
site per product, sharing a design-system package. Bun is the only
supported package manager and runtime. Conventions mirror
gwm-cli so the muscle memory is the same.
Table of contents
- Project layout
- Development
- Quality gates
- Local hooks
- Branches
- Commits
- Pull Requests
- Merge strategy
- Versioning & releases
Project layout
kbrdn-docs/
├── package.json # Bun workspaces root (private)
├── bunfig.toml
├── flake.nix # Nix dev shell
├── .oxlintrc.json
├── .prettierrc.json
├── packages/
│ └── ds-shared/ # @kbrdn/ds-shared — shared theme + overrides + MDX components
└── sites/
└── gwm/ # @kbrdn/docs-gwm — Starlight site for gwm
Development
Prerequisites
- Bun ≥ 1.3 (the only supported toolchain).
- Optional: Nix —
nix developdrops you in a shell with bun + git (+ curl, jq). No node: every script goes throughbun run.
Commands
bun install # install all workspaces
bun run dev # dev server for gwm
bun run build # build every site (static)
bun run check # lint + format check + astro check
bun run lint # oxlint
bun run lint:fix # oxlint --fix
bun run format # prettier --write
bun run format:check # prettier --check
bun --filter='@kbrdn/docs-gwm' run preview # preview a built site
Quality gates
Four gates must pass before a PR is mergeable (CI enforces them):
| Gate | Command |
|---|---|
| Lint | bun run lint |
| Format | bun run format:check |
| Type & content check | bun --filter='*' run check |
| Build (no broken refs) | bun run build |
.astro files are type-checked by astro check, not oxlint.
Published metadata
bun test (part of bun run check) guards what every page puts in
<meta name="description">, since that is what a search result shows:
- 250 characters max — past that, nothing survives the snippet cut.
- no two pages of the same locale describing themselves with the same sentence — measured as word overlap, 0.5 max. The two locales are crawled as separate sites, so a shared description across them is a translation, not a duplicate.
Changelog pages are exempt from the second rule only: their description quotes the summary upstream writes for that version, and three of those summaries are literally the same RC-promotion sentence. Making them distinct would mean inventing text the changelog does not say.
The guard also runs inside sync-gwm.yml, between the conversion and the
bot's commit. That workflow is the one path that publishes without anyone
reading the diff, so a description that trips a rule stops the sync instead of
being deployed and going red on the next PR.
Title casing is deliberately not guarded — separating Getting started
from GitHub issue / PR linking needs a hand-written list of proper nouns that
goes stale on the first page named after something not on it. gwm-cli made
the same call.
Local hooks
An opt-in POSIX pre-commit lives under .githooks/. It is not
installed automatically — enable it with:
git config core.hooksPath .githooks
It runs prettier + oxlint + astro check on staged files only, and
short-circuits in O(1) when nothing relevant is staged. Bypass once with
git commit --no-verify.
Branches
main— what ships ("latest" of every product). User-visible changes go through a PR.dev— integration branch (Dependabot targets it).- Feature branches:
<type>/#<issue-number>-<short-description>.
Examples: feat/#3-header-override, fix/#7-dead-link, docs/#5-gwm-config.
Commits
Format: <emoji> <type>(<scope>): <subject> (Gitmoji + Conventional Commits).
Types & emojis
| Emoji | Type | When |
|---|---|---|
| ✨ | feat | new content / component / capability |
| 🐛 | fix | broken link, wrong content, bug |
| 💄 | style | theme, layout, component styling |
| ♻️ | refactor | restructuring, no observable change |
| 📝 | docs | repo docs (README / CONTRIBUTING / etc.) |
| 🔧 | chore | deps, scripts, config |
| 👷 | ci | workflows, deployment |
| ⬆️ | chore | dependency bumps |
Scopes (suggested)
gwm, ds-shared, ci, deps, structure, theme, content.
Examples
✨ feat(ds-shared): add Header override with product switcher📝 docs(gwm): document the .gwm.toml trust ledger🔧 chore(deps): bump astro to 6.5💄 style(gwm): tune accent palette for dark mode
Pull Requests
Use the PR template. Before opening:
-
bun run lint -
bun run format:check -
bun --filter='*' run check -
bun run build - CHANGELOG.md updated under
## [Unreleased]
Merge strategy
- Never squash. Regular merge commit so the atomic history is preserved.
- Never delete the source branch after merge.
gh pr merge <num> --merge # NOT --squash, NOT --delete-branch
Versioning & releases
Two things are versioned here, and they do not move together.
The platform — the monorepo itself: @kbrdn/ds-shared, the tooling, the
CI, the generation pipeline. SemVer, tagged vX.Y.Z from v1.0.0 on. Cutting
one: [Unreleased] migrates to changelogs/X.Y.Z.md, the workspace
package.json files are bumped, the commit is born on dev, dev merges into
main, and the tag is pushed after that merge so it points at a commit that
already carries both. The GitHub release is created by hand
(gh release create vX.Y.Z --notes-file changelogs/X.Y.Z.md) — no workflow
watches tags.
The product docs — versioning is by branch, per product; there is no
in-tree versioned_docs/ mechanism.
-
maindeploys the latest docs of every product (production). -
When a product cuts a version whose docs must be frozen, branch from
main:git switch -c release/gwm/v0.9 git push -u origin release/gwm/v0.9That branch keeps the docs as they were for gwm v0.9. Cloudflare Pages deploys it as a separate deployment (a versioned URL). A version-selector component (shared, in
ds-shared) links the live versions together. -
Bugfixes to an old version land on its release branch; forward-port to
mainif still relevant.
Full rationale and the deployment topology are in PLAN.md.
By contributing, you agree your changes are licensed under the MIT License (see LICENSE.md).