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:

  1. 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.
  2. Global Scope Pollution: Included templates frequently overwrote global before_script or variables blocks in the consuming pipeline.
  3. Zero Versioning Safety: Pushing a change to the main branch 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:

  1. Dynamic Job Names: You can include the same component three times in one pipeline with different job_prefix inputs (rust-linux-compile, rust-arm-compile, rust-wasm-compile) without job name collisions!
  2. 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:

  1. Go to Settings $\rightarrow$ General $\rightarrow$ Expand Visibility, project features, permissions.
  2. 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

  1. Treat CI/CD Like Software: Version your pipeline logic with Semantic Versioning (@1.0.0), write README documentation, and test components before releasing.
  2. Enforce Contracts with spec:inputs: Eliminate undocumented environment variable dependencies.
  3. Avoid Copy-Paste YAML: Consolidate your organization's Docker builds, linters, and deployment jobs into modular Catalog components.

Resources & Links

Have you migrated your legacy YAML templates to GitLab CI/CD Components yet? Let's discuss in the comments below!