Developer Experience
What Makes CI/CD Templates Actually Useful
What I learned moving our Azure DevOps pipelines to YAML and into one shared, versioned template repository.
- CI/CD
- DevEx
- Azure DevOps
Most pipelines start as a copy of another pipeline. That works until there are enough copies that nobody knows which one is right.
Our Azure DevOps setup got there. Many pipelines still lived in the classic UI editor, where a change can't be reviewed and has no history next to the code. Teams kept rebuilding the same build, test, packaging and deploy steps, and the copies drifted apart. I moved the pipelines to YAML, then pulled the repeated steps into one shared template repository. Here is what made teams willing to depend on it.
Only share what already repeats
I didn't start by designing a template library. I started with what the pipelines were already doing. Building, running tests, packaging and deploying showed up in nearly all of them, each with small differences nobody had chosen on purpose.
If only one project needs a step, it stays in that project's pipeline. A template with one user only gives people one more file to open.
Treat it like an API
Once other pipelines depend on a template, its parameters are a public interface. Rename one and you break someone's build.
So I keep the parameter list short, give each one a default that works for the common case, and name them after what they mean to the person using the template. A team should be able to read its own pipeline and know what it does without opening the template repository. Using one looks something like this:
resources:
repositories:
- repository: templates
type: git
name: Shared/pipeline-templates
ref: refs/tags/v2.3.0
steps:
- template: dotnet/build-and-test.yml@templates
parameters:
solution: src/App.sln
If a template needs twenty parameters, it isn't saving anyone much. If it hides every step behind one vague name, nobody can tell what broke when it fails.
Version it with tags
This is the part that made it safe. Pipelines don't point at the template repository's main branch. They point at a semantic version tag:
- A patch release fixes something without changing behavior.
- A minor release adds something optional.
- A major release changes the contract.
Each team decides when to move up. I can keep improving the templates, and nothing I merge can break another team's release build that afternoon. A breaking change goes into a new major version, and the old tag keeps working for anyone who isn't ready.
It takes some discipline to decide which kind of change you're making. That is still far cheaper than every team maintaining its own copy of the same YAML.
Leave a few doors open
Every project is a little different: paths, test projects, deploy targets. Templates need room for that.
The trap is letting every step be overridden. Then the template keeps nothing consistent and just moves the confusion into one file. I allow the variations that are real, like paths, optional stages and environment values. The order of the steps and the safety checks stay the same for everyone.
Make failures easy to read
A template that saves ten minutes of setup and costs an hour every time a build breaks is a bad trade.
Step names should say exactly what they do, so the failing one is obvious in the log. Failures should happen close to their cause. Artifact names and variables should be predictable enough that you can work out what happened without reading the template's source.
Write down how to use it
Short docs, kept next to the templates: a minimal example, a fuller one, what each parameter does, and what changed in each major version. Most of what stops people from adopting shared tooling is not knowing what they're signing up for, and a page of notes answers most of that.
Pipelines are now defined the same way across teams, complex ones take less time to set up, and a fix to a shared step is written once. They went from settings in a UI to code we review and version like everything else.