Maintain Docs-as-Code Workflow in 20 Minutes

Keep documentation synced with code using automated testing, version control, and CI/CD validation for technical teams.

Problem: Documentation Becomes Outdated and Disconnected from Code

Your team writes docs once, then they drift out of sync with the codebase. Developers skip updating docs because the workflow is clunky, and users hit outdated information.

You'll learn:

  • How to keep docs in version control with code
  • Automated testing and validation for documentation
  • CI/CD workflows that enforce doc quality

Time: 20 min | Level: Intermediate


Why This Happens

Traditional documentation lives in separate systems (Confluence, Google Docs) with different workflows than code. Developers don't update docs because:

Common symptoms:

  • Code examples in docs throw errors
  • API docs reference removed endpoints
  • Installation steps fail on current versions
  • No one reviews docs in PRs

Solution

Step 1: Co-locate Docs with Code

Move documentation into your repository alongside the code it describes.

# Recommended structure
my-project/
├── src/
├── docs/
│   ├── getting-started.md
│   ├── api/
│   └── guides/
├── examples/          # Working code that docs reference
└── .github/workflows/ # CI for docs validation

Why this works: Docs become part of the same PR review process as code changes. Version control tracks when docs were last updated.

If it fails:

  • Team resists: Start with API docs only, expand gradually
  • Docs too large: Use git submodules or separate repo with synchronized releases

Step 2: Extract Code Examples from Tests

Don't write code examples manually—extract them from your test suite.

// tests/examples/quickstart.test.ts
test('quickstart example from docs', async () => {
  // DOCS_START: quickstart
  import { createClient } from '@myapp/sdk';
  
  const client = createClient({
    apiKey: process.env.API_KEY
  });
  
  const result = await client.users.list();
  // DOCS_END: quickstart
  
  expect(result.length).toBeGreaterThan(0);
});

Then extract for docs:

# Extract code between markers
sed -n '/DOCS_START: quickstart/,/DOCS_END: quickstart/p' \
  tests/examples/quickstart.test.ts \
  | grep -v 'DOCS_' > docs/snippets/quickstart.ts

Expected: Every code example in docs passes tests. Breaking changes in code automatically break doc builds.


# .github/workflows/docs-validation.yml
name: Validate Docs

on: [pull_request]

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Check for broken links
      - name: Check markdown links
        uses: gaurav-nelson/github-action-markdown-link-check@v1
        with:
          config-file: '.markdown-link-check.json'
      
      # Verify code examples compile
      - name: Test code snippets
        run: |
          npm install
          npm run test:docs-examples
      
      # Check for outdated version references
      - name: Validate versions
        run: |
          # Fail if docs reference versions older than package.json
          node scripts/check-version-refs.js

Why this works: CI catches broken links, outdated code, and version mismatches before merge.


Step 4: Automate Doc Freshness Checks

// scripts/check-doc-freshness.js
const fs = require('fs');
const { execSync } = require('child_process');

// Get files changed in last 90 days
const recentFiles = execSync(
  'git log --since="90 days ago" --name-only --pretty=format: | sort -u'
).toString().split('\n');

const sourceFiles = recentFiles.filter(f => f.startsWith('src/'));
const docFiles = recentFiles.filter(f => f.startsWith('docs/'));

// If source changed but docs didn't, flag it
sourceFiles.forEach(srcFile => {
  const relatedDoc = `docs/${srcFile.replace('src/', '').replace('.ts', '.md')}`;
  
  if (fs.existsSync(relatedDoc) && !docFiles.includes(relatedDoc)) {
    console.warn(`⚠️  ${srcFile} changed but ${relatedDoc} wasn't updated`);
  }
});

Expected: Weekly reports showing which docs might be stale based on related code changes.


Step 5: Document in Pull Requests

Add a docs checklist to your PR template:

<!-- .github/pull_request_template.md -->
## Documentation Changes

- [ ] Updated relevant markdown files in `/docs`
- [ ] Added/updated code examples (extracted from tests)
- [ ] Verified all links work locally
- [ ] Updated CHANGELOG.md with user-facing changes
- [ ] No changes needed (internal refactor only)

Why this works: Makes doc updates a required step, not an afterthought.


Verification

Test the workflow:

# 1. Make a code change
echo "export function newFeature() {}" >> src/api.ts

# 2. Update docs
echo "## New Feature\n\n..." >> docs/api.md

# 3. Extract examples
npm run docs:extract-examples

# 4. Run validation
npm run docs:validate

# 5. Preview locally
npm run docs:serve

You should see:

  • ✅ All links valid
  • ✅ Code examples compile
  • ✅ No version mismatches
  • 🌐 Local preview at localhost:3000

What You Learned

  • Co-locating docs with code enables version control and review
  • Extracting examples from tests prevents outdated code
  • CI/CD for docs catches issues before users see them
  • Automation reduces the burden on developers

Limitations:

  • Requires cultural shift—team must value docs as code
  • Initial setup takes 1-2 days for CI integration
  • Not ideal for non-technical docs (marketing, legal)

Tooling Recommendations

Static Site Generators (Hugo-Compatible)

# For Hugo sites, add to config.toml
[markup.goldmark.renderer]
  unsafe = true  # Allow HTML in markdown for code extraction

[outputs]
  home = ["HTML", "JSON"]  # Generate JSON for API docs
<!-- Custom shortcode for tested examples -->
<!-- layouts/shortcodes/tested-example.html -->
{{ $file := .Get "file" }}
{{ readFile (printf "docs/snippets/%s" $file) | highlight (.Get "lang") "" }}
<small>✓ Tested in <code>tests/examples/</code></small>

Alternative: VitePress (Modern, Fast)

// .vitepress/config.js
export default {
  title: 'My Project Docs',
  themeConfig: {
    // Automatic sidebar from file structure
    sidebar: 'auto',
    
    // Show last updated from git
    lastUpdated: true,
    
    // Edit link in GitHub
    editLink: {
      pattern: 'https://github.com/user/repo/edit/main/docs/:path',
      text: 'Edit this page'
    }
  }
}

Documentation Linting

# Install vale (prose linter)
# brew install vale

# .vale.ini
StylesPath = styles
MinAlertLevel = suggestion

[*.md]
BasedOnStyles = write-good, Microsoft

Real-World Examples

Stripe's API Docs:

  • Code examples tested in CI
  • Version selector shows different SDK versions
  • "Try it" sandbox uses same code as docs

Rust's Documentation:

  • cargo test runs code in doc comments
  • Docs build fails if examples don't compile
  • Every release includes tested docs

Kubernetes Docs:

  • YAML manifests validated against schemas
  • kubectl commands tested in CI
  • Community PRs required for API changes

Migration Checklist

Moving from traditional docs to docs-as-code:

  • Audit existing docs (what's still relevant?)
  • Set up docs directory in repository
  • Configure static site generator
  • Add CI validation workflow
  • Extract first set of code examples from tests
  • Update PR template with docs checklist
  • Train team on markdown and git workflow
  • Deprecate old documentation system (after 90 days)
  • Set up redirect from old URLs to new site

Timeline: 2-3 weeks for initial setup, 1-2 months for full team adoption.


Tested with Hugo 0.122+, VitePress 1.0+, GitHub Actions, GitLab CI