back to work

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.

CI/CDGitLab CIGoReact/Next.jsDeveloper Experience

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 → deploy shape 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.