The first pipeline in an organisation is born in one repository. The second project copies the file and changes the image name. The third copies from the second and adds a step. A year later there are twenty files with a common ancestor, and a security fix in the build step needs twenty changes, several of which will never be made.
The rule: logic in the template, differences in variables
A project’s pipeline should be a dozen lines: an include of the template from a central repository and a few variables. Everything that is the same for every project (how the image is built, publishing to the registry, scanning, updating manifests) lives in one place.
include:
- project: platform/ci-templates
ref: v2
file: /templates/container-app.yml
variables:
APP_NAME: orders-api
APP_PATH: services/orders
TEST_COMMAND: "npm test"
The project defines no jobs of its own. If it needs something the template does not provide, that is a signal to extend the template, not to override a job locally.
Template structure
Stages are fixed and always in the same order: lint and style checks, tests, build, publish, update of deployment manifests. A project can disable a stage with a variable (for example SKIP_TESTS: "true" with a justification in a comment), but it cannot change its content.
Extension points are explicit: a variable with the test command, an optional script run before the build, a list of extra paths to scan. Every extension point is documented in the variables table in the template’s README. An extension point not in the table does not exist.
Versioning
The template has versions marked with tags. Projects reference a major version (v2), which is a moving pointer to the latest backwards-compatible release. A breaking change means a new major version (v3) and projects migrate at their own pace. Referencing the template’s main branch is forbidden: a change in the template must not unexpectedly break every pipeline in the organisation.
The template change process
- A change on a template branch, with a description of what and why.
- A run on the reference project: a simple application that exists only to test the template and the whole path to deployment.
- A run on two or three real projects designated as pilots.
- A new tag and moving the major version pointer.
- An entry in the template’s changelog.
The reference project is the most important element: without it, every template change is tested on somebody else’s production.
Drift detection
A scheduled job checks every repository: does it include the template, from which version, does it define its own jobs. The result becomes a list: projects on an outdated version, projects with local overrides. The list is short if the rules are enforced from the start and very long if they are introduced a year later.
What stays in the project
The things that really differ between projects: the application name, the path in a monorepo, the test command, the runtime version, the list of target environments. All of these are values, not logic. If script: appears in a project’s file, something has gone wrong.
Summary
One template instead of twenty copies means one fix, one audit, and a working pipeline for a new project in fifteen minutes. The conditions: logic only in the template, explicit extension points, tagged versions, a reference project for testing, and a job that detects deviations. A template that cannot be extended without copying will be copied.