← Standards

pipeline

The estate runs CI through one shared catalog: gitlab.com/dunn.dev/pipeline. Every project's .gitlab-ci.yml is a short list of includes from that catalog at one pinned version. The catalog owns the shape (job definitions, runner image, signing pipeline, release machinery); the consumer owns the inputs (binary name, output dir, secrets).

What this standard checks

Generated from ./check.sh, in the order the check runs them.

assertion target pattern or value note
file exists .gitlab-ci.yml a repo with no pipeline file runs none of the estate's gates
file matches .gitlab-ci.yml ^[^#]*gitlab\.com/dunn\.dev/((cairn|immutable)/)?pipeline/[a-z][a-z0-9-]*@[0-9]+(\.[0-9]+){0,2}([^0-9A-Za-z.-]|$) hand-rolled job definitions stop tracking the catalog the estate maintains
file does not match .gitlab-ci.yml ^[^#]*gitlab\.com/dunn\.dev/((cairn|immutable)/)?pipeline/[a-z][a-z0-9-]*@latest @latest re-resolves on every run, so a green build says nothing about the next one
file does not match .gitlab-ci.yml ^[^#]*gitlab\.com/dunn\.dev/((cairn|immutable)/)?pipeline/[a-z][a-z0-9-]*@main @main puts unreleased catalog changes straight into production pipelines

How a project consumes the catalog

default:
  interruptible: true

workflow:
  auto_cancel:
    on_new_commit: interruptible

stages:
  - lint
  - build
  - deploy

include:
  - component: gitlab.com/dunn.dev/pipeline/reference-check@X.Y.Z
    inputs:
      reference_ref: main          # or pin to a tag for stability
  - component: gitlab.com/dunn.dev/pipeline/cloudflare-worker@X.Y.Z
    inputs:
      wrangler_config: site/wrangler.toml
      build_command: npx astro build
      output_dir: site/dist
      needs_npm_install: true
      npm_dir: site
      production_url: https://example.dunn.dev

Every line except the includes is consumer-specific. The includes are the contract. @X.Y.Z is a placeholder; pin to the current release or later.

Validate before push

Before pushing a .gitlab-ci.yml change, run:

glab ci lint .gitlab-ci.yml

GitLab's merge-request UI does not report yaml-shape errors at lint time; it only reports them on the pipeline page, after the broken pipeline has run and failed, burning one CI run for the discovery. glab ci lint sends the file to the same validator and returns the error inline before the push. Treat a failed lint like a failed local test: fix and re-lint until it passes. A pre-push hook on any branch touching .gitlab-ci.yml catches the class entirely.

This is the same shape as the rsvg-convert strict-XML validation for SVG favicons (see repository/CONTRACT.md): a small local check that the deploy pipeline itself can't tell you about.

Catalog design contract

The catalog is itself disciplined by four rules. They land here in reference so that anyone adding to the catalog (or auditing it) is working off the same shape.

  1. Components define jobs only. No stages:, no workflow:, no global variables:. The consumer's .gitlab-ci.yml owns the pipeline scaffold; the catalog fills it in. A component declares its stage as an input with a sensible default but never the stage list itself.

  2. Runner-agnostic. Components do not pin tags:. The estate runs on hosted GitLab CI minutes by default; a consumer that wants a larger runner class (saas-linux-medium-amd64, saas-linux-large-amd64) opts in via default.tags in its own pipeline, not via the catalog.

  3. Image-tracking-version. Every component's image: default resolves to registry.gitlab.com/dunn.dev/pipeline/ci-<lang>:$[[ component.version ]]. A consumer pinned at @X.Y.Z provably gets the ci-go:X.Y.Z image that shipped alongside that catalog tag. Bumping the catalog pin advances the toolchain in lockstep with the job shape, with no :latest drift between the two.

  4. Self-documenting components. Every templates/*.yml ships with two complementary documentation surfaces:

    • A header comment block describing the component's design intent, when to use it, when not to use it, and any constraints the consumer has to satisfy.
    • A GitLab spec.description plus a description: on every inputs.* field. These surface in GitLab's "CI/CD Catalog" UI and in the GraphQL inputs query.

    Consumers should never need to read the template body to understand the component. If they do, the comment block is incomplete and is the next thing to fix.

Component domains

The catalog's components fall into domains by purpose. Pick from the right domain when adding a consumer's include: block:

Domain Components Purpose
Lint / audit go-lint, go-audit, rust-lint, rust-audit, node-audit Per-language static checks. Lint is a real gate; audit is allow_failure because vuln-DB shifts shouldn't block release.
Build go-build, rust-build, node-build Cross-compile / package matrix on every push + MR. Module/build-cached. Skipped on tag pipelines (the release-side build supersedes).
Release / supply chain go-release-binary, rust-release-binary, node-publish, go-sbom, rust-sbom, node-sbom, container-sbom, slsa-attest, release-create, rust-crates-publish Tag-only. Cosign-signed binaries and npm tarballs, a CycloneDX SBOM for every artifact shape (binary, tarball, or image), SLSA v1.0 provenance, an idempotent GitLab Release, and an optional crates.io publish.
Deploy cloudflare-worker, cloudflare-r2, container-image Workers, R2 buckets, OCI images (latest / tag / from-artifacts modes).
Renovate automerge verdict automerge-gate, osv-verdict, pin-audit, secret-verdict, model-id-drift The fail-closed decision job (automerge-gate) plus the verdict-emitting signals it aggregates (newly introduced OSV findings, untrackable image pins, blocking secret detection, stale Claude model ids) before handing a dependency bump to GitLab's native auto-merge.
Brand / drift reference-check, claude-drift-triage Conformance against dunn.dev/reference; LLM-augmented drift classification with optional issue-on-drift.

The mapping is observable in the catalog: each domain shares a release cadence, and every component's image is drawn from the same handful of families (ci-go, ci-rust, the generic ci) picked by the component's language, not by its domain.

Self-reference deviation

The catalog itself cannot consume itself. pipeline's own .gitlab-ci.yml is a meta-pipeline that builds the container images, validates the templates, and publishes the catalog tag. It fails this standard's file-contains assertion by construction: there is no version of pipeline that pre-dates itself.

The right shape is to register the deviation in pipeline/.deviations.yaml:

- standard: pipeline
  assertion: file-contains:.gitlab-ci.yml
  reason: |
    pipeline IS the catalog. There is no version of itself it
    can include.
  owner: <maintainer>

The same pattern applies to any single project that publishes a catalog the rest of the estate consumes: the publisher carries the deviation, and every consumer satisfies the assertion.

When project-local is the right answer

Project-specific business logic that has no general form (a one-off data migration as a scheduled pipeline) stays project-local. Anything you can imagine reusing belongs in the catalog instead.

What this standard does NOT cover (yet)

  • Mixed-tag detection. Asserting every include is at the same version requires a yaml-aware primitive that doesn't exist yet. Treat mismatched tags as a tracked deviation (see Deviations for the format), with the migration that closes it named in the entry's reason.
  • Catalog version freshness. Pin at an older tag while a newer one is current and the standard still passes. Use Renovate (config below) to bump.
  • Non-catalog includes. Includes from other catalogs are out of scope.

Renovate (the estate preset bumps every catalog include)

{
  "extends": ["gitlab>dunn.dev/pipeline:renovate-config"]
}

Renovate's native gitlab-ci manager already recognizes a component include: (gitlab.com/dunn.dev/pipeline/<name>@X.Y.Z) as a bare catalog dependency and proposes the version bump on its own; extending the preset is what puts that dependency under the estate's policy (internal catalogs carry no release-age delay). A project-local regexManagers or customManagers rule to detect the same include would only duplicate what the native manager already does.

How to satisfy

Include the catalog components your project's language and artifacts call for, each at a pinned version, in the canonical include: - component: gitlab.com/dunn.dev/pipeline/<name>@X.Y.Z shape documented above. Keep the pin current by extending the estate Renovate preset (above) rather than bumping by hand.

fetch as markdown
curl https://reference.dunn.dev/standards/pipeline/index.md
check conformance
just -f ~/gitlab.com/dunn.dev/reference/justfile check