CI/CD Pipeline Setup
Azure Pipelines provide powerful automation capabilities for Documentation as Code workflows. This guide covers comprehensive pipeline configuration for building, testing, and deploying documentation sites with quality gates, security scanning, and multi-environment deployment strategies.
Prerequisites
Before setting up CI/CD pipelines, ensure you have:
- Azure DevOps project with appropriate permissions
- Azure App Service environments configured
- Service connections established
- DocFX project properly configured
- Understanding of YAML pipeline syntax
Pipeline Strategy
Pipeline Architecture
graph TB
A[Code Commit] --> B[Pull Request Pipeline]
A --> C[Main Branch Pipeline]
B --> D[Build Validation]
B --> E[Link Checking]
B --> F[Quality Gates]
C --> G[Build Documentation]
C --> H[Security Scanning]
C --> I[Deploy to Staging]
I --> J[Smoke Tests]
J --> K[Deploy to Production]
L[Scheduled Pipeline] --> M[Content Validation]
L --> N[Performance Testing]
L --> O[Analytics Reporting]
Pipeline Types
Pipeline Type | Trigger | Purpose | Environments |
---|---|---|---|
Pull Request | PR creation/update | Validation and quality checks | None (build only) |
Main Branch | Push to main | Full deployment cycle | Staging → Production |
Release | Manual/scheduled | Production deployment | Production only |
Maintenance | Scheduled | Health checks and updates | All environments |
Service Connections
Azure Service Connection
Create Service Principal:
# Create service principal for Azure connection
az ad sp create-for-rbac \
--name "docs-pipeline-sp" \
--role "Contributor" \
--scopes "/subscriptions/{subscription-id}/resourceGroups/docs-prod-rg-eastus"
Configure Service Connection in Azure DevOps:
- Navigate to Project Settings → Service connections
- Click New service connection → Azure Resource Manager
- Choose Service principal (automatic)
- Configure connection details:
# Service connection configuration
connection_name: "Azure-Documentation-Production"
subscription_id: "{subscription-id}"
subscription_name: "Production Subscription"
resource_group: "docs-prod-rg-eastus"
service_principal:
application_id: "{app-id}"
tenant_id: "{tenant-id}"
service_principal_key: "{secret}"
Repository Service Connection
Git Repository Access:
# Repository service connection
name: "DocumentationRepository"
type: "Git"
properties:
url: "https://dev.azure.com/organization/project/_git/docs"
authentication:
type: "PersonalAccessToken"
token: "{pat-token}"
Pull Request Pipeline
PR Validation Pipeline
azure-pipelines-pr.yml:
# Pull Request validation pipeline
name: 'Documentation-PR-$(Date:yyyyMMdd)$(Rev:.r)'
trigger: none
pr:
branches:
include:
- main
- develop
paths:
exclude:
- README.md
- .gitignore
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
docfxVersion: '2.75.3'
stages:
- stage: Validation
displayName: 'Pull Request Validation'
jobs:
- job: BuildValidation
displayName: 'Build and Validate Documentation'
steps:
- task: UseNode@1
displayName: 'Setup Node.js'
inputs:
version: '18.x'
- task: NuGetCommand@2
displayName: 'Install DocFX'
inputs:
command: 'custom'
arguments: 'install docfx.console -Version $(docfxVersion) -OutputDirectory packages'
- script: |
echo "##[section]Validating Markdown files"
npm install -g markdownlint-cli
markdownlint content/**/*.md --config .markdownlint.json
displayName: 'Markdown Validation'
continueOnError: false
- script: |
echo "##[section]Checking for broken links"
npm install -g markdown-link-check
find content -name "*.md" -exec markdown-link-check {} \;
displayName: 'Link Validation'
continueOnError: true
- script: |
echo "##[section]Building documentation"
./packages/docfx.console.$(docfxVersion)/tools/docfx.exe docfx.json
displayName: 'DocFX Build'
continueOnError: false
- script: |
echo "##[section]Validating build output"
if [ ! -f "_site/index.html" ]; then
echo "##vso[task.logissue type=error]Build output missing index.html"
exit 1
fi
if [ ! -f "_site/toc.html" ]; then
echo "##vso[task.logissue type=warning]Build output missing toc.html"
fi
echo "Build validation passed"
displayName: 'Output Validation'
- task: PublishTestResults@2
displayName: 'Publish Validation Results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/validation-results.xml'
mergeTestResults: true
failTaskOnFailedTests: true
condition: always()
- task: PublishBuildArtifacts@1
displayName: 'Publish Build Artifacts'
inputs:
pathToPublish: '_site'
artifactName: 'documentation-site-pr'
condition: succeededOrFailed()
- job: SecurityScan
displayName: 'Security Scanning'
dependsOn: BuildValidation
steps:
- task: CredScan@3
displayName: 'Credential Scanner'
inputs:
toolMajorVersion: 'V2'
scanFolder: '$(Build.SourcesDirectory)'
debugMode: false
- script: |
echo "##[section]Scanning for sensitive information"
# Custom security scanning
grep -r -i "password\|secret\|key\|token" content/ --exclude-dir=_site || echo "No sensitive information found"
displayName: 'Sensitive Information Scan'
- task: PublishSecurityAnalysisLogs@3
displayName: 'Publish Security Analysis Logs'
inputs:
artifactName: 'CodeAnalysisLogs'
toolLogsNotFoundAction: 'Standard'
- job: QualityGates
displayName: 'Quality Gates'
dependsOn: [BuildValidation, SecurityScan]
steps:
- script: |
echo "##[section]Checking documentation coverage"
# Calculate documentation coverage
total_files=$(find content -name "*.md" | wc -l)
documented_files=$(grep -l "^# \|^## " content/**/*.md | wc -l)
coverage=$((documented_files * 100 / total_files))
echo "Documentation coverage: $coverage%"
if [ $coverage -lt 80 ]; then
echo "##vso[task.logissue type=warning]Documentation coverage below 80%"
fi
displayName: 'Documentation Coverage Check'
- script: |
echo "##[section]Validating content freshness"
# Check for outdated content
find content -name "*.md" -mtime +90 -exec echo "Warning: {} not updated in 90+ days" \;
displayName: 'Content Freshness Check'
PR Comment Integration
PR Comments Script:
- task: PowerShell@2
displayName: 'Update PR with Build Status'
inputs:
targetType: 'inline'
script: |
$headers = @{
'Authorization' = "Bearer $(System.AccessToken)"
'Content-Type' = 'application/json'
}
$comment = @{
status = "completed"
state = "succeeded"
description = "Documentation build and validation completed successfully"
targetUrl = "$(System.CollectionUri)$(System.TeamProject)/_build/results?buildId=$(Build.BuildId)"
} | ConvertTo-Json
$uri = "$(System.CollectionUri)$(System.TeamProject)/_apis/git/repositories/$(Build.Repository.ID)/pullRequests/$(System.PullRequest.PullRequestId)/statuses?api-version=6.0"
try {
Invoke-RestMethod -Uri $uri -Method Post -Body $comment -Headers $headers
Write-Host "##[section]PR status updated successfully"
}
catch {
Write-Warning "##[warning]Failed to update PR status: $($_.Exception.Message)"
}
condition: always()
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
Main Branch Pipeline
Production Deployment Pipeline
azure-pipelines-main.yml:
# Main branch deployment pipeline
name: 'Documentation-Main-$(Date:yyyyMMdd)$(Rev:.r)'
trigger:
branches:
include:
- main
paths:
exclude:
- README.md
- .gitignore
- docs/**.md
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
docfxVersion: '2.75.3'
azureSubscription: 'Azure-Documentation-Production'
stagingAppName: 'docs-prod-app-eastus'
productionAppName: 'docs-prod-app-eastus'
resourceGroupName: 'docs-prod-rg-eastus'
stages:
- stage: Build
displayName: 'Build Documentation'
jobs:
- job: BuildDocs
displayName: 'Build Documentation Site'
steps:
- task: UseNode@1
displayName: 'Setup Node.js'
inputs:
version: '18.x'
- task: Cache@2
displayName: 'Cache DocFX'
inputs:
key: 'docfx | $(docfxVersion)'
path: 'packages'
cacheHitVar: 'CACHE_RESTORED'
- task: NuGetCommand@2
displayName: 'Install DocFX'
inputs:
command: 'custom'
arguments: 'install docfx.console -Version $(docfxVersion) -OutputDirectory packages'
condition: ne(variables.CACHE_RESTORED, 'true')
- script: |
echo "##[section]Building documentation with DocFX"
./packages/docfx.console.$(docfxVersion)/tools/docfx.exe docfx.json --logLevel Verbose
displayName: 'Build Documentation'
- script: |
echo "##[section]Optimizing assets"
# Minify CSS and JavaScript
npm install -g clean-css-cli uglify-js
find _site -name "*.css" -exec cleancss -o {} {} \;
find _site -name "*.js" -not -path "*/node_modules/*" -exec uglifyjs {} -c -m -o {} \;
# Optimize images
npm install -g imagemin-cli imagemin-mozjpeg imagemin-pngquant
imagemin "_site/images/*.{jpg,png}" --out-dir="_site/images" --plugin=mozjpeg --plugin=pngquant
displayName: 'Asset Optimization'
- script: |
echo "##[section]Generating sitemap"
npm install -g sitemap-generator-cli
sitemap-generator https://docs.yourdomain.com --save _site/sitemap.xml --priority-map "1.0,0.8,0.6"
displayName: 'Generate Sitemap'
- task: PublishBuildArtifacts@1
displayName: 'Publish Documentation Artifacts'
inputs:
pathToPublish: '_site'
artifactName: 'documentation-site'
- stage: SecurityScan
displayName: 'Security Scanning'
dependsOn: Build
jobs:
- job: SecurityAnalysis
displayName: 'Security Analysis'
steps:
- download: current
artifact: 'documentation-site'
- task: WhiteSource@21
displayName: 'WhiteSource Security Scan'
inputs:
cwd: '$(Pipeline.Workspace)/documentation-site'
productName: 'Documentation'
projectName: 'Documentation-Main'
- script: |
echo "##[section]Scanning for security vulnerabilities"
# Check for mixed content
grep -r "http://" $(Pipeline.Workspace)/documentation-site/ && echo "##vso[task.logissue type=warning]Mixed content detected" || echo "No mixed content found"
# Check for exposed sensitive data
grep -r -E "(api[_-]?key|password|secret|token)" $(Pipeline.Workspace)/documentation-site/ && echo "##vso[task.logissue type=error]Potential sensitive data exposed" || echo "No sensitive data found"
displayName: 'Custom Security Checks'
- stage: Deploy_Staging
displayName: 'Deploy to Staging'
dependsOn: [Build, SecurityScan]
condition: succeeded()
jobs:
- deployment: DeployStaging
displayName: 'Deploy to Staging Environment'
environment: 'Documentation-Staging'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: 'documentation-site'
- task: AzureWebApp@1
displayName: 'Deploy to Staging Slot'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webApp'
appName: '$(stagingAppName)'
slotName: 'staging'
package: '$(Pipeline.Workspace)/documentation-site'
deploymentMethod: 'zipDeploy'
- task: AzureAppServiceManage@0
displayName: 'Start Staging Slot'
inputs:
azureSubscription: '$(azureSubscription)'
action: 'Start Azure App Service'
webAppName: '$(stagingAppName)'
specifySlotOrASE: true
slot: 'staging'
- stage: Testing
displayName: 'Staging Tests'
dependsOn: Deploy_Staging
jobs:
- job: SmokeTests
displayName: 'Smoke Tests'
steps:
- script: |
echo "##[section]Running smoke tests against staging"
staging_url="https://$(stagingAppName)-staging.azurewebsites.net"
# Test homepage
response=$(curl -s -o /dev/null -w "%{http_code}" "$staging_url")
if [ "$response" != "200" ]; then
echo "##vso[task.logissue type=error]Homepage test failed: HTTP $response"
exit 1
fi
# Test search functionality
search_response=$(curl -s -o /dev/null -w "%{http_code}" "$staging_url/search.html")
if [ "$search_response" != "200" ]; then
echo "##vso[task.logissue type=warning]Search page test failed: HTTP $search_response"
fi
# Test API documentation
api_response=$(curl -s -o /dev/null -w "%{http_code}" "$staging_url/api/")
if [ "$api_response" != "200" ]; then
echo "##vso[task.logissue type=warning]API documentation test failed: HTTP $api_response"
fi
echo "Smoke tests completed successfully"
displayName: 'Smoke Tests'
- job: PerformanceTests
displayName: 'Performance Tests'
steps:
- script: |
echo "##[section]Running performance tests"
npm install -g lighthouse-ci
staging_url="https://$(stagingAppName)-staging.azurewebsites.net"
# Run Lighthouse audit
lhci autorun --upload.target=temporary-public-storage --collect.url="$staging_url"
displayName: 'Lighthouse Performance Audit'
- stage: Deploy_Production
displayName: 'Deploy to Production'
dependsOn: Testing
condition: succeeded()
jobs:
- deployment: DeployProduction
displayName: 'Deploy to Production Environment'
environment: 'Documentation-Production'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: 'documentation-site'
- task: AzureWebApp@1
displayName: 'Deploy to Production'
inputs:
azureSubscription: '$(azureSubscription)'
appType: 'webApp'
appName: '$(productionAppName)'
package: '$(Pipeline.Workspace)/documentation-site'
deploymentMethod: 'zipDeploy'
- task: AzureAppServiceManage@0
displayName: 'Restart Production App'
inputs:
azureSubscription: '$(azureSubscription)'
action: 'Restart Azure App Service'
webAppName: '$(productionAppName)'
- script: |
echo "##[section]Post-deployment validation"
production_url="https://docs.yourdomain.com"
# Wait for deployment to be ready
sleep 30
# Validate deployment
response=$(curl -s -o /dev/null -w "%{http_code}" "$production_url")
if [ "$response" != "200" ]; then
echo "##vso[task.logissue type=error]Production deployment validation failed: HTTP $response"
exit 1
fi
echo "Production deployment validated successfully"
displayName: 'Validate Production Deployment'
- stage: PostDeployment
displayName: 'Post-Deployment Tasks'
dependsOn: Deploy_Production
jobs:
- job: CacheWarming
displayName: 'Cache Warming'
steps:
- script: |
echo "##[section]Warming up caches"
production_url="https://docs.yourdomain.com"
# Warm up key pages
curl -s "$production_url" > /dev/null
curl -s "$production_url/api/" > /dev/null
curl -s "$production_url/articles/" > /dev/null
curl -s "$production_url/tutorials/" > /dev/null
echo "Cache warming completed"
displayName: 'Warm Up Caches'
- job: Notifications
displayName: 'Send Notifications'
steps:
- task: PowerShell@2
displayName: 'Send Teams Notification'
inputs:
targetType: 'inline'
script: |
$webhook = "$(TeamsWebhookUrl)"
$message = @{
"@type" = "MessageCard"
"@context" = "https://schema.org/extensions"
summary = "Documentation Deployment Completed"
themeColor = "00FF00"
sections = @(
@{
activityTitle = "Documentation Site Updated"
activitySubtitle = "Build $(Build.BuildNumber) deployed successfully"
facts = @(
@{ name = "Environment"; value = "Production" }
@{ name = "Build"; value = "$(Build.BuildNumber)" }
@{ name = "Commit"; value = "$(Build.SourceVersion)" }
@{ name = "Triggered by"; value = "$(Build.RequestedFor)" }
)
}
)
potentialAction = @(
@{
"@type" = "OpenUri"
name = "View Documentation"
targets = @(
@{ os = "default"; uri = "https://docs.yourdomain.com" }
)
}
)
} | ConvertTo-Json -Depth 10
try {
Invoke-RestMethod -Uri $webhook -Method Post -Body $message -ContentType 'application/json'
Write-Host "##[section]Notification sent successfully"
}
catch {
Write-Warning "##[warning]Failed to send notification: $($_.Exception.Message)"
}
condition: always()
Maintenance Pipeline
Scheduled Maintenance
azure-pipelines-maintenance.yml:
# Scheduled maintenance pipeline
name: 'Documentation-Maintenance-$(Date:yyyyMMdd)'
trigger: none
schedules:
- cron: "0 2 * * 0"
displayName: Weekly maintenance
branches:
include:
- main
always: true
pool:
vmImage: 'ubuntu-latest'
jobs:
- job: HealthCheck
displayName: 'Documentation Health Check'
steps:
- script: |
echo "##[section]Comprehensive link checking"
npm install -g markdown-link-check
# Check all markdown files
find content -name "*.md" -exec markdown-link-check --config .mlc-config.json {} \; > link-check-results.txt
# Report broken links
broken_links=$(grep -c "ERROR" link-check-results.txt || echo "0")
if [ "$broken_links" -gt "0" ]; then
echo "##vso[task.logissue type=warning]Found $broken_links broken links"
grep "ERROR" link-check-results.txt
fi
displayName: 'Link Health Check'
- script: |
echo "##[section]Content freshness analysis"
# Find stale content
find content -name "*.md" -mtime +180 -exec echo "Stale content: {}" \;
# Find missing metadata
grep -L "^---" content/**/*.md | head -10
displayName: 'Content Analysis'
- job: SecurityScan
displayName: 'Security Scan'
steps:
- script: |
echo "##[section]Dependency vulnerability scan"
npm audit --audit-level high
# Check for outdated packages
npm outdated
displayName: 'Dependency Security Scan'
- job: PerformanceAudit
displayName: 'Performance Audit'
steps:
- script: |
echo "##[section]Performance audit"
npm install -g lighthouse-ci
# Run performance audit
lhci autorun --collect.url="https://docs.yourdomain.com"
displayName: 'Lighthouse Audit'
- job: Analytics
displayName: 'Analytics Report'
steps:
- task: PowerShell@2
displayName: 'Generate Analytics Report'
inputs:
targetType: 'inline'
script: |
# Generate weekly analytics report
$report = @{
date = Get-Date -Format "yyyy-MM-dd"
metrics = @{
# Add Application Insights queries here
}
}
Write-Host "##[section]Weekly analytics report generated"
Advanced Pipeline Features
Matrix Builds
Multi-environment Testing:
strategy:
matrix:
Windows:
imageName: 'windows-latest'
docfxCommand: 'packages\docfx.console.$(docfxVersion)\tools\docfx.exe'
Linux:
imageName: 'ubuntu-latest'
docfxCommand: './packages/docfx.console.$(docfxVersion)/tools/docfx'
macOS:
imageName: 'macOS-latest'
docfxCommand: './packages/docfx.console.$(docfxVersion)/tools/docfx'
pool:
vmImage: $(imageName)
Conditional Deployments
Environment-based Conditions:
- stage: Deploy_Development
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
- stage: Deploy_Production
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
Variable Groups
Shared Configuration:
variables:
- group: 'Documentation-Shared'
- group: 'Documentation-Production'
- name: 'buildConfiguration'
value: 'Release'
Pipeline Templates
Reusable Build Template
templates/build-docs.yml:
parameters:
- name: environment
type: string
- name: docfxVersion
type: string
default: '2.75.3'
steps:
- task: UseNode@1
displayName: 'Setup Node.js'
inputs:
version: '18.x'
- task: NuGetCommand@2
displayName: 'Install DocFX'
inputs:
command: 'custom'
arguments: 'install docfx.console -Version ${{ parameters.docfxVersion }} -OutputDirectory packages'
- script: |
echo "##[section]Building for ${{ parameters.environment }}"
./packages/docfx.console.${{ parameters.docfxVersion }}/tools/docfx.exe docfx.json
displayName: 'Build Documentation'
- task: PublishBuildArtifacts@1
displayName: 'Publish Artifacts'
inputs:
pathToPublish: '_site'
artifactName: 'documentation-${{ parameters.environment }}'
Using the Template:
steps:
- template: templates/build-docs.yml
parameters:
environment: 'production'
docfxVersion: '2.75.3'
Monitoring and Alerts
Pipeline Health Monitoring
Pipeline Health Dashboard:
# Add to pipeline variables
variables:
- name: 'dashboardId'
value: 'docs-pipeline-health'
Alert Configuration:
{
"alertRules": [
{
"name": "Pipeline Failure Alert",
"condition": "Build.Result eq 'Failed'",
"actions": [
{
"type": "email",
"recipients": ["devops-team@organization.com"]
},
{
"type": "teams",
"webhook": "$(TeamsWebhookUrl)"
}
]
}
]
}
Troubleshooting
Common Pipeline Issues
Build Failures:
- script: |
echo "##[section]Diagnostic information"
echo "DocFX version: $(docfxVersion)"
echo "Build configuration: $(buildConfiguration)"
echo "Source branch: $(Build.SourceBranch)"
ls -la packages/
cat docfx.json
displayName: 'Diagnostic Information'
condition: failed()
Deployment Issues:
- script: |
echo "##[section]Deployment diagnostics"
az webapp log tail --name $(appName) --resource-group $(resourceGroupName)
displayName: 'Check Deployment Logs'
condition: failed()
Next Steps
After setting up CI/CD pipelines:
- Content Strategy - Plan your documentation content approach
- Team Training - Train team members on the new workflow
- Monitoring Setup - Implement comprehensive monitoring
- Performance Optimization - Optimize build and deployment performance
Additional Resources
This CI/CD pipeline setup provides comprehensive automation for Documentation as Code with quality gates, security scanning, and production-ready deployment strategies.