Markdownlint CI/CD Integration
GitHub Actions
Basic Workflow
name: Markdown Lint
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint markdown files
run: npm run lint:md
Using markdownlint-cli2-action
name: Markdown Lint
on: [push, pull_request]
jobs:
markdown-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Lint markdown
uses: DavidAnson/markdownlint-cli2-action@v16
with:
globs: |
**/*.md
#node_modules
With Auto-fix and Commit
name: Markdown Lint & Fix
on:
pull_request:
branches: [main]
jobs:
markdown-lint-fix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.head_ref }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Fix markdown issues
run: npm run lint:md:fix
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: 'docs: auto-fix markdown lint issues'
file_pattern: '*.md'
Matrix Strategy (Multiple Node Versions)
name: Markdown Lint
on: [push, pull_request]
jobs:
markdown-lint:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm run lint:md
GitLab CI
Basic Pipeline
stages:
- test
markdown-lint:
stage: test
image: node:20
before_script:
- npm ci
script:
- npm run lint:md
only:
- merge_requests
- main
- develop
With Caching
markdown-lint:
stage: test
image: node:20
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
before_script:
- npm ci
script:
- npm run lint:md
artifacts:
reports:
junit: markdown-lint-report.xml
when: always
With Auto-fix
markdown-lint-fix:
stage: test
image: node:20
before_script:
- npm ci
- git config user.name "GitLab CI"
- git config user.email "ci@gitlab.com"
script:
- npm run lint:md:fix
- |
if [[ -n $(git status -s) ]]; then
git add "*.md"
git commit -m "docs: auto-fix markdown lint issues [skip ci]"
git push "https://oauth2:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git" HEAD:${CI_COMMIT_REF_NAME}
fi
only:
- merge_requests
CircleCI
version: 2.1
executors:
node-executor:
docker:
- image: cimg/node:20.0
jobs:
markdown-lint:
executor: node-executor
steps:
- checkout
- restore_cache:
keys:
- node-deps-{{ checksum "package-lock.json" }}
- run:
name: Install dependencies
command: npm ci
- save_cache:
key: node-deps-{{ checksum "package-lock.json" }}
paths:
- node_modules
- run:
name: Lint markdown
command: npm run lint:md
workflows:
version: 2
test:
jobs:
- markdown-lint
Jenkins
Declarative Pipeline
pipeline {
agent any
tools {
nodejs 'NodeJS 20'
}
stages {
stage('Install') {
steps {
sh 'npm ci'
}
}
stage('Lint Markdown') {
steps {
sh 'npm run lint:md'
}
}
}
post {
always {
cleanWs()
}
failure {
emailext(
subject: "Markdown Lint Failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check console output at ${env.BUILD_URL}",
to: "team@example.com"
)
}
}
}
Scripted Pipeline
node {
stage('Checkout') {
checkout scm
}
stage('Install') {
nodejs(nodeJSInstallationName: 'NodeJS 20') {
sh 'npm ci'
}
}
stage('Lint Markdown') {
nodejs(nodeJSInstallationName: 'NodeJS 20') {
sh 'npm run lint:md'
}
}
}
Azure Pipelines
trigger:
- main
- develop
pool:
vmImage: 'ubuntu-latest'
steps:
- task: NodeTool@0
inputs:
versionSpec: '20.x'
displayName: 'Install Node.js'
- script: npm ci
displayName: 'Install dependencies'
- script: npm run lint:md
displayName: 'Lint markdown files'
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/markdown-lint-report.xml'
failTaskOnFailedTests: true
Pre-commit Hooks
Husky + lint-staged
package.json:
{
"scripts": {
"prepare": "husky install"
},
"lint-staged": {
"*.md": [
"markdownlint-cli2 --fix",
"git add"
]
},
"devDependencies": {
"husky": "^9.0.0",
"lint-staged": "^15.0.0",
"markdownlint-cli2": "^0.12.0"
}
}
.husky/pre-commit:
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
Setup:
npm install -D husky lint-staged
npx husky install
npx husky add .husky/pre-commit "npx lint-staged"
pre-commit Framework
.pre-commit-config.yaml:
repos:
- repo: https://github.com/DavidAnson/markdownlint-cli2
rev: v0.12.1
hooks:
- id: markdownlint-cli2
args: ['--fix']
Setup:
pip install pre-commit
pre-commit install
Docker Integration
Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run lint:md
docker-compose.yml
version: '3.8'
services:
markdown-lint:
image: node:20-alpine
working_dir: /app
volumes:
- .:/app
command: sh -c "npm ci && npm run lint:md"
Usage:
docker-compose run markdown-lint
Monorepo Integration
Turborepo
turbo.json:
{
"pipeline": {
"lint:md": {
"outputs": [],
"cache": true
}
}
}
package.json (root):
{
"scripts": {
"lint:md": "turbo run lint:md"
}
}
packages/docs/package.json:
{
"scripts": {
"lint:md": "markdownlint-cli2 \"**/*.md\""
}
}
Lerna
lerna.json:
{
"version": "independent",
"npmClient": "npm",
"command": {
"run": {
"stream": true
}
}
}
package.json (root):
{
"scripts": {
"lint:md": "lerna run lint:md"
}
}
Reporting and Metrics
Generate HTML Report
// scripts/markdown-lint-report.js
const { markdownlint } = require('markdownlint/promise');
const fs = require('fs').promises;
async function generateReport() {
const result = await markdownlint({
files: ['**/*.md'],
config: { default: true }
});
let html = '<html><body><h1>Markdown Lint Report</h1>';
Object.keys(result).forEach(file => {
const errors = result[file];
if (errors.length > 0) {
html += `<h2>${file}</h2><ul>`;
errors.forEach(err => {
html += `<li>Line ${err.lineNumber}: ${err.ruleDescription}</li>`;
});
html += '</ul>';
}
});
html += '</body></html>';
await fs.writeFile('markdown-lint-report.html', html);
}
generateReport();
JSON Output for CI
package.json:
{
"scripts": {
"lint:md:json": "markdownlint-cli2 --config .markdownlint-cli2.jsonc \"**/*.md\""
}
}
.markdownlint-cli2.jsonc:
{
"outputFormatters": [
["markdownlint-cli2-formatter-json", { "path": "markdown-lint-report.json" }]
]
}
Best Practices
- CI Integration - Run markdown linting on every PR
- Pre-commit Hooks - Catch errors before commit
- Auto-fix in Development - Use
--fixlocally, not in CI - Fail Fast - Exit with error code on lint failures
- Cache Dependencies - Speed up CI with npm/node_modules caching
- Ignore Generated Files - Exclude
node_modules,dist, etc. - Version Lock - Pin markdownlint version in package.json
- Separate Jobs - Keep linting separate from tests/builds
- Report Artifacts - Save lint reports as build artifacts
- Matrix Testing - Test on multiple OS/Node versions if needed
Common Pitfalls
- Missing Ignore Patterns - Forgetting to ignore
node_modules - Wrong Glob Patterns - Using incorrect file patterns
- Config Not Found - Config file not in expected location
- CI Timeout - Linting too many files without exclusions
- No Exit Code Check - Not failing build on lint errors
- Auto-fix in CI - Using
--fixin CI can cause unexpected commits - Cache Poisoning - Not invalidating cache when config changes
- Slow Builds - Not using caching or parallel jobs