⏱ 9 min read | ~1743 words
🔑 Key Takeaways
- ✅ CI pipelines auto‑run doc generation on every code change
- ✅ Store docs in version‑controlled /docs/ for traceability
- ✅ Integrate Claude 4.6 Opus and GPT‑5.4 Pro via reusable actions
- ✅ Publish via MkDocs Material to GitHub Pages or GitLab Pages
- ✅ Fail builds on documentation generation errors to enforce quality
AI‑Powered Automated Documentation Generator for Codebases — Part 6: Automating Documentation Updates via CI/CD
In the first five installments we covered the fundamentals of extracting code‑level metadata, prompting Claude 4.6 Opus and GPT‑5.4 Pro to synthesize human‑readable docs, storing the output in a version‑controlled docs/ folder, and exposing the result through a static‑site generator (MkDocs with Material theme). Based on my technical understanding as a Lead Programmer Analyst, the next logical step is to make those docs live‑up‑to‑date without any manual “run‑once” step.
This article walks you through a full CI/CD‑driven workflow that automatically regenerates and publishes documentation whenever source code changes. We’ll:
- Pick a CI platform (GitHub Actions & GitLab CI) and explain why AI‑enhanced pipelines matter in 2026.
- Show a reusable
docgenDocker image that bundles Claude‑4.6 Opus and GPT‑5.4 Pro clients, Python helpers, and MkDocs. - Write the pipeline scripts that (a) detect changed modules, (b) invoke the LLMs, (c) diff the new docs, and (d) push an update‑only commit back to the repo.
- Integrate a “Documentation‑Debt” badge that reflects the percentage of undocumented symbols, using data from the Zylos Research study.
- Securely manage API keys and discuss best‑practice secrets handling for compliance.
Why Automate Documentation in the CI/CD Layer?
Documentation debt is still one of the most pervasive forms of technical debt. The Zylos Research report (2026‑01‑28 AI Documentation Generation) estimates that up to 30 % of developer time is spent maintaining docs. In a world where AI coding assistants now generate a substantial share of production code (Northflank 2026), the bottleneck has shifted from “write code” to “keep docs in sync”. Embedding doc generation into the CI/CD pipeline means:
- Documentation is always a first‑class artifact of a build.
- Documentation‑debt metrics can be gated as a quality gate (e.g., fail a merge request if undocumented symbols exceed a threshold).
- Teams get immediate feedback via pull‑request comments powered by GitLab Duo AI Code Review (AugmentCode 2026).
High‑Level Architecture
| Component | Responsibility | Technology (2026) |
|---|---|---|
| Source Repository | Stores code, docs, CI pipeline definitions. | GitHub or GitLab (both support native AI integrations). |
| CI Runner | Executes the docgen container on every push. | GitHub Actions, GitLab CI, or self‑hosted runners. |
| LLM Clients | Calls Claude 4.6 Opus & GPT‑5.4 Pro to synthesize docs. | Python anthropic SDK, openai SDK (v1.2+). |
| Static Site Generator | Builds HTML docs from Markdown. | MkDocs + Material for MkDocs (v9.5). |
| Artifact Store | Publishes the built site to a CDN. | GitHub Pages, GitLab Pages, or Cloudflare Pages. |
1. Building a Reusable docgen Docker Image
The container isolates all heavy dependencies (LLM SDKs, Python 3.12, MkDocs) and can be cached across pipelines. Below is a minimal but production‑ready Dockerfile that you can push to Docker Hub or your private registry.
<!-- docgen/Dockerfile -->
FROM python:3.12-slim-bullseye
# Install system utilities
RUN apt-get update && \
apt-get install -y --no-install-recommends git curl && \
rm -rf /var/lib/apt/lists/*
# Create a non‑root user
ARG UID=1001
ARG GID=1001
RUN groupadd -g $GID appgroup && \
useradd -m -u $UID -g $GID -s /bin/bash appuser
# Install Python dependencies
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the doc‑generation scripts
COPY src/ ./src/
COPY mkdocs.yml .
COPY docs/ ./docs/
# Set entrypoint to a thin wrapper (we’ll override in CI)
ENTRYPOINT ["/usr/bin/env", "python", "-m", "src.docgen"]
requirements.txt (excerpt):
anthropic==0.7.3
openai==1.2.0
mkdocs==1.6.0
mkdocs-material==9.5.14
pydantic==2.7
GitPython==3.1.43
The src/docgen package contains the core logic. Let’s explore it.
2. Core Python Logic – Detecting Changes & Generating Docs
We need three capabilities:
- Identify which modules have changed since the last successful doc run.
- Collect relevant source snippets (function signatures, class docstrings, type hints).
- Prompt the LLMs, merge their responses, and write Markdown files.
Below is a fully functional docgen.py. It is deliberately split into small, testable functions so you can reuse them in other pipelines.
# src/docgen/__init__.py
import os, json, subprocess, textwrap, hashlib
from pathlib import Path
from typing import List, Dict, Tuple
import git
import anthropic
import openai
import mkdocs
from pydantic import BaseModel, Field
# ----------------------------------------------------------------------
# Configuration – keep it declarative, easy to override via env vars
# ----------------------------------------------------------------------
REPO_ROOT = Path(os.getenv('REPO_ROOT', '.')).resolve()
DOCS_DIR = REPO_ROOT / 'docs'
SRC_DIR = REPO_ROOT / 'src' # Adjust if your code lives elsewhere
CACHE_FILE = REPO_ROOT / '.docgen_cache.json'
ANTHROPIC_API_KEY = os.getenv('ANTHROPIC_API_KEY')
OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
MAX_TOKENS = int(os.getenv('MAX_TOKENS', '3000'))
# ----------------------------------------------------------------------
# Helper models
# ----------------------------------------------------------------------
class SymbolInfo(BaseModel):
name: str
kind: str # function / class / method
signature: str
docstring: str = ''
source: str
class GenerationResult(BaseModel):
path: Path
content: str
checksum: str = Field(default_factory=lambda: '')
# ----------------------------------------------------------------------
# Git utilities
# ----------------------------------------------------------------------
def get_repo() -> git.Repo:
return git.Repo(REPO_ROOT, search_parent_directories=True)
def get_changed_files() -> List[Path]:
"""Return a list of source files changed since the last tag `docs-last`."""
repo = get_repo()
# If the tag does not exist, fall back to the initial commit
try:
last_tag = repo.tags['docs-last']
diff = repo.commit(last_tag).diff('HEAD')
except (IndexError, AttributeError):
diff = repo.head.commit.diff('HEAD~1')
return [REPO_ROOT / item.a_path for item in diff if item.a_path.startswith('src/') and item.a_path.endswith('.py')]
# ----------------------------------------------------------------------
# Source parsing (simple AST based extraction)
# ----------------------------------------------------------------------
def extract_symbols(file_path: Path) -> List[SymbolInfo]:
import ast
with open(file_path, 'r', encoding='utf-8') as f:
source = f.read()
tree = ast.parse(source, filename=str(file_path))
symbols: List[SymbolInfo] = []
for node in ast.iter_child_nodes(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
sig = f\"def {node.name}({', '.join(arg.arg for arg in node.args.args)})\"
doc = ast.get_docstring(node) or ''
symbols.append(SymbolInfo(name=node.name, kind='function', signature=sig, docstring=doc, source=source[node.lineno-1:node.end_lineno]))
elif isinstance(node, ast.ClassDef):
bases = ', '.join(base.id for base in node.bases if isinstance(base, ast.Name))
sig = f\"class {node.name}({bases})\" if bases else f\"class {node.name}\"
doc = ast.get_docstring(node) or ''
symbols.append(SymbolInfo(name=node.name, kind='class', signature=sig, docstring=doc, source=source[node.lineno-1:node.end_lineno]))
return symbols
# ----------------------------------------------------------------------
# Prompt engineering (Claude + GPT)
# ----------------------------------------------------------------------
def build_prompt(symbol: SymbolInfo) -> str:
"""Compose a concise prompt that works for both Claude and GPT."""
base = textwrap.dedent(f\"\"\"
You are a senior software engineer documenting a Python codebase.
Write a clear, concise Markdown section for the following {symbol.kind}.
**Signature**
```python
{symbol.signature}
```
**Existing docstring (if any)**
{symbol.docstring or '_None_'}
\"\"\")
# Add a short instruction for style consistency
base += \"\\nUse the project’s tone: technical but approachable. Keep each section under 150 words.\\n\"
return base
def call_claude(prompt: str) -> str:
client = anthropic.Anthropic(api_key=ANTHROPIC_API_KEY)
response = client.messages.create(
model='claude-3-opus-20240229',
max_tokens=MAX_TOKENS,
temperature=0.0,
system='You generate API documentation in Markdown.',
messages=[{'role': 'user', 'content': prompt}]
)
return response.content[0].text
def call_gpt(prompt: str) -> str:
client = openai.OpenAI(api_key=OPENAI_API_KEY)
response = client.chat.completions.create(
model='gpt-5.4-pro',
messages=[{'role': 'user', 'content': prompt}],
temperature=0.0,
max_tokens=MAX_TOKENS
)
return response.choices[0].message.content
def merge_responses(claude: str, gpt: str) -> str:
"""Simple heuristic: keep Claude's core description, supplement with GPT's examples."""
# Split on first newline to isolate description vs examples
claude_parts = claude.strip().split('\\n\\n', 1)
gpt_parts = gpt.strip().split('\\n\\n', 1)
description = claude_parts[0]
examples = gpt_parts[1] if len(gpt_parts) > 1 else ''
merged = f\"{description}\\n\\n{examples}\" if examples else description
return merged
# ----------------------------------------------------------------------
# Documentation rendering
# ----------------------------------------------------------------------
def render_markdown(symbol: SymbolInfo, body: str) -> str:
header = f\"## {symbol.name}\\n\\n**{symbol.kind.title()}**\\n\\n```python\\n{symbol.signature}\\n```\\n\\n"
return header + body + \"\\n\\n---\\n\\n\"
def write_doc(result: GenerationResult):
result.path.parent.mkdir(parents=True, exist_ok=True)
with open(result.path, 'w', encoding='utf-8') as f:
f.write(result.content)
def checksum(content: str) -> str:
return hashlib.sha256(content.encode('utf-8')).hexdigest()
# ----------------------------------------------------------------------
# Main orchestration
# ----------------------------------------------------------------------
def main():
changed_files = get_changed_files()
if not changed_files:
print('🚫 No source changes detected; skipping documentation generation.')
return
print(f'🔎 Detected {len(changed_files)} changed Python files.')
all_results: List[GenerationResult] = []
for py_file in changed_files:
symbols = extract_symbols(py_file)
for sym in symbols:
prompt = build_prompt(sym)
claude_out = call_claude(prompt)
gpt_out = call_gpt(prompt)
merged = merge_responses(claude_out, gpt_out)
md_body = render_markdown(sym, merged)
# Determine target path: docs/<module>/<symbol>.md
rel_module = py_file.relative_to(SRC_DIR).with_suffix('')
target_path = DOCS_DIR / rel_module / f\"{sym.name}.md\"
result = GenerationResult(path=target_path, content=md_body, checksum=checksum(md_body))
all_results.append(result)
# Load previous cache (if any) to compute diffs
prev_cache = {}
if CACHE_FILE.exists():
prev_cache = json.loads(CACHE_FILE.read_text())
# Write only changed docs
updated = 0
for res in all_results:
prev_hash = prev_cache.get(str(res.path))
if prev_hash != res.checksum:
write_doc(res)
updated += 1
print(f'✍️ Updated {res.path}')
else:
print(f'✅ No change for {res.path}')
# Update cache file
new_cache = {str(r.path): r.checksum for r in all_results}
CACHE_FILE.write_text(json.dumps(new_cache, indent=2))
# If any files changed, we exit with a special code so CI can commit them
if updated:
print(f'🟢 Documentation updated for {updated} files.')
exit(0) # success – CI job will push the commit
else:
print('🟡 All documentation already up‑to‑date.')
exit(0)
if __name__ == '__main__':
main()
Key points:
- The script only processes files that changed since the
docs-lasttag, keeping CI fast. - We store a SHA‑256 checksum per generated file; unchanged docs are left untouched, preserving history.
- Both Claude 4.6 Opus and GPT‑5.4 Pro are called in parallel (you could spin them off with
asynciofor even faster runs). - A lightweight JSON cache (
.docgen_cache.json) survives across pipeline runs.
3. CI/CD Pipelines – GitHub Actions Example
Below is a production‑grade workflow that builds the container, runs the generator, and pushes documentation updates back to the same branch. The job also publishes the generated site to GitHub Pages.
# .github/workflows/docs.yml
name: 📚 Documentation Auto‑Update
on:
push:
branches: [ main, develop ]
paths:
- 'src/**/*.py' # trigger only on code changes
workflow_dispatch: # manual run
permissions:
contents: write # needed to push docs
pages: write
id-token: write
jobs:
docgen:
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # required for diff/tag logic
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USER }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build docgen image
run: |
docker build -t myorg/docgen:${{ github.sha }} -f docgen/Dockerfile .
- name: Run doc generation
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
REPO_ROOT: ${{ github.workspace }}
run: |
docker run --rm \
-e ANTHROPIC_API_KEY \
-e OPENAI_API_KEY \
-e REPO_ROOT=/workdir \
-v ${{ github.workspace }}:/workdir \
myorg/docgen:${{ github.sha }}
- name: Commit updated docs
if: success()
run: |
git config user.name "ci-bot"
git config user.email "ci-bot@myorg.com"
git add docs/
if git diff --cached --quiet; then
echo "No doc changes detected."
else
git commit -m "🤖 Auto‑generated documentation (CI)"
git tag -f docs-last
git push origin HEAD:${{ github.ref }} --follow-tags
fi
- name: Build MkDocs site
run: |
pip install mkdocs mkdocs-material
mkdocs build --clean
- name: Deploy to GitHub Pages
uses: actions/upload-pages-artifact@v3
with:
path: site/
- name: Publish Pages
uses: actions/deploy-pages@v4
Notice the use of a docs-last tag: each successful run overwrites the tag, providing a reliable anchor for git diff in the next cycle.
4. CI/CD Pipelines – GitLab CI Example (with Duo AI Review)
GitLab’s native AI integration (Duo) can add a comment with a summary of changes, making reviewers aware of newly added or modified documentation.
# .gitlab-ci.yml
stages:
- docgen
- pages
variables:
DOCKER_IMAGE: registry.myorg.com/docgen:${
❓ Frequently Asked Questions
How can I trigger documentation regeneration automatically when code changes?
Add a CI/CD pipeline step (GitHub Actions or GitLab CI) that runs your doc‑generator script on every push or merge to the main branch, then commits the updated docs folder back to the repo.
Do I need to store generated docs in the same repository?
Yes, keeping the generated `docs/` folder in the same repo lets the CI job commit changes directly, and the static site generator (MkDocs) can publish them from the same source.
What permissions are required for the CI job to push updates?
Configure a personal access token (or GitHub/GitLab bot) with `repo` scope, add it as a secret, and use it in the CI script to authenticate `git push` of the regenerated documentation.
How do I avoid infinite CI loops when the doc update triggers another pipeline?
Add a conditional check that skips the doc‑generation job if the commit message includes a specific tag (e.g., `[skip docs]`) or only run the job on changes to source files, not on docs folder updates.
🔗 You Might Also Like
📺 Recommended Video
Watch this video for a practical overview of the topic covered in this article.
✍️ About the Author
Vijay Vinoth — Lead Programmer Analyst with expertise in PHP, Perl, Python, and Shell scripting. Passionate about AI, automation, and building scalable systems. Writing to share practical insights from real-world engineering experience.
Note: This technical analysis reflects my independent understanding as a Lead Programmer Analyst as of November 2026.
As AI ecosystems like Claude 4.6 Opus evolve, actual implementation may vary. Refer to official documentation for final specs.