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!

