Reusable GitLab CI Templates: 15 → 4 Minute Pipelines
Replacing copy-pasted pipelines with versioned, reusable GitLab CI templates — parallelized and layer-cached — cutting pipeline time ~60% (15 → 4 min) and standardizing builds across repos.
Problem
Across the org, every team maintained its own .gitlab-ci.yml. They were near-duplicates that
drifted over time: inconsistent stages, ad-hoc (or missing) caching, and no shared quality
gates. Builds were slow, and fixing one bug meant editing it in many places.
Whole-pipeline runs took around 15 minutes — long enough that people batched pushes and context-switched while waiting.
Constraint
- No secrets in pipeline definitions — sensitive values come from masked variables or a secret store.
- A
validate/lint stage must run before any build or deploy. - Templates must serve two different ecosystems (Go and React/Next.js) without forking.
- Adoption had to be incremental — repos opt in one at a time, no flag day.
Architecture
I built a central template repository exposing reusable jobs via include. Each consumer repo
includes the relevant template and overrides only what's repo-specific. Templates are versioned,
so a repo pins a version and upgrades deliberately. The speed came from two levers baked into the
templates: stage parallelization and Docker layer caching.
Standard stages enforced by the templates:
- validate — lint and static checks first; fail fast on the cheapest stage.
- build / test — parallelized jobs with restored dependency cache.
- package — Docker build with layer caching; pinned, reproducible images.
- deploy — gated and environment-aware, secrets injected at runtime.
Decision / Trade-off
- Versioned templates over a single shared "latest". Pinning protects consumers from surprise breakage; the cost is that upgrades are a deliberate per-repo action.
- Convention over configuration. Templates assume a standard layout — repos that fit get fast wins; non-standard ones need small adapters. An acceptable trade for consistency.
- Caching as a first-class concern. Correct, aggressive layer + dependency caching was the single biggest lever on pipeline time.
Result
- Pipeline time cut ~60% — 15 → 4 minutes via parallelization and Docker layer caching.
- A single template change now propagates to every adopting repo on upgrade, instead of dozens of manual edits.
- Consistent
validate → build → package → deployshape across Go and React/Next.js. - Adopted across many repositories in a multi-team environment.
What I'd watch next: publish a changelog + migration notes per template version, and keep a canary repo on the newest template to catch regressions before everyone upgrades.