I once worked on a team that was fully committed to Azure Boards for planning and Azure Repos for source control, but wanted to keep Jenkins for the actual build and deploy orchestration since that’s where all our existing pipeline logic already lived. That combination turns out to work really well once you understand how the two systems talk to each other. This guide covers connecting Jenkins to Azure DevOps for source control triggers, build status reporting, and deployment to Azure.
Why Combine Jenkins and Azure DevOps
Azure DevOps offers its own native pipelines (Azure Pipelines), so pairing it with Jenkins makes sense when you already have Jenkins infrastructure and expertise, need Jenkins-specific plugins not available in Azure Pipelines, or run a hybrid environment where some services deploy via Jenkins and others via Azure Pipelines, with Azure Repos as the shared source of truth.
Jenkins Architecture in This Setup
- Controller: Receives webhook triggers from Azure Repos and reports build status back to Azure DevOps via its REST API.
- Agent: Runs the actual build, test, and deployment steps, typically authenticating to Azure using a Service Principal.
- Azure DevOps: Hosts the Git repository (Azure Repos) and/or work item tracking, and can display Jenkins build status directly on pull requests via the Jenkins service connection.
Prerequisites
- An Azure DevOps organization and project with a Git repository
- Jenkins (2.4+ LTS)
- An Azure subscription with permissions to create a Service Principal
- Azure CLI installed on the Jenkins agent
Step 1: Install Required Plugins
- Azure Credentials Plugin
- Team Foundation Server Plugin (supports Azure Repos/TFS source control integration)
- Git Plugin
- Azure CLI Plugin (optional convenience wrapper)
Step 2: Create a Service Principal for Azure Authentication
az login
az ad sp create-for-rbac --name "jenkins-deploy-sp" \
--role Contributor \
--scopes /subscriptions/<subscription-id>/resourceGroups/<resource-group>
This returns an appId, password, and tenant — store these in Jenkins Credentials as an “Azure Service Principal” credential type (provided by the Azure Credentials Plugin).
Step 3: Connect Jenkins to Azure Repos
- In Azure DevOps, go to Project Settings > Repositories and generate a Personal Access Token (PAT) with Code (Read) scope, or set up SSH keys.
- In Jenkins, create a new Pipeline job (or multibranch pipeline) and set the repository URL to your Azure Repos Git URL:
https://dev.azure.com/{org}/{project}/_git/{repo}. - Add the PAT as a Jenkins “Username with password” credential (username can be anything, password is the PAT).
Step 4: Set Up Webhook Triggers from Azure Repos
Azure Repos supports Service Hooks to notify external systems on push events:
- In Azure DevOps: Project Settings > Service Hooks > Create Subscription.
- Choose Web Hooks as the service, and Code pushed as the trigger event.
- Set the URL to
https://your-jenkins-url/git/notifyCommit?url=<repo-url>or, if using the generic webhook trigger plugin, a custom endpoint that parses the Azure DevOps payload.
Alternatively, for more reliable integration, use the Azure Pipelines built-in ability to call Jenkins as an external build step — some teams run a minimal Azure Pipeline purely to trigger a Jenkins job via the Jenkins REST API:
# azure-pipelines.yml — triggers Jenkins remotely
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- script: |
curl -X POST "https://your-jenkins-url/job/myapp-pipeline/build" \
--user "$(JENKINS_USER):$(JENKINS_TOKEN)" \
--data-urlencode json='{"parameter": [{"name":"COMMIT_SHA","value":"$(Build.SourceVersion)"}]}'
displayName: 'Trigger Jenkins Pipeline'
Step 5: Write the Jenkinsfile
pipeline {
agent any
environment {
AZURE_RESOURCE_GROUP = 'myapp-rg'
AZURE_WEBAPP_NAME = 'myapp-webapp'
}
stages {
stage('Checkout') {
steps {
git branch: 'main',
credentialsId: 'azure-repos-pat',
url: 'https://dev.azure.com/myorg/myproject/_git/myapp'
}
}
stage('Install Dependencies') {
steps {
sh 'npm ci'
}
}
stage('Run Tests') {
steps {
sh 'npm test'
}
}
stage('Build') {
steps {
sh 'npm run build'
sh 'zip -r app.zip dist/'
}
}
stage('Azure Login') {
steps {
withCredentials([azureServicePrincipal('azure-deploy-sp')]) {
sh '''
az login --service-principal \
-u $AZURE_CLIENT_ID \
-p $AZURE_CLIENT_SECRET \
--tenant $AZURE_TENANT_ID
'''
}
}
}
stage('Deploy to Azure Web App') {
steps {
sh '''
az webapp deploy \
--resource-group $AZURE_RESOURCE_GROUP \
--name $AZURE_WEBAPP_NAME \
--src-path app.zip \
--type zip
'''
}
}
}
post {
success {
azureUpdatePullRequest(status: 'succeeded')
}
failure {
azureUpdatePullRequest(status: 'failed')
}
}
}
azureServicePrincipal (provided by the Azure Credentials Plugin) exposes AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, and AZURE_TENANT_ID as environment variables scoped to the block.
Step 6: Reporting Build Status Back to Azure DevOps
For pull requests in Azure Repos, use the Azure DevOps REST API to post a status check so reviewers see Jenkins results directly on the PR:
stage('Report Status to Azure DevOps') {
steps {
withCredentials([string(credentialsId: 'azure-devops-pat', variable: 'ADO_PAT')]) {
sh '''
curl -X POST \
"https://dev.azure.com/myorg/myproject/_apis/git/repositories/myapp/pullRequests/$PR_ID/statuses?api-version=7.0" \
-H "Content-Type: application/json" \
-u ":$ADO_PAT" \
-d '{
"state": "succeeded",
"description": "Jenkins build passed",
"context": {"name": "jenkins-ci", "genre": "continuous-integration"}
}'
'''
}
}
}
Deploying to Different Azure Targets
The same Jenkinsfile pattern extends to other Azure services with minor swaps:
- Azure Kubernetes Service (AKS): replace the
az webapp deploystep withaz aks get-credentialsfollowed bykubectl applyor a Helm upgrade. - Azure Functions: use
az functionapp deployment source config-zip. - Azure Container Instances: use
az container createwith an updated image tag.
Real-World Workflow
- A developer pushes to a feature branch in Azure Repos.
- A service hook (or lightweight Azure Pipeline trigger) notifies Jenkins, which runs tests and reports status back to the PR.
- On merge to
main, Jenkins builds, authenticates to Azure via Service Principal, and deploys to a staging Azure Web App slot. - A smoke test hits the staging slot; if healthy, Jenkins swaps the staging and production deployment slots using
az webapp deployment slot swap— giving near-zero-downtime cutover. - Work items linked to the commit in Azure Boards automatically transition state via Azure DevOps’ built-in commit-linking, independent of Jenkins.
stage('Swap Deployment Slots') {
steps {
input message: 'Swap staging into production?'
sh '''
az webapp deployment slot swap \
--resource-group $AZURE_RESOURCE_GROUP \
--name $AZURE_WEBAPP_NAME \
--slot staging \
--target-slot production
'''
}
}
Security Best Practices
- Use a Service Principal scoped to only the resource group(s) Jenkins needs, not subscription-wide Contributor access.
- Store the Service Principal secret and Azure DevOps PAT in Jenkins Credentials, and rotate both on a schedule.
- Restrict PAT scopes to the minimum needed (Code Read for triggers, Code Read & Write only if Jenkins needs to push tags or status).
- Use Azure Key Vault integration (via the Azure Key Vault Plugin) to centralize secret management instead of duplicating secrets across Jenkins and Azure DevOps.
Troubleshooting
- Webhook not triggering Jenkins builds: Check the Service Hook’s delivery history in Azure DevOps for HTTP errors, and confirm Jenkins’ URL is reachable from Azure’s servers (not just internally).
az loginfails with service principal: Double check the tenant ID matches the Azure AD tenant the Service Principal was created in, not a different tenant in a multi-tenant org.- PR status never updates: Confirm the PAT has “Pull Request Threads” or equivalent status-write scope, and that
$PR_IDis being populated correctly from the trigger payload.
Linking Jenkins Builds to Azure Boards Work Items
Traceability between a deployment and the work item it resolves is one of the more useful things this integration enables. If commit messages reference a work item ID (Azure Boards’ default convention is #123 or AB#123), Jenkins can parse that and update the work item’s state via the REST API once a deployment succeeds:
stage('Update Linked Work Items') {
steps {
withCredentials([string(credentialsId: 'azure-devops-pat', variable: 'ADO_PAT')]) {
script {
def commitMsg = sh(script: 'git log -1 --pretty=%B', returnStdout: true).trim()
def matcher = commitMsg =~ /AB#(\d+)/
if (matcher.find()) {
def workItemId = matcher.group(1)
sh """
curl -X PATCH \
"https://dev.azure.com/myorg/myproject/_apis/wit/workitems/${workItemId}?api-version=7.0" \
-H "Content-Type: application/json-patch+json" \
-u ":\$ADO_PAT" \
-d '[{"op":"add","path":"/fields/System.State","value":"Deployed"}]'
"""
}
}
}
}
}
This closes the loop between planning (Azure Boards) and delivery (Jenkins) without requiring the team to manually update ticket status after every release.
Handling Azure DevOps Variable Groups and Key Vault
If your team already stores shared configuration in Azure DevOps Variable Groups, Jenkins can pull those values via the REST API rather than duplicating configuration in two places:
stage('Fetch Variable Group') {
steps {
withCredentials([string(credentialsId: 'azure-devops-pat', variable: 'ADO_PAT')]) {
sh '''
curl -s -u ":$ADO_PAT" \
"https://dev.azure.com/myorg/myproject/_apis/distributedtask/variablegroups/1?api-version=7.0" \
| jq -r '.variables' > variables.json
'''
}
}
}
For secrets specifically, prefer Azure Key Vault directly over Variable Groups — the Azure Key Vault Plugin for Jenkins can fetch secrets at pipeline runtime, keeping a single source of truth shared across both Azure Pipelines and Jenkins without copy-pasting values between systems.
FAQs
Do I need Azure Pipelines at all if I’m using Jenkins? Not necessarily — some teams use Azure DevOps purely for Repos and Boards, keeping all pipeline logic in Jenkins. Others use a thin Azure Pipeline as a trigger relay, as shown above.
Can Jenkins read and update Azure Boards work items directly? Yes, via the Azure DevOps REST API using a PAT, though this typically requires custom pipeline scripting since there’s no dedicated plugin for work item updates as robust as the source control integration.
Is Azure Repos Git functionally different from GitHub for Jenkins purposes? No — it’s standard Git under the hood; the differences are in authentication (PAT vs GitHub token) and webhook/service hook configuration syntax.
What’s the advantage of deployment slots over a direct deploy? Slots let you deploy and warm up the new version before it receives production traffic, and the swap operation is nearly instant with an automatic rollback path (swap back) if something’s wrong.
Summary
Jenkins and Azure DevOps integrate cleanly once you set up the right authentication pieces: a Service Principal for Azure resource access, a PAT for Azure Repos/API interaction, and either a service hook or a thin Azure Pipeline trigger to kick off builds. From there, deploying to Azure Web Apps, AKS, or Functions follows the same checkout-test-build-deploy pattern as any other Jenkins pipeline.