Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for GitLab CI/CD By James Joyner IV · · 11 min read

Publishing Versioned GitLab CI/CD Catalog Components Your Teams Will Actually Use

Stop copy-pasting pipeline YAML between projects. Here is how I build, version, and publish reusable GitLab CI/CD Catalog components, with AI on boilerplate.

  • #gitlab
  • #ci-cd
  • #components
  • #ai

I spent two years watching the same forty lines of Docker-build YAML get copy-pasted into every new repo at my company. Each copy drifted. One had a cache key that worked, one didn’t, and nobody could remember which was canonical. The GitLab CI/CD Catalog fixed that for us — but only after I learned that consuming a component is easy and publishing a good one is the actual skill. Here is the whole workflow, including where I lean on AI and where I refuse to.

Why a component, not just an include

You can already share YAML with include: project:. A component goes further: it lives in a dedicated project marked as a catalog resource, it is versioned by Git tag, and it declares typed inputs. That last part is the difference between a snippet and a product. Consumers get autocomplete, validation, and a stable contract.

The mental model: a component is a parameterized function. The repo is the package. The Git tag is the version. The catalog is the registry.

Anatomy of a component repository

A catalog project needs a README.md, a description in the project settings, and components under templates/. Each component is either templates/<name>.yml or templates/<name>/template.yml for the directory form.

Here is a Docker build component with typed inputs:

# templates/docker-build/template.yml
spec:
  inputs:
    image:
      description: "Fully qualified image name to build and push."
    context:
      default: "."
      description: "Docker build context path."
    dockerfile:
      default: "Dockerfile"
    push:
      type: boolean
      default: true
---
"build-$[[ inputs.image ]]":
  stage: build
  image: docker:27
  services:
    - docker:27-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build -t "$[[ inputs.image ]]:$CI_COMMIT_SHORT_SHA" -f "$[[ inputs.dockerfile ]]" "$[[ inputs.context ]]"
    - if [ "$[[ inputs.push ]]" = "true" ]; then docker push "$[[ inputs.image ]]:$CI_COMMIT_SHORT_SHA"; fi

The spec: block followed by --- is the component header. The $[[ inputs.x ]] interpolation happens before the YAML is parsed, which is why you can even build job names dynamically.

Pro Tip: Always give every input a description. The catalog UI renders them, and your future consumers — who are humans skimming, not reading — will thank you. AI is genuinely good at writing these descriptions; ask it to “write a one-line description for each input that explains the consequence of changing it.”

Consuming the component

On the other side, a consumer pins to a version:

include:
  - component: $CI_SERVER_FQDN/platform/ci-components/docker-build@1.2.0
    inputs:
      image: registry.example.com/team/api
      dockerfile: build/Dockerfile

Note the @1.2.0. Never tell people to use @main. A floating ref means every consumer’s pipeline can break the moment you push. Tags are your release surface.

Versioning and releasing properly

A component version only exists in the catalog once you create a release for the tag — a plain tag is not enough. I automate this in the component repo’s own pipeline:

stages: [test, release]

include:
  - component: $CI_SERVER_FQDN/components/opentofu/validate@1.0.0

create-release:
  stage: release
  image: registry.gitlab.com/gitlab-org/release-cli:latest
  rules:
    - if: $CI_COMMIT_TAG
  script:
    - echo "Releasing component version $CI_COMMIT_TAG"
  release:
    tag_name: $CI_COMMIT_TAG
    description: "Release $CI_COMMIT_TAG"

Adopt semantic versioning seriously. Adding an optional input is a minor bump. Changing a default, renaming an input, or changing a job name is a breaking change and deserves a major bump, because consumers reference job names in needs:.

Testing components before you tag

The mistake I made early was tagging first and discovering the bug in a consumer repo. Now I test the component inside its own pipeline by including it locally:

test-component:
  stage: test
  trigger:
    include:
      - local: templates/docker-build/template.yml
        inputs:
          image: registry.example.com/test/throwaway
          push: false
    strategy: depend

This spins up a child pipeline using the component exactly as a consumer would, but against the working tree instead of a tag. If it fails, you never publish.

Where AI helps and where it must not

I treat AI here like a fast junior engineer who has read every GitLab doc but has never been paged at 3am. It is excellent at:

  • Scaffolding the spec.inputs block from a plain-English description of parameters.
  • Converting an old copy-pasted job into a parameterized component.
  • Writing the README usage table and input descriptions.
  • Suggesting which fields should be inputs versus hard-coded.

It is unreliable at:

  • Knowing whether $[[ inputs.x ]] (interpolation) or $x (runtime variable) is correct in a given spot. It mixes these constantly.
  • Remembering that a release-cli job is required for catalog publication.
  • Judging what counts as a breaking change for your consumers.

So I draft with AI, then I read every line before merge. I never paste the component repo’s deploy token or registry credentials into a chat to “help it debug.” A component is infrastructure that dozens of pipelines will trust; hand-verify it. If you want a structured second pass on the diff, our code review dashboard is built for exactly this kind of YAML-heavy change.

The prompt I reuse most: “Here is a job we copy-paste across repos. Convert it to a GitLab CI/CD Catalog component with typed inputs and sensible defaults. Use $[[ inputs.x ]] for inputs and keep $CI_* predefined variables as-is. Explain any field you’re unsure about.” That last sentence surfaces the spots I need to check.

Inputs, defaults, and the contract you owe consumers

The hardest design decisions in a component are about its inputs, not its jobs. Every input is a promise: change its meaning later and you break someone. I follow three rules. First, make the common case the default, so a consumer can include the component with zero inputs and get something sensible. Second, prefer typed inputs (type: boolean, type: number, type: array) over stringly-typed ones, because GitLab validates types at pipeline creation and catches mistakes before a runner ever spins up. Third, resist the urge to expose everything — each input is forever, and a component with thirty knobs is one nobody can use confidently.

spec:
  inputs:
    stage:
      default: "test"
      description: "Pipeline stage this job runs in."
    coverage_threshold:
      type: number
      default: 80
      description: "Fail the job if coverage falls below this percentage."
    languages:
      type: array
      default: ["python"]
      description: "Languages to scan."

A typed coverage_threshold means a consumer who passes "eighty" gets a clear validation error, not a job that silently treats the string as zero. That early failure is a gift. AI drafts these spec blocks well, but it tends to over-expose inputs — I prune its output down to what consumers genuinely need to vary.

Rolling it out across teams

Publishing is half the job; adoption is the other half. What worked for us:

  1. Put all components in one well-named platform project so the catalog page is a single browseable list.
  2. Pin a @major convention internally and document it once, prominently.
  3. Add a deprecation note to the README and an echo warning in the deprecated component’s first job, so consumers see it in logs.
  4. Keep a CHANGELOG. The catalog shows the release description, but a real changelog explains why.

I keep my component-generation prompts in a shared library so the team produces consistent specs — you can browse starter prompts in our prompt collection, and the curated prompt packs include a platform-engineering set.

Conclusion

The GitLab CI/CD Catalog turns tribal pipeline knowledge into a versioned, discoverable contract. The components themselves are mostly boilerplate, which is exactly the kind of work AI accelerates — let it draft the spec, the interpolation, and the docs. Then do the part it cannot: decide the contract, verify the interpolation, version honestly, and never let it near your tokens. More gitlab-cicd guides live in the GitLab CI/CD category.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.