The cost of copy-paste pipelines
In a young Azure DevOps organisation every repository gets its own azure-pipelines.yml, copied from the last one. A year later there are forty variants. One has the vulnerability scan, one does not. Three still use a deprecated task version. A bug fixed in one place is alive in the others. Nobody can answer the question “what do all our builds actually do?”
Templates are the fix. They are a core part of our Azure DevOps consulting, and they sit directly on top of a well-designed multi-stage pipeline.
What good looks like
- Common logic lives in one place and is consumed by reference.
- A template has a small, documented interface: parameters with sensible defaults.
- Templates are versioned, so a change is released rather than leaked.
- Security and compliance steps are enforced centrally, not requested politely.
- Teams still have room to extend, so the template is a platform and not a cage.
How it works in practice
Four kinds of reuse
Step templates share a sequence of tasks, such as restore, build and test for a language. Job templates share a whole job, including its pool and dependencies. Stage templates share a full stage, for example “deploy to an environment”. Variable templates share values. We start with the smallest unit that removes real duplication, because large templates are harder to adapt.
A step template with parameters, saved as templates/dotnet-build.yml:
parameters:
- name: project
type: string
- name: configuration
type: string
default: Release
steps:
- task: DotNetCoreCLI@2
displayName: Restore
inputs:
command: restore
projects: ${{ parameters.project }}
- task: DotNetCoreCLI@2
displayName: Build and test
inputs:
command: test
projects: ${{ parameters.project }}
arguments: --configuration ${{ parameters.configuration }}
A consuming pipeline pulls the shared repository in as a resource and pins a tag:
resources:
repositories:
- repository: platform
type: git
name: Platform/pipeline-templates
ref: refs/tags/v2.3.0
stages:
- stage: Build
jobs:
- job: Build
steps:
- template: templates/dotnet-build.yml@platform
parameters:
project: src/Api/Api.csproj
Because the reference pins v2.3.0, a new template release reaches a service only when its team bumps the tag. That is the safety property that makes sharing practical.
Guardrails with extends
An extends template takes control of the whole pipeline structure. The shared template defines the required stages, such as a security scan before any deployment, and lets the team inject their own steps through parameters. When combined with required template checks on environments and service connections, it becomes enforceable: a pipeline that does not extend the approved template cannot reach production resources. Without those checks it is only convention, so we configure both.
Converting task groups
Task groups often hide important behaviour behind a name. We open each, list its tasks and parameters, and rewrite it as a step template. The new template is reviewed, tested on one pipeline, and then adopted widely. This is also the moment to remove tasks that are deprecated or no longer needed.
Testing and documenting templates
Shared code deserves the same care as application code. Each template in the central repository has a small sample pipeline that exercises it, and a validation pipeline runs those samples on every pull request to the template repository. A broken template is then caught before a tag is cut, not after forty teams pull it. Azure Pipelines can also validate a YAML file without running it, which is a cheap first check.
Documentation matters as much as the code. Every template carries a short description of its purpose, a table of parameters with types and defaults, and one copy-paste example. We keep this beside the template, so it changes in the same pull request as the behaviour.
Handling variation between teams
Teams rarely differ in everything. Most variation is a language version, a project path, a test filter or a target environment. We model those as parameters with defaults. When a team needs something genuinely different, we prefer to add a clearly named optional step list parameter, so they can inject steps at defined points, rather than forking the template. Forks are how consistency dies quietly.
Governance without bureaucracy
A lightweight change process keeps the shared repository healthy: pull requests reviewed by at least one platform engineer, a changelog, semantic version tags, and a deprecation window for old versions. For most organisations this is an hour of effort per release and avoids days of firefighting.
Decisions and trade-offs
Central repository or per-team templates? A central repository gives consistency but needs an owner and a release process. For a small organisation, one folder in one repository is enough. For many teams, a dedicated repository with tags is worth it.
Flexibility or strictness? More parameters make templates flexible and harder to reason about. We expose what teams genuinely vary, and keep the rest fixed.
When not to template. If a pipeline is unique, used once, and unlikely to change, inline YAML is clearer. Templates earn their keep from the third repetition onward.
Variable templates and environment settings
Values that change by environment, such as resource group names or scale settings, belong in variable templates keyed by stage. This keeps the deploy template identical for dev and production while the inputs differ, and it makes a review of “what is different in production” a one-file diff.
Common failure modes
- Over-abstraction. Templates calling templates calling templates make debugging a treasure hunt.
- Unpinned references. Pointing at
mainmeans a template change can break every build at once. - No owner. Shared code without an owner stagnates. We name one and document the contribution path.
- Parameter explosion. When a template needs thirty parameters, it is probably two templates.
Delivery and handover
You receive the template repository, documentation with an example per template, a release process, and converted pipelines. Templates also make later work simpler, including a Jenkins migration, because converted jobs map onto known building blocks.
How to tell it is working
A change to a shared step reaches all services within days, new repositories get a working pipeline in minutes, audit questions about “what every build does” have a one-page answer, and the number of unique pipeline files shrinks. If you would like a review of your pipeline estate, get in touch.