Docs & Code Guide

Documentation checks in CI: link checkers, prose linters, freshness gates, and rules that demand doc updates

CI can verify four things about documentation: that links resolve, that prose meets a style bar, that critical pages carry a recent review date, and that changes to watched code paths arrive with doc changes attached. It cannot verify that a sentence is true. The useful pipeline automates the four checkable properties and routes the truth question to regeneration and review.

8 min readFor engineers wiring docs checks into the pipeline

See it as a diagram

Everything below, as a diagram you can edit. Describe yours and see it in seconds.

186/20003 credits left
Try:

No account needed · Editable canvas, not a picture

Prose linters: Vale for style, markdownlint for structure

Vale lints prose against rule packages, including implementations of the Google and Microsoft style guides, plus custom rules, banned phrases, casing, terminology. Running vale --minAlertLevel=error docs/ in CI keeps only rule violations you have promoted to errors as merge blockers, which is the setting that keeps teams from ripping the linter out in week two. markdownlint-cli2 covers structure, heading hierarchy, list formatting, bare URLs, and runs as markdownlint-cli2 "docs/**/*.md".

Prose linters check style, not correctness. A fluent, well-formed page describing a deleted service passes both tools. That gap is what the next two checks exist for.

The freshness gate

Give critical pages a front matter field, last_reviewed: 2026-08-12, plus a tier marker, and a small script fails the build when any critical page's review date is older than the window. This is the check that catches the failure link checkers cannot: the page nobody has touched, describing a system everyone has.

name: docs-checks
on:
  pull_request:
    paths: ['docs/**', '**/*.md']

jobs:
  links:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: lycheeverse/lychee-action@v2
        with:
          args: --no-progress --cache 'docs/**/*.md' 'README.md'

  prose:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: errata-ai/vale-action@v2
        with:
          files: docs

  freshness:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Critical pages reviewed within 90 days
        run: |
          cutoff=$(date -d '90 days ago' +%F)
          stale=0
          for f in $(grep -rl 'tier: critical' docs); do
            reviewed=$(grep -oP 'last_reviewed: \K\d{4}-\d{2}-\d{2}' "$f" || true)
            if [ -z "$reviewed" ] || [ "$reviewed" \< "$cutoff" ]; then
              echo "STALE: $f (last_reviewed: $reviewed)"
              stale=1
            fi
          done
          exit $stale

Rules that demand doc updates with code changes

Danger runs a script against each PR's metadata and can warn or fail based on what changed. The docs rule is four lines: if files under a watched service path changed and no file under that service's docs path did, post a warning asking for the doc update or an explicit reason it is not needed. Teams usually start with warn rather than fail, because plenty of code changes legitimately need no doc change, and a hard fail teaches people to make junk edits to satisfy the bot.

The same idea scales down to plain CI: a shell step comparing git diff --name-only against two path lists needs no framework at all. What Danger adds is the PR comment surface and per-rule messaging.

// dangerfile.js
const changed = danger.git.modified_files.concat(danger.git.created_files);

const touchedPayments = changed.some((f) => f.startsWith('services/payments/'));
const touchedTheirDocs = changed.some((f) => f.startsWith('docs/payments/'));

if (touchedPayments && !touchedTheirDocs) {
  warn(
    'services/payments changed but docs/payments did not. ' +
    'Update the doc, or note in the PR description why it still holds.'
  );
}

Where diagram regeneration fits in the same pipeline

Every check above verifies text. Architecture diagrams need the equivalent of the Danger rule with the repair attached: after a merge that touches infrastructure paths, a pipeline step asks an agent connected to Datadef's MCP server (registry name io.datadef/mcp) to update the diagram from what the repo now contains. The agent reads the code, the diagram gets redrawn, and because the published diagram is an embed URL rather than a committed image, every README and wiki page showing it follows within minutes. The full recipe lives in a living diagram from GitHub Actions.

The honest boundaries: Datadef does not watch the repository, so the trigger is your pipeline's job, exactly like every other check on this page. The MCP connection needs an API key, available on paid plans. And the embed URL requires the project to be shared public, which matters for confidential systems. For prose that must track code, function references, walkthroughs, this is out of Datadef's scope; a code-coupled docs tool like Swimm is the honest recommendation there.

A pipeline that fits in one afternoon

lychee on PRs with a weekly external-link run, Vale at error level, the 90-day freshness gate on critical pages, one Danger rule per watched service, and a post-merge diagram regeneration step. Each piece is independent; add them in that order.

FAQ

What documentation checks should run in CI?

Four kinds: a link checker such as lychee to catch broken references, a prose linter such as Vale at error level for style and terminology, a freshness gate that fails when critical pages carry a last-reviewed date older than the window, and a rule, via Danger or a shell diff, that flags PRs changing watched code paths without touching the matching docs. Diagram regeneration then runs post-merge in the same pipeline.

Can CI detect outdated documentation?

CI detects proxies for outdatedness rather than outdatedness itself: broken links, old last-reviewed dates, and code changes unaccompanied by doc changes. Whether a paragraph still tells the truth is not machine-checkable from the text alone. For diagrams there is a stronger option: diffing the diagram's component list against Terraform state, and regenerating from the repo when it drifts.

Should documentation checks block merges?

Links and structure on changed files: yes, they are objective and fixable in the PR. Prose style: only at error level. Doc-update rules: start as warnings, because many code changes legitimately need no doc change and a hard fail invites token edits. Freshness gates: block on critical pages only, or run them on a schedule with an alert instead of a PR failure.

How do I require doc updates when code changes?

A Danger rule comparing changed paths works in about four lines: if danger.git.modified_files includes files under the watched service path and none under its docs path, call warn or fail with a message. Without Danger, a CI shell step over git diff --name-only does the same comparison; Danger adds the PR comment and per-rule messaging.

lychee or markdown-link-check?

lychee for CI: it is fast, retries flaky hosts, caches results between runs, and has a maintained GitHub Action. markdown-link-check fits per-file use in pre-commit hooks. Either way, keep external-link checking on a schedule rather than per-PR so third-party outages do not block unrelated merges.