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.
Components define jobs only. No
stages:, noworkflow:, no globalvariables:. The consumer's.gitlab-ci.ymlowns the pipeline scaffold; the catalog fills it in. A component declares itsstageas an input with a sensible default but never the stage list itself.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 viadefault.tagsin its own pipeline, not via the catalog.Image-tracking-version. Every component's
image:default resolves toregistry.gitlab.com/dunn.dev/pipeline/ci-<lang>:$[[ component.version ]]. A consumer pinned at@X.Y.Zprovably gets theci-go:X.Y.Zimage that shipped alongside that catalog tag. Bumping the catalog pin advances the toolchain in lockstep with the job shape, with no:latestdrift between the two.Self-documenting components. Every
templates/*.ymlships 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.descriptionplus adescription:on everyinputs.*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.
curl https://reference.dunn.dev/standards/pipeline/index.md
check conformance
just -f ~/gitlab.com/dunn.dev/reference/justfile check