How To: CI/CD Linting Integration
This guide shows how to integrate documentation linting into your CI/CD pipeline for automated validation.
Overview
CI/CD linting provides:
- Automated validation: Catch issues before merge
- Consistent enforcement: All PRs validated equally
- Visible feedback: Clear error messages in PR comments
- Quality gates: Block merges if docs fail validation
GitHub Actions
Basic Workflow
Create .github/workflows/lint-docs.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
name: Lint Documentation
on:
pull_request:
paths:
- 'docs/**'
- '**.md'
push:
branches:
- main
jobs:
lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Install DocBuilder
run: |
go install github.com/your-org/docbuilder/cmd/docbuilder@latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Lint Documentation
run: |
docbuilder lint --format=json > lint-report.json
docbuilder lint # Human-readable output
- name: Upload Lint Report
if: always()
uses: actions/upload-artifact@v3
with:
name: lint-report
path: lint-report.json
retention-days: 30
|
Features:
- ✅ Runs on PRs affecting docs
- ✅ Installs DocBuilder from source
- ✅ Generates both JSON and text reports
- ✅ Uploads artifacts for later review
Post lint results directly on PR:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
|
name: Lint Documentation with PR Comments
on:
pull_request:
paths:
- 'docs/**'
- '**.md'
jobs:
lint:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # Required for PR comments
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Install DocBuilder
run: |
go install github.com/your-org/docbuilder/cmd/docbuilder@latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Lint Documentation
id: lint
run: |
set +e # Don't fail on linting errors
docbuilder lint --format=json > lint-report.json
LINT_EXIT=$?
echo "exit_code=$LINT_EXIT" >> $GITHUB_OUTPUT
# Generate summary
ERROR_COUNT=$(jq '[.issues[] | select(.severity=="error")] | length' lint-report.json)
WARNING_COUNT=$(jq '[.issues[] | select(.severity=="warning")] | length' lint-report.json)
echo "errors=$ERROR_COUNT" >> $GITHUB_OUTPUT
echo "warnings=$WARNING_COUNT" >> $GITHUB_OUTPUT
exit $LINT_EXIT
- name: Comment PR - Success
if: steps.lint.outputs.exit_code == '0'
uses: actions/github-script@v6
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: '✅ **Documentation linting passed!**\n\nAll files meet linting standards.'
})
- name: Comment PR - Warnings
if: steps.lint.outputs.exit_code == '1'
uses: actions/github-script@v6
with:
script: |
const warnings = ${{ steps.lint.outputs.warnings }};
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `⚠️ **Documentation has ${warnings} warning(s)**\n\nConsider fixing before merge. Run \`docbuilder lint --fix\` locally.`
})
- name: Comment PR - Errors
if: steps.lint.outputs.exit_code == '2'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const report = JSON.parse(fs.readFileSync('lint-report.json', 'utf8'));
const errors = report.issues.filter(i => i.severity === 'error');
let comment = `❌ **Documentation linting failed with ${errors.length} error(s)**\n\n`;
comment += '### Errors\n\n';
errors.slice(0, 10).forEach(issue => {
comment += `- **${issue.file}**\n`;
comment += ` \`${issue.message}\`\n\n`;
});
if (errors.length > 10) {
comment += `\n_...and ${errors.length - 10} more errors. Download full report from artifacts._\n`;
}
comment += '\n**How to fix**:\n';
comment += '```bash\n';
comment += 'docbuilder lint --fix\n';
comment += 'git add -A\n';
comment += 'git commit -m "docs: fix linting issues"\n';
comment += '```';
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
})
- name: Upload Report
if: always()
uses: actions/upload-artifact@v3
with:
name: lint-report
path: lint-report.json
- name: Fail if errors
if: steps.lint.outputs.exit_code == '2'
run: exit 1
|
Features:
- ✅ Posts summary comment on PR
- ✅ Shows first 10 errors inline
- ✅ Provides fix instructions
- ✅ Fails workflow if errors found
- ✅ Allows warnings without blocking
Auto-Fix on Push
Automatically fix issues and commit:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
name: Auto-Fix Documentation
on:
push:
branches:
- main
paths:
- 'docs/**'
- '**.md'
jobs:
auto-fix:
runs-on: ubuntu-latest
permissions:
contents: write # Required to push commits
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Install DocBuilder
run: |
go install github.com/your-org/docbuilder/cmd/docbuilder@latest
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
- name: Auto-Fix Issues
run: |
docbuilder lint --fix --yes
- name: Commit Fixes
run: |
git config user.name "docbuilder-bot"
git config user.email "bot@example.com"
if [[ -n $(git status -s) ]]; then
git add -A
git commit -m "docs: auto-fix linting issues [skip ci]"
git push
else
echo "No fixes needed"
fi
|
⚠️ Warning: Auto-fix on push can create unexpected commits. Consider using only for specific branches or requiring review.
Lint Only Changed Files
Optimize CI by linting only changed files:
1
2
3
4
5
6
7
8
9
10
11
12
|
- name: Get Changed Files
id: changed-files
run: |
git fetch origin ${{ github.base_ref }}
CHANGED=$(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -E '\.(md|markdown)$' || echo "")
echo "files=$CHANGED" >> $GITHUB_OUTPUT
echo "$CHANGED" > changed_files.txt
- name: Lint Changed Files
if: steps.changed-files.outputs.files != ''
run: |
cat changed_files.txt | xargs docbuilder lint
|
GitLab CI
Basic Pipeline
Add to .gitlab-ci.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
stages:
- test
lint-docs:
stage: test
image: golang:1.21
before_script:
- go install github.com/your-org/docbuilder/cmd/docbuilder@latest
- export PATH=$PATH:$(go env GOPATH)/bin
script:
- docbuilder lint --format=json | tee lint-report.json
- docbuilder lint # Human-readable output
artifacts:
when: always
paths:
- lint-report.json
reports:
junit: lint-report.json
expire_in: 30 days
only:
changes:
- docs/**
- '**/*.md'
|
Post lint results to merge request:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
lint-docs-with-comments:
stage: test
image: golang:1.21
before_script:
- go install github.com/your-org/docbuilder/cmd/docbuilder@latest
- export PATH=$PATH:$(go env GOPATH)/bin
- apt-get update && apt-get install -y jq curl
script:
- |
set +e
docbuilder lint --format=json > lint-report.json
LINT_EXIT=$?
ERROR_COUNT=$(jq '[.issues[] | select(.severity=="error")] | length' lint-report.json)
WARNING_COUNT=$(jq '[.issues[] | select(.severity=="warning")] | length' lint-report.json)
if [ "$LINT_EXIT" -eq 2 ]; then
STATUS="❌ **Linting failed** with $ERROR_COUNT error(s)"
elif [ "$LINT_EXIT" -eq 1 ]; then
STATUS="⚠️ **Linting passed** with $WARNING_COUNT warning(s)"
else
STATUS="✅ **Linting passed**"
fi
COMMENT="$STATUS\n\nRun \`docbuilder lint --fix\` to auto-fix issues."
# Post comment using GitLab API
curl --request POST \
--header "PRIVATE-TOKEN: $CI_JOB_TOKEN" \
--header "Content-Type: application/json" \
--data "{\"body\": \"$COMMENT\"}" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
exit $LINT_EXIT
artifacts:
when: always
paths:
- lint-report.json
only:
- merge_requests
|
Auto-Fix on Main
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
auto-fix-docs:
stage: fix
image: golang:1.21
before_script:
- go install github.com/your-org/docbuilder/cmd/docbuilder@latest
- export PATH=$PATH:$(go env GOPATH)/bin
- git config user.name "DocBuilder Bot"
- git config user.email "bot@example.com"
script:
- docbuilder lint --fix --yes
- |
if [[ -n $(git status -s) ]]; then
git add -A
git commit -m "docs: auto-fix linting issues [skip ci]"
git push "https://oauth2:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:main
fi
only:
- main
when: on_success
|
Jenkins
Pipeline Configuration
Create Jenkinsfile:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
pipeline {
agent any
environment {
GOPATH = "${WORKSPACE}/go"
PATH = "${PATH}:${GOPATH}/bin"
}
stages {
stage('Setup') {
steps {
sh 'go install github.com/your-org/docbuilder/cmd/docbuilder@latest'
}
}
stage('Lint Documentation') {
steps {
script {
def lintStatus = sh(
script: 'docbuilder lint --format=json > lint-report.json && docbuilder lint',
returnStatus: true
)
archiveArtifacts artifacts: 'lint-report.json', allowEmptyArchive: false
if (lintStatus == 2) {
error("Documentation linting failed with errors")
} else if (lintStatus == 1) {
unstable("Documentation has warnings")
}
}
}
}
}
post {
always {
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: '.',
reportFiles: 'lint-report.json',
reportName: 'Lint Report'
])
}
}
}
|
CircleCI
Configuration
Create .circleci/config.yml:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
|
version: 2.1
jobs:
lint-docs:
docker:
- image: cimg/go:1.21
steps:
- checkout
- restore_cache:
keys:
- go-mod-v1-{{ checksum "go.sum" }}
- run:
name: Install DocBuilder
command: |
go install github.com/your-org/docbuilder/cmd/docbuilder@latest
- save_cache:
key: go-mod-v1-{{ checksum "go.sum" }}
paths:
- "/home/circleci/go/pkg/mod"
- run:
name: Lint Documentation
command: |
docbuilder lint --format=json > lint-report.json
docbuilder lint
- store_artifacts:
path: lint-report.json
destination: lint-report
- store_test_results:
path: lint-report.json
workflows:
version: 2
lint:
jobs:
- lint-docs:
filters:
branches:
ignore:
- gh-pages
|
Generic CI Systems
Docker-Based Approach
For any CI that supports Docker:
1
2
3
4
5
6
7
8
9
10
|
# Dockerfile.lint
FROM golang:1.21-alpine
RUN apk add --no-cache git
RUN go install github.com/your-org/docbuilder/cmd/docbuilder@latest
WORKDIR /workspace
ENTRYPOINT ["docbuilder", "lint"]
|
Build image:
1
|
docker build -f Dockerfile.lint -t docbuilder-lint:latest .
|
Use in CI:
1
2
|
# Generic CI script
docker run --rm -v $(pwd):/workspace docbuilder-lint:latest --format=json > lint-report.json
|
Shell Script Approach
For CI without Docker support:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
#!/bin/bash
# lint-docs.sh
set -e
# Install Go if needed
if ! command -v go &> /dev/null; then
echo "Installing Go..."
wget https://go.dev/dl/go1.21.0.linux-amd64.tar.gz
tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz
export PATH=$PATH:/usr/local/go/bin
fi
# Install DocBuilder
echo "Installing DocBuilder..."
go install github.com/your-org/docbuilder/cmd/docbuilder@latest
export PATH=$PATH:$(go env GOPATH)/bin
# Run linting
echo "Linting documentation..."
docbuilder lint --format=json > lint-report.json
docbuilder lint
# Check exit code
LINT_EXIT=$?
if [ $LINT_EXIT -eq 2 ]; then
echo "❌ Linting failed with errors"
exit 1
elif [ $LINT_EXIT -eq 1 ]; then
echo "⚠️ Linting passed with warnings"
exit 0
else
echo "✅ Linting passed"
exit 0
fi
|
Advanced Patterns
Parallel Linting
Lint multiple directories in parallel:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
# GitHub Actions
jobs:
lint-api-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # ... setup steps
- run: docbuilder lint docs/api/
lint-guides:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # ... setup steps
- run: docbuilder lint docs/guides/
lint-reference:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # ... setup steps
- run: docbuilder lint docs/reference/
|
Conditional Enforcement
Different rules for different branches:
1
2
3
4
5
6
7
|
- name: Lint (Strict on Main)
if: github.ref == 'refs/heads/main'
run: docbuilder lint # Fail on any error
- name: Lint (Relaxed on Feature Branches)
if: github.ref != 'refs/heads/main'
run: docbuilder lint || true # Don't block
|
Scheduled Deep Scans
Run comprehensive linting weekly:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
name: Weekly Documentation Audit
on:
schedule:
- cron: '0 2 * * 0' # Sunday 2am
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- # ... setup steps
- name: Full Lint
run: |
docbuilder lint --format=json > weekly-audit.json
- name: Generate Report
run: |
jq -r '.issues[] | "\(.severity): \(.file) - \(.message)"' weekly-audit.json > weekly-report.txt
- name: Email Report
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.example.com
server_port: 465
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: Weekly Documentation Audit
to: docs-team@example.com
from: CI Bot
attachments: weekly-report.txt
|
Cache Dependencies
1
2
3
4
5
6
7
8
9
10
|
# GitHub Actions
- name: Cache Go modules
uses: actions/cache@v3
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
|
Incremental Linting
Only lint changed files:
1
2
3
4
5
6
7
8
9
10
11
12
|
- name: Get changed markdown files
id: changed-files
uses: tj-actions/changed-files@v39
with:
files: |
**/*.md
**/*.markdown
- name: Lint changed files
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "${{ steps.changed-files.outputs.all_changed_files }}" | xargs docbuilder lint
|
Matrix Builds
Test against multiple DocBuilder versions:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
jobs:
lint:
strategy:
matrix:
docbuilder-version: ['latest', 'v1.0.0', 'v1.1.0']
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install DocBuilder ${{ matrix.docbuilder-version }}
run: |
go install github.com/your-org/docbuilder/cmd/docbuilder@${{ matrix.docbuilder-version }}
- name: Lint
run: docbuilder lint
|
Monitoring and Metrics
Track Lint Success Rate
1
2
3
4
5
6
7
8
9
10
11
12
|
- name: Record Metrics
if: always()
run: |
LINT_EXIT=$?
curl -X POST https://metrics.example.com/api/lint \
-H "Content-Type: application/json" \
-d "{
\"repo\": \"${{ github.repository }}\",
\"pr\": \"${{ github.event.pull_request.number }}\",
\"exit_code\": $LINT_EXIT,
\"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}"
|
Dashboard Integration
Export metrics to dashboard tools:
1
2
3
4
5
6
7
8
|
- name: Export to DataDog
if: always()
run: |
ERROR_COUNT=$(jq '[.issues[] | select(.severity=="error")] | length' lint-report.json)
WARNING_COUNT=$(jq '[.issues[] | select(.severity=="warning")] | length' lint-report.json)
echo "lint.errors:$ERROR_COUNT|g|#repo:${{ github.repository }}" | nc -u -w1 datadog-agent 8125
echo "lint.warnings:$WARNING_COUNT|g|#repo:${{ github.repository }}" | nc -u -w1 datadog-agent 8125
|
Troubleshooting CI
Issue: CI Timeout
Solution: Lint only changed files or increase timeout
1
2
3
|
- name: Lint with timeout
timeout-minutes: 10
run: docbuilder lint
|
Issue: False Positives in CI
Solution: Ensure consistent environment
1
2
3
4
5
|
- name: Normalize line endings
run: git config core.autocrlf false
- name: Lint
run: docbuilder lint
|
Issue: Secrets in Error Messages
Solution: Sanitize output
1
2
3
|
- name: Lint
run: |
docbuilder lint 2>&1 | sed 's/${{ secrets.TOKEN }}/***REDACTED***/g'
|
Best Practices
- Start permissive: Allow warnings initially, tighten later
- Fast feedback: Lint only changed files in PRs
- Clear messages: Use PR comments for actionable feedback
- Auto-fix carefully: Only on trusted branches
- Monitor trends: Track lint success rates over time
- Document exceptions: Explain any
--no-verify usage
- Version pin: Use specific DocBuilder version in CI
Next Steps
CI Integration Checklist:
permalink[how-to-ci-cd-linting-integration](https://docs.home.luguber.info/_uid/a89ff86e-31ab-43b5-b751-05c37768b0ba/)