Skip to content
SRE & DevOpsDeep Dive Published Updated 11 min readViews unavailable

GitLab CI/CD Components: Typed Inputs, Version Pins, and Safe Reuse

Build reusable GitLab CI components with validated inputs, collision-safe jobs, deliberate version pins, and tests that catch breaking pipeline changes.

Copying a working CI job from one repository to another is fast until the copies drift. One team patches an insecure default, another keeps a stale image, and a third changes a stage name without noticing that every consumer depends on it. GitLab CI/CD components provide a versioned unit of pipeline configuration that projects can include with explicit inputs. They are useful for standardizing repeated build, test, release, and policy jobs, but a shared YAML file is executable configuration, not a harmless snippet.

A production-quality component needs a clear contract: which behavior it owns, which values consumers may set, which pipeline names and stages it introduces, what permissions its code receives, how it is tested, and what version a consumer actually resolves. The goal is controlled reuse, not to hide an entire pipeline behind an opaque include.

Model a component as an API

A CI/CD component is a reusable pipeline configuration unit. A component project contains a top-level templates/ directory, a README describing the published components, and usually a license and a pipeline that tests and releases changes. A single component can be a file such as templates/security-scan.yml; a directory component uses a template.yml within its component directory. Supporting files inside a component directory are not automatically released as part of the component template, so do not design a consumer-facing job that assumes an arbitrary helper script will be available there.

The component reference includes the GitLab host, project path, component name, and version. Components are hosted on the same GitLab instance as the project that includes them. If a team wants a shared component for GitLab Self-Managed and GitLab.com projects, it needs a mirroring or publishing strategy that makes the source available on both instances.

Inputs form the component’s public configuration surface. Declare them in spec:inputs, document each one, and validate values where possible. The following example defines a generic scanner job. The image input is required and constrained to a digest-qualified reference from an example registry; replace the registry and policy with values your organization actually controls. The input does not grant or protect access to the image repository.

# templates/security-scan.yml
spec:
  inputs:
    job-prefix:
      description: "Unique prefix for the job created by this component"
      default: security
      regex: '^[a-z][a-z0-9-]{0,20}$'
    job-stage:
      description: "Stage that exists in the consuming pipeline"
      default: test
    scanner-image:
      description: "Approved scanner image, pinned by digest"
      regex: '^registry\.example\.com/ci/scanner@sha256:[0-9a-f]{64}$'
    severity-threshold:
      description: "Minimum finding severity that fails the job"
      default: high
      options: [critical, high, medium, low]
    report-format:
      description: "Machine-readable report format"
      default: json
      options: [json, sarif]
---

"$[[ inputs.job-prefix ]]-scan":
  stage: $[[ inputs.job-stage ]]
  image: "$[[ inputs.scanner-image ]]"
  script:
    - scanner scan --severity "$[[ inputs.severity-threshold ]]" --format "$[[ inputs.report-format ]]" --output report.json .
  artifacts:
    when: always
    paths:
      - report.json
    expire_in: 7 days

The --- separates the input specification from the job document. Input expressions are resolved while GitLab creates the pipeline configuration, before a runner starts the job. They are not shell variables and cannot inspect runtime job state. The required image input has no default, so a consumer must provide it; this is intentional when silently choosing a mutable scanner image would violate an organization’s supply-chain policy. The options list makes the accepted threshold and report formats explicit, while the regular expressions constrain the job prefix and image reference shape.

The component does not promise that test exists in every consuming pipeline. A consumer with custom stages must pass a stage it has declared. A component should not add or replace global stages, default, or other global keywords because merged configuration can affect unrelated jobs in the consuming pipeline. Prefer a job-specific configuration and unique names. In the example, a caller can choose another prefix if it needs two independent scanner jobs in one pipeline.

Choose inputs and variables for different jobs

Inputs and CI/CD variables are related but not interchangeable. Inputs are declared in configuration, validated at pipeline creation, interpolated into the configuration, and remain fixed for that pipeline run. They are a good fit for a reusable component’s options: stage, feature selection, job prefix, output mode, or a required deployment region.

Variables are environment values that can be available to job scripts and can be changed or generated during execution. Use CI/CD variables, protected settings, or an external secrets mechanism for credentials and sensitive runtime values. Do not pass a password as a component input merely because inputs are convenient: configuration may be displayed, interpolated, or logged differently from a protected runtime secret. Keep secret values out of generated job names, image references, command arguments, and reports.

An input declared in one configuration file is scoped to that file. If a component includes another configuration file, pass values explicitly through include:inputs; do not assume that an input is globally visible. Give inputs useful descriptions, safe defaults only where they are genuinely safe, and narrow options or regex validation for values that should not be arbitrary. Avoid exposing dozens of low-level implementation settings. Too many switches make the component hard to test and let each consumer create a different, poorly understood variant.

Consume an explicit component version

Here is a consumer configuration. The example uses an exact semantic version as a readable release contract; the organization publishing it should protect release tags and have a process that prevents a tag from being moved after publication. For especially strict reproducibility, a consumer may use a commit SHA. A branch name or ~latest deliberately follows a moving target and should not be treated as a fixed production dependency.

include:
  - component: "$CI_SERVER_FQDN/platform/ci-components/[email protected]"
    inputs:
      job-prefix: app
      job-stage: verify
      scanner-image: registry.example.com/ci/scanner@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
      severity-threshold: high
      report-format: json

stages:
  - verify
  - test
  - deploy

The digest is an illustrative placeholder, not a real scanner image. Replace it with a digest that the organization has verified and can retrieve. Likewise, ensure that the verify stage exists in the real pipeline. The include merges component configuration with the project’s configuration; it does not create an isolated sub-pipeline. If both sides define the same job name, GitLab can merge those definitions. A naming collision or an unexpected extends target can therefore alter behavior without producing an obvious error. Give component jobs distinctive names and test the fully merged pipeline, not just the template file.

GitLab supports references such as commit SHA, tag, branch, and catalog version selectors. Exact catalog versions make upgrades an explicit change request. Partial versions such as 1.4 intentionally select the latest compatible minor or patch release from the catalog, while ~latest follows the latest published version and can move across major versions. Use partial selectors only when the team’s upgrade policy explicitly wants that behavior. A semantic version label is useful only if the publishing process maintains the expected compatibility rules and release tags cannot be silently changed.

Keep dependencies inside a component project small. If a component calls other components, pin those dependencies to released versions or commit SHAs, then update them deliberately and publish a new version of the parent component. A dependency on ~latest can change the behavior of an unchanged parent release. This can make incident reproduction and rollback difficult because the text of the consumer pipeline no longer identifies the full configuration that ran.

Avoid hidden pipeline-wide side effects

Components are merged into the consumer’s configuration. That has practical consequences beyond duplicate job names:

  • Do not use global keywords to change the image, cache, retry policy, or stages of jobs outside the component.
  • Use job-specific values and distinctive hidden-template names if extends is needed.
  • Avoid assuming a variable or stage exists merely because it exists in the component’s test project.
  • Document artifacts, dotenv reports, caches, services, and resource groups that the component creates.
  • Do not make a supposedly optional component silently alter deployment behavior through broad workflow or job rules.
  • Treat component scripts and included images as code that runs with the consumer job’s available credentials.

For repeated use, add an input for the job prefix or full job name rather than asking consumers to rename jobs after inclusion. If two instances need different behavior, include the component twice with different names and values and verify that the merged job graph contains both intended jobs. If a component’s changes would alter a global setting or require a fundamentally different version lifecycle, it may be too large or coupled for one reusable component.

Test the component before publishing it

Test changes in the component project by including the component at the commit being tested, not by testing only the last published version. A project can use a reference to its own current commit SHA during the test pipeline. Add representative sample files where the component needs real input, then verify its behavior and its side effects in the merged pipeline.

A useful test suite covers more than YAML syntax:

  1. A default-input case proves that documented defaults produce the expected job.
  2. A custom-input case exercises a non-default stage, prefix, and valid scanner image.
  3. Invalid input cases confirm that unsupported stages, malformed image references, or unapproved enum values stop pipeline creation rather than reaching a runner.
  4. A composition case includes the component alongside ordinary project jobs and another component, then checks for duplicate names, accidental merges, and unexpected dependencies.
  5. A permission test runs with the narrowest job token and credentials the component is supposed to need.
  6. A failure case confirms that scanner exit status and report behavior match the documented policy.
  7. A consumer fixture with a custom stage list verifies that the component does not assume stages that are absent.

Use GitLab’s CI configuration validation and pipeline editor to inspect the expanded configuration for the target GitLab version. Also execute the component’s test pipeline against a disposable project or representative sample project. Configuration validation can identify syntax and some composition failures; it cannot prove that a command scans the intended files, that a credential is least-privilege, or that a report is retained for the correct duration.

Publish, upgrade, and retire versions deliberately

Document each component in the project README: its purpose, inputs, outputs, permissions, supported stages, compatibility expectations, and a minimal include example. Keep release notes specific about changed defaults, input removals, job-name changes, image updates, and permission changes. Those are API changes even if the component’s YAML file still parses.

Before releasing a new version, test the candidate from its own commit, review its dependency versions, and ensure the release pipeline creates the intended tag only after tests pass. Use semantic-version increments according to the compatibility impact. Do not assume a version number alone enforces immutability; use repository controls to protect release tags and restrict who can publish. If a component exposes a vulnerable or unsafe behavior, publish a corrected version and communicate which consumers need to update. Where the organization has usage telemetry, use it to plan a deprecation window instead of deleting a version that active pipelines still require.

When consuming a public component, inspect its source and release process rather than treating catalog presence as an endorsement. A component executes in the context of your pipeline and may be able to read variables, job tokens, workspace files, or artifacts available to its job. Grant only the permissions it needs, use protected or short-lived credentials for sensitive operations, and avoid passing secrets to untrusted merge-request pipelines. Pin versions so a component update is a visible, reviewable change.

Production review checklist

  • Does every input have a documented purpose, a safe default or an explicit required status, and validation where possible?
  • Are runtime credentials kept in protected variables or a secret provider rather than configuration inputs?
  • Are job names and hidden templates unique enough to avoid accidental merges with consumer configuration?
  • Does the component avoid changing global configuration for unrelated jobs?
  • Are component and transitive dependency versions pinned according to a documented upgrade policy?
  • Are release tags protected and are breaking changes reflected in release notes and versioning?
  • Has the component been tested at its candidate commit with valid, invalid, and composition cases?
  • Have its source, runner image, required permissions, artifacts, and credential handling been reviewed?
  • Can operators identify the exact component version and commit that contributed to a pipeline run?

Reusable CI configuration is production infrastructure because it changes what runs, with which inputs, and under which identity. A well-designed GitLab component is deliberately small, validates its public interface before execution, avoids surprising changes outside its own jobs, and is consumed at a version that reviewers can reproduce.

Related:

Sources:

Comments