Why teams leave Jenkins, and why migrations stall
The reasons are consistent. A Jenkins server that nobody wants to upgrade, plugins with security advisories, pipelines written in Groovy by someone who has left, and a controller that is a single point of failure. Teams that already use Azure Boards, Repos or Artifacts also want one system for planning, code and delivery.
Migrations stall for a different reason: they are treated as translation. Someone starts converting the largest Jenkinsfile line by line, hits a plugin with no equivalent and the project loses momentum. This page describes the method we use as part of our Azure DevOps consulting to avoid that. If you are leaving Jenkins for a different target, our guide to migrating Jenkins to GitHub Actions covers that route, and the wider picture is in our CI/CD pipeline service.
What good looks like
- Every job in scope is classified: convert, redesign or retire.
- New pipelines are built from shared templates, not copied YAML.
- No credential is copied across unchanged; each is reviewed, rotated or removed.
- Old and new pipelines run side by side until results match.
- Developers know what changed for them and where to look.
- The old server has a decommission date and it is met.
How it works in practice
Inventory before anything else
We export the job list, the Jenkinsfiles and shared library code, the installed plugins, the credentials store and the agent definitions. Then we ask for each job: when did it last run, who owns it, and what depends on its output? In most estates a meaningful share of jobs have not run for months. Retiring them is the cheapest migration work there is.
Mapping concepts
| Jenkins | Azure Pipelines |
|---|---|
| Jenkinsfile (declarative) | azure-pipelines.yml |
| Stages and steps | Stages, jobs and steps |
| Shared library | Template repository |
| Credentials store | Service connections and Key Vault |
| Agents and labels | Agent pools and demands |
| Post actions | Conditions such as succeeded() and always() |
| Multibranch pipeline | Branch triggers and pull request validation |
The mapping is a guide, not a recipe. A Jenkins shared library with many functions is usually better expressed as pipeline templates with parameters.
A before and after
A simple declarative stage:
stage('Test') {
steps { sh 'npm ci && npm test' }
post { always { junit 'reports/*.xml' } }
}
becomes, in the target design:
- job: Test
steps:
- script: npm ci && npm test
displayName: Test
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: JUnit
testResultsFiles: "reports/*.xml"
The translation is easy here. The real work is in everything around it: caching, artefact handling, deployment and credentials. See multi-stage YAML design for the structure we aim for.
Agents and credentials
Jenkins agents often have tools installed by hand. We replace them with Microsoft-hosted agents where possible, and with self-hosted agent pools where network access or tooling demands it. Credentials move into Key Vault and service connections rather than being copied. This is the best moment to find and revoke the ones nobody recognises.
Coming from GitHub Actions
Workflows map to pipelines, jobs to jobs, reusable workflows to templates, and secrets to variable groups or Key Vault. Marketplace actions need an equivalent task, extension or script, and hosted runner labels become pool selections. The decision to move should rest on a real need, such as consolidating on Azure DevOps. If the repositories stay on GitHub, Azure Pipelines can still build them through a GitHub service connection.
Parallel running
For each wave, the new pipeline runs alongside the old one. We compare produced artefacts, test results, deployment outcomes and timings. Only when they agree for an agreed number of runs do we disable the Jenkins trigger. Deployments should be made by one system only at a time, to avoid both pushing to the same target.
Sequencing the waves
We order services by risk rather than by team. The first wave is internal tools and libraries whose failure affects few people. The second is customer-facing services with simple deployments. The last is the release pipelines that touch production databases or have strict change controls, because by then the templates, agents and credential patterns have been proven. Each wave ends with a short review: what broke, which template needed a parameter, which plugin had no equivalent. The findings feed the next wave, so effort per service falls as the migration proceeds.
Decisions and trade-offs
Redesign or lift and shift? Lift and shift is faster for a handful of stable jobs. For a large estate, templates pay back quickly because later waves reuse them.
Big bang or waves? Waves cost more calendar time but keep risk low. Big bang only suits small estates with few release pipelines.
Hosted or self-hosted agents? Hosted is simpler to run. Self-hosted gives network reach and custom tooling.
When not to migrate. If Jenkins is stable, patched and well understood, and nothing about Azure DevOps solves a problem you have, the cost may not be justified. Migration should buy something specific.
Common failure modes
- Converting dead jobs. Effort spent on pipelines nobody uses.
- Hidden plugin dependencies. A job quietly relies on a plugin side effect.
- Credential copying. Old secrets carried over unchanged.
- Double deployment. Both systems deploy during overlap.
- No decommission date. Jenkins lives on beside Azure DevOps for years.
Delivery and handover
You receive the inventory and classification, a template library, converted pipelines with owners, a parallel-run comparison log, a cutover checklist and a decommission record showing credentials revoked. Developer notes explain the differences in daily workflow. To scope your migration, contact us.
How to tell it is working
Build and deploy success rates match or beat the old system, the Jenkins server is switched off on schedule, new services start from templates rather than copied files, and nobody asks where to find a build log.