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.inputsblock 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:
- Put all components in one well-named platform project so the catalog page is a single browseable list.
- Pin a
@majorconvention internally and document it once, prominently. - Add a deprecation note to the README and an
echowarning in the deprecated component’s first job, so consumers see it in logs. - 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.
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.