Reusable Pipelines Done Right: Building Typed GitLab CI/CD Components for the CI/CD Catalog
For years, sharing CI/CD logic across multiple repositories in GitLab meant using include:project or include:remote to pull raw YAML files from a central templates repository.
While include: worked for simple tasks, it suffered from three major architectural flaws at scale:
-
No Input Contracts: Passing parameters relied on global environment variables (
variables: DEPLOY_ENV: "prod"), making it impossible to know which variables a template required without reading its source code. -
Global Scope Pollution: Included templates frequently overwrote global
before_scriptorvariablesblocks in the consuming pipeline. -
Zero Versioning Safety: Pushing a change to the
mainbranch of a central template repository could instantly break 200 downstream pipelines.
To solve this permanently, GitLab introduced CI/CD Components and the GitLab CI/CD Catalog (gitlab.com/explore/catalog).
In this guide, I will walk you through building, testing, and publishing a strongly-typed, semantically versioned GitLab CI/CD Component from scratch.
1. Legacy include: vs. Modern CI/CD Components
+-------------------------------------------------------------+
| LEGACY YAML INCLUDE |
| |
| - Parameters passed via untyped global ENV variables |
| - No default value validation |
| - Unpinned master branch breaks downstream pipelines |
+-------------------------------------------------------------+
vs.
+-------------------------------------------------------------+
| GITLAB CI/CD COMPONENT |
| |
| - Explicit `spec: inputs:` contract (string, boolean, etc) |
| - Evaluated at YAML parse time ($[[ inputs.parameter ]]) |
| - Pinned to immutable SemVer tags (@1.2.0) |
| - Discoverable in the public/private CI/CD Catalog |
+-------------------------------------------------------------+
2. Repository Structure of a CI/CD Component Project
A single component repository can host multiple reusable components inside a templates/ directory:
my-devops-components/
├── README.md # Mandatory documentation shown in the Catalog UI
├── .gitlab-ci.yml # Self-testing & release pipeline for the component
└── templates/
├── rust-build.yml # Component 1: templates/rust-build
└── security-audit/ # Component 2 (Complex): templates/security-audit
├── template.yml
└── scripts/
└── run-scan.sh
3. Writing a Strongly-Typed Component (templates/rust-build.yml)
Every CI/CD Component begins with a spec: header separated from the job definitions by ---.
spec:
inputs:
job_prefix:
type: string
default: "rust"
description: "Prefix applied to generated CI job names"
rust_version:
type: string
default: "1.82-slim"
description: "Official Rust Docker image tag"
release_mode:
type: boolean
default: true
description: "Compile with --release optimizations enabled"
target_arch:
type: string
default: "x86_64-unknown-linux-gnu"
options:
- "x86_64-unknown-linux-gnu"
- "aarch64-unknown-linux-gnu"
- "wasm32-unknown-unknown"
description: "Target architecture triple for compilation"
---
"$[[ inputs.job_prefix ]]-compile":
stage: build
image: "rust:$[[ inputs.rust_version ]]"
script:
- rustup target add $[[ inputs.target_arch ]]
- |
if [ "$[[ inputs.release_mode ]]" = "true" ]; then
cargo build --release --target $[[ inputs.target_arch ]]
else
cargo build --target $[[ inputs.target_arch ]]
fi
artifacts:
paths:
- target/$[[ inputs.target_arch ]]/**
Why $[[ inputs.* ]] Is a Game Changer:
Unlike runtime shell variables ($VAR), $[[ inputs.* ]] interpolation happens before the pipeline runs, during YAML compilation:
-
Dynamic Job Names: You can include the same component three times in one pipeline with different
job_prefixinputs (rust-linux-compile,rust-arm-compile,rust-wasm-compile) without job name collisions! -
Strict Enum Validation: If a developer passes
target_arch: "invalid-arch", GitLab rejects the pipeline immediately with a clear syntax error before wasting a single second of runner compute.
4. Testing & Releasing Your Component to the CI/CD Catalog
To list your project in the GitLab CI/CD Catalog:
- Go to Settings $\rightarrow$ General $\rightarrow$ Expand Visibility, project features, permissions.
- Toggle on CI/CD Catalog project.
Next, configure the component repository’s own .gitlab-ci.yml to test the component and publish semantic version releases automatically:
stages:
- validate
- release
# Include our own component on the current commit to verify it works!
include:
- component: $CI_SERVER_FQDN/$CI_PROJECT_PATH/rust-build@$CI_COMMIT_SHA
inputs:
job_prefix: "test-self"
release_mode: false
# Publish to the CI/CD Catalog when a SemVer tag is pushed (e.g., 1.0.0)
publish_catalog_version:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
rules:
- if: $CI_COMMIT_TAG =~ /^\d+\.\d+\.\d+$/
script:
- echo "Releasing component version $CI_COMMIT_TAG to GitLab CI/CD Catalog..."
release:
tag_name: $CI_COMMIT_TAG
description: "Catalog release $CI_COMMIT_TAG"
5. Consuming the Component in Downstream Projects
Once tagged (git tag 1.0.0 && git push origin 1.0.0), any repository can consume your component with clean, self-documenting syntax:
stages:
- build
include:
# Build for x86_64 Linux
- component: gitlab.com/my-org/my-devops-components/rust-build@1.0.0
inputs:
job_prefix: "linux-x64"
target_arch: "x86_64-unknown-linux-gnu"
# Build for ARM64 in parallel using the exact same component!
- component: gitlab.com/my-org/my-devops-components/rust-build@1.0.0
inputs:
job_prefix: "linux-arm64"
target_arch: "aarch64-unknown-linux-gnu"
6. Key Takeaways
-
Treat CI/CD Like Software: Version your pipeline logic with Semantic Versioning (
@1.0.0), write README documentation, and test components before releasing. -
Enforce Contracts with
spec:inputs: Eliminate undocumented environment variable dependencies. - Avoid Copy-Paste YAML: Consolidate your organization's Docker builds, linters, and deployment jobs into modular Catalog components.
Resources & Links
- GitLab CI/CD Catalog: gitlab.com/explore/catalog
- GitLab Profile: gitlab.com/hrlpavan
- GitLab Contributor Profile: contributors.gitlab.com/users/hrlpavan
- GitHub: @hrlpavan
Have you migrated your legacy YAML templates to GitLab CI/CD Components yet? Let's discuss in the comments below!
Top comments (0)