Spec: cicd self-test fan-out churn scoping¶
- Repository:
gitlab.com/phpboyscout/cicd - Released as: no component release — a root
.gitlab-ci.yml(CI-internal) change, shipped as aci:commit (no version bump). - Driver: the task-2 churn audit (follow-up noted in the v0.10.8 and v0.11 specs).
Problem¶
Each of the 19 self-test:<component> triggers carried:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
changes: [templates/<name>.yml, tests/<name>/**/*]
- if: $CI_COMMIT_BRANCH || $CI_COMMIT_TAG # <-- unscoped fallback
The fallback matches on every tag ($CI_COMMIT_TAG), every
default-branch commit ($CI_COMMIT_BRANCH), every stray feature-branch
push, and (since a schedule sets $CI_COMMIT_BRANCH) any future
schedule — running all 19 child pipelines each time, regardless of
what changed. Concretely:
- Tags: a release tag is cut from already-green
mainand the tag pipeline has no publish jobs (CLAUDE.md: "currently nothing"). Running 19 self-tests on it validates nothing. Two tags were cut in one session → 38 wasted child pipelines. - Default-branch commits: every merge re-runs the full suite —
including
chore(main): releasecommits that only touchCHANGELOG.md.
Decisions¶
D1 — One rule per trigger: (MR or default-push) AND changed¶
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event" || ($CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH)'
changes: [templates/<name>.yml, tests/<name>/**/*]
A single rule covering both contexts where a self-test is meaningful,
both gated by the component's own changes: list. Net effect per
pipeline source:
| Source | Before | After |
|---|---|---|
| MR | changed components | changed components (unchanged) |
| default-branch push | all 19 | only components whose files changed |
| tag | all 19 | none |
| schedule | all 19 | none |
| stray feature branch | all 19 | none (MR pipeline covers the work) |
D2 — Accept the changed-scoping trade-off on the default branch¶
Scoping the default-branch run by changes: (rather than always
running the full suite) means a cross-component interaction — MR-A
breaks component B without touching B's files — would not be re-tested
on main. Accepted: the repo uses changed-detection at MR time and
merges keep main equal to the tested tree, so the residual risk is
low and worth the large churn reduction. The MR changes: list for
tofu-apply already includes templates/tofu-plan.yml (its real
cross-file dependency), and that coupling is preserved.
D3 — No when: never guards needed¶
The combined if only matches MR and default-branch-push, so tag /
schedule / stray-branch pipelines simply find no matching rule and the
trigger is not added — no explicit never rule required.
Out of scope (this pass)¶
zensical-buildbuilding docs on tag pipelines (deploy is branch-only) — a component-level patch, deferred.- Consumer pipelines were audited and are clean: gates are MR-only, deploys/releases default-branch-only, goreleaser tag-only.