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

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 develop drops you in a shell with bun + git (+ curl, jq). No node: every script goes through bun 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):

GateCommand
Lintbun run lint
Formatbun run format:check
Type & content checkbun --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

EmojiTypeWhen
featnew content / component / capability
🐛fixbroken link, wrong content, bug
💄styletheme, layout, component styling
♻️refactorrestructuring, no observable change
📝docsrepo docs (README / CONTRIBUTING / etc.)
🔧choredeps, scripts, config
👷ciworkflows, deployment
⬆️choredependency 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.

  • main deploys 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.9
    

    That 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 main if 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).