- Write long-lived feature and configuration docs in the present tense for the behavior readers will use when the docs are published. Avoid time-relative wording such as "upcoming", "new", or "now" that requires later cleanup.
- PRs for unreleased behavior can be ready for review. Coordinate merge timing with the relevant release or weekly update, and record that dependency in the PR description, not in customer prose.
- Describe the current behavior concisely. Put release transitions and old/new comparisons in upgrade notes rather than permanent feature docs. Retain version-specific guidance only when readers still need it to use or upgrade supported releases.
- Number ordered Markdown lists sequentially (
1.,2.,3.), rather than writing every item as1..
- Type Check:
npx tsc --noEmit - Package manager:
pnpm(packageManagerinpackage.json;mise installgives you the pinned Node and pnpm from.tool-versions). Nevernpm run - Build:
pnpm run build - Dev:
pnpm run dev - Lint:
pnpm run lint(ESLint 9 flat config ineslint.config.mjs; React Compiler rules are warnings until the vendoredsrc/components/search/docsearchcode is rewritten) - Framework: Next.js 16.
next builduses Turbopack;dev/build-content.mjsrunscontentlayer2 buildfirst, with its cache under.next/cacheso Vercel keeps it between deploys, and with each.mdxfile's mtime set from its content, since contentlayer2 keys its cache on mtime and a fresh clone resets those (workaround; drop once timlrx/contentlayer2#94 ships).next dev --webpackstill uses thenext-contentlayer2webpack plugin to regenerate content on change (next.config.jsapplies it only in the dev phase). The request rewrite lives insrc/proxy.ts;/api/releasesand/api/versionsopt into static caching withexport const dynamic = 'force-static' - Checks:
pnpm run checkruns the checks indev/checks.mjs(links, filenames, images);pnpm run buildruns filenames and images first, so a finding from those fails a deploy. Links is not in the build: it runs as its own PR check (.github/workflows/check-links.yml) - Check redirects:
node dev/check-redirects.mjsreports broken entries insrc/data/redirects.ts(CI comments on PRs that break redirects; see the script header for what it checks). Not part ofpnpm run check: main has hundreds of pre-existing findings, and CI only reports the ones a PR adds. When a check script crashes, CI comments that the check could not run, linking the job log, instead of failing the PR: an empty report says nothing about the PR - Prove changed links resolve on a deploy:
node dev/verify-links-live.mjs --site <vercel-preview-url>prints a Markdown table for the PR description - Spell check:
.github/workflows/check-spelling.ymlrunsdev/check-spelling.mjson the lines a PR adds plus its title and description, and comments the findings (advisory, never fails the PR). Add product names and identifiers tocspell-allow-list.txtwithnode dev/allow-spelling-words.mjs <words>, which keeps it in alphabetical order (the check reports out-of-order entries), or comment/spell <words>on the PR:.github/workflows/spellcheck-allow.ymlcommits them to the branch and runs the check again. CSpell is not a project dependency:npm install --global cspell@10, thennode dev/check-spelling.mjs --diff <(git diff -U0 origin/main)runs the check on your branch - Example hostnames: placeholder hostnames in docs are
*.example.com(sourcegraph.example.comfor the reader's instance,github.example.comfor a code host,https://sourcegraph.example.comwhere a whole URL is meant); never<URL>,[hostname], or a$VARIABLEthe reader has to guess. Example URLs usehttps://, except where needed, eg. loopback, in-cluster, proxy, and XML-namespace addresses. In a shell snippet a$VARIABLEis fine only when the same snippet exports it with an example value..github/workflows/check-hostnames.ymlrunsdev/check-hostnames.mjson the lines a PR adds and comments the findings with suggested changes (advisory, never fails the PR).dev/example-hostnames.jsonmaps each recommended hostname to the placeholders seen in its place, as either literal text, a*glob, or a/regex/, matched case-insensitively with-,_, and.interchangeable; add to it when the check misses one, keeping keys and lists sorted.node dev/check-hostnames.mjschecks all ofdocs/The🤖 Sync generated docsPRs overwrite theSCHEMA_SYNC_START…SCHEMA_SYNC_ENDblocks and whole pages (self-hosted/observability/{alerts,dashboards}.mdx,cli/references/,ai/models.mdx,cody/capabilities/supported-models.mdx,admin/telemetry/); fix those in sourcegraph/sourcegraph instead.git log --author=sourcegraph-buildkite -- <file>shows whether a page is one - Duplicated content:
.github/workflows/check-duplicates.ymlrunsdev/check-duplicates.mjsand comments the passages a PR adds that say the same thing as a passage elsewhere underdocs/, paraphrased or verbatim, and the passages a PR edits that were already duplicated, so the author can pick the home and replace the other copy with a link (advisory, never fails the PR). Not part ofpnpm run check: main has hundreds of pre-existing findings, and CI only reports the ones a PR adds or edits.node dev/check-duplicates.mjs --diff <(git diff -U0 origin/main)runs it on your branch; without--diffit lists every duplicate indocs/
- Each fact has one home. Link to it instead of copying it, so the copies cannot drift apart.
- Before writing a config snippet,
rgthe key underdocs/and link to the existing one. - The
SCHEMA_SYNC_START…SCHEMA_SYNC_ENDblocks are the home for every setting's default. Never restate a default elsewhere.
pnpm run check links --check-anchors --check-self-links. CI comments on PRs that break links and fails theBroken links introduced by this PRcheck; seedev/check-links.mjs.- When moving a page or renaming a heading, update every link to it; a
redirect in
src/data/redirects.tsdoes not satisfy the check. - Link to this site with relative paths (
/admin/config/site-config), neverhttps://sourcegraph.com/docs/…orhttps://docs.sourcegraph.com/…. - main has hundreds of pre-existing findings. To see only the ones your
branch adds, as CI does, record the merge base with
--format jsonand pass it as--baseline;--diffdoes not filter findings, it only scopes--check-externaland the review comments to added lines:
base=$(git merge-base origin/main HEAD)
git worktree add --detach /tmp/docs-base "$base"
node dev/check-links.mjs --check-anchors --check-self-links --format json \
--root /tmp/docs-base > /tmp/docs-base-links.json
git worktree remove /tmp/docs-base
git diff -U0 "$base" | pnpm run check links --check-anchors --check-self-links \
--check-external --baseline /tmp/docs-base-links.json --diff /dev/stdin- Pipe the diff to
--diff /dev/stdin, not--diff <(git diff ...):dev/checks.mjsspawns the check with only stdin/stdout/stderr, so the process-substitution descriptor is gone and the check fails withEBADF
The spelling, links, redirects, example hostnames, and duplicated content
checks comment on the PR: a summary comment per check, plus an inline review
comment on each flagged line, with a suggestion block where the fix is
mechanical. After pushing, read them and follow the instructions they give
(fix the spelling, the link, the redirect, or the hostname; replace the
duplicated passage with a link to its home); do not work around a finding,
and add a word to cspell-allow-list.txt only when it is correct.
gh pr checks <pr>
gh api repos/sourcegraph/docs/issues/<pr>/comments --paginate --jq '.[].body'
gh api repos/sourcegraph/docs/pulls/<pr>/comments --paginate \
--jq '.[] | "\(.path):\(.line)\n\(.body)\n"'Fork PRs get no comments; the findings are in the job log
(gh run view <run-id> --log). The checks resolve their own comments on the
next run: the summary is minimized and the inline comments are deleted. A PR
that never had findings never gets a comment.
The five check workflows are one actions/checkout step plus
.github/actions/pr-check, which runs dev/check-<name>.mjs and comments.
A check script parses its flags and the diff, lists files, drops baseline
findings, and formats the report with dev/check-shared.mjs; it writes a
Markdown report and a review JSON for the lines a PR adds, exits 1 with
findings, and leaves the report empty when it crashes. The action turns that
into result=clean|found|crashed and dev/check-report.sh posts the summary
and the inline comments. The action and scripts run from the merge commit,
like the workflow file itself, so a branch that predates a check still gets
checked. Each workflow sets only what differs: the script, the comment
marker, extra arguments, which paths to diff, whether to compare against a
baseline of the base branch, an npm dependency to install, and whether
findings fail the check.
- Vercel shows build logs only to its team members, so
.github/workflows/vercel-build-log.ymlattaches the log to the Vercel Slack app's "failed to deploy" post in theSLACK_CHANNEL_IDchannel and comments a link to it on the PR (seedev/vercel-build-log.mjs). The log itself never goes on the PR, since the repository is public. A build that passed also lists the pages the PR changed in the Slack reply, from thepreview-links.ymlcomment: on the preview for an open PR, in production for a merged one. - It reads Vercel with the
VERCEL_TOKENrepo secret, a token scoped to thesourcegraph-docsproject that expires 2026-12-10; mint a new one withPOST /v3/user/tokens?teamId=<team>andprojectIdin the body. - Slack needs the
SLACK_BOT_TOKENrepo secret andSLACK_CHANNEL_IDrepo variable. The bot is the Slack app indev/vercel-build-log-slack-app-manifest.json; to recreate it, paste that manifest at https://api.slack.com/apps?new_app=1 (From a manifest), install it, copy its Bot User OAuth Token into the secret, and/invite @Vercel build logto the channel.
- Prefix every branch with a name that identifies the human it belongs to
(first name, username, or nickname), then a slash:
marc/fix-moved-page-links. Agents use the name of the human they are working for, never their own