Change-detection¶
"The fastest job is the one you don't run." Before change-detection, every
gate job ran on every merge request — a zensical-build (the docs site)
would fire on a Go-only MR just as readily as on a docs MR. On a
constrained, self-hosted runner (a fixed concurrency ceiling), that's real
wasted capacity, contended by everyone else's pipeline. Change-detection is
GitLab's rules:changes mechanism, wired into every relevant gate
component as a default-on, consumer-extensible input.
The mechanism¶
Each gate component gains a changes array input with a conservative,
toolchain-appropriate default, attached to the job's existing MR rule:
spec:
inputs:
changes:
type: array
default: ["**/*.go", "go.mod", "go.sum", ".gitlab-ci.yml"]
---
<job>:
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
when: never
- if: '$[[ inputs.if ]]'
changes: $[[ inputs.changes ]]
The schedule-never guard stays the first rule, unconditionally; changes
attaches only to the MR rule — so a consumer widening if: still can't pull
a job onto the schedule, and tag pipelines (where a component has one) are
unaffected either way.
Consumers extend the default rather than replace it. The one case that
matters in practice: a Go project embedding a Svelte UI extends
go-test's changes with its frontend path
(pkg/studio/web/**), so a frontend-only change still runs the Go
embed/served-UI tests — see
the Svelte frontend track. Setting
changes: ["**/*"] on any component is the universal escape hatch back to
"run on every MR."
The reliability constraint (why this is scoped to MRs)¶
rules:changes doesn't mean the same thing on every pipeline type:
| Pipeline | changes compares against |
Reliable? |
|---|---|---|
merge_request_event |
the MR diff, vs. the target merge-base | yes |
| branch push (incl. default branch) | the push's before→after SHA | often — but a new branch, a multi-commit push, or a force-push can make it always-true |
| tag / scheduled | nothing meaningful | always true |
Every gate this applies to is already MR-gated, so adding changes to
those MR rules is reliable by construction. Deploy, release, and schedule
rules stay unfiltered — which is exactly what's wanted there: a release tag
should always run its publish job regardless of which files a changes
filter would (unreliably) match against.
The three traps a default has to avoid¶
- False negatives are the real danger — not false positives. A filter
that's too narrow lets an untested change merge silently. Every default
therefore includes all the relevant source globs, every lockfile and
tool config (
go.mod/go.sum,Cargo.toml/Cargo.lock,rustfmt.toml,rust-toolchain.toml,*.tf, …), and — critically — the consumer's own.gitlab-ci.yml, so a CI edit always re-runs everything. The bias throughout is "run a needless job" over "skip a needed one." - Build and deploy must skip or run together. Where a component has a
pagesjob thatneeds:a build job (zensical-pages,hugo-pages), both jobs share the exact samechangesfilter. Without that, a build could skip while deploy still tries to run — a deploy with no artifact, which fails outright. With matching filters, a no-content push to the default branch correctly skips both: nothing changed, nothing redeploys. - The whole security category is exempt. See security, always-on — this was significant enough to become its own decision, not a footnote here.
Rollout: pilot first, then propagate¶
Change-detection didn't land everywhere at once. It shipped as a pilot
on zensical-pages first —
docs-only, no code false-negative danger, and the one component that could
prove the mechanism actually works: whether a type: array component input
interpolates correctly into a rules:changes: YAML list. Only once that
was validated did it propagate to hugo-pages (with a schedule carve-out —
see static sites), then the Go/Rust gates, then the Tofu
gates. The Svelte track shipped with change-detection built in from day
one, since it landed after the mechanism was already proven.
See also¶
- Security, always-on — the one category exempt from this.
- The Svelte frontend track — the
go-testembed-coupling extension. - Static sites — the
hugo-pagesschedule exemption. - How-to: Add change-detection to a gate
- Spec:
2026-06-23-change-detection