DEV Community

Pavan Kumar Sadashiv
Pavan Kumar Sadashiv

Posted on

Reusable Pipelines Done Right: Building Typed GitLab CI/CD Components for the CI/CD Catalog

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         |
+-------------------------------------------------------------+
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 ]]/**
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)