Skip to content

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 a ci: 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 main and 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): release commits that only touch CHANGELOG.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-build building 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.