0311 — Pipeline trigger economy — batch:true estate-wide, cross-repo triggers path-scoped
The pitlab CI pool is a single serial agent, which makes pipeline run volume a shared budget: every avoidable run delays every necessary one. This ADR extends ADR-0054’s build coalescing from two repos to every CI trigger in the estate, mandates path filters on cross-repo (resources.repositories) triggers, and changes pipewait to accept coalesced runs by ancestry by default so batching cannot masquerade as a dead trigger. It adds the Pipeline Trigger Economy section to the CI/CD & Pipelines Standard.
Status
Section titled “Status”Accepted — 2026-08-04. Amends the CI/CD & Pipelines Standard; extends ADR-0054. Delivered under Epic #2166 (pipeline run-volume reduction).
Context — 200 runs in 19 hours, three-quarters of them re-validating nothing
Section titled “Context — 200 runs in 19 hours, three-quarters of them re-validating nothing”Measured over 2026-08-03/04: ~200 pipeline runs in 19 hours on the one-agent pool, with 73% of the volume from six pipelines. The dominant costs were structural, not workload:
- Repo-wide guards fired on every ansible push.
ansible-secret-scan(all paths — deliberate),ansible-control01-pull(all paths), andansible-validate-notification-catalog(playbooks/ + roles/ + scripts/≈ the whole repo) each ran per push, plus whichever deploy pipeline the push actually targeted: one push, four to five queued serial runs. - No ansible pipeline batched. ADR-0054 applied
batch: trueto the docs and docker-stacks pipelines in isolation and never became a standard clause, so all 72 ansible pipelines accrued without it — a session pushing four commits fired four full guard fan-outs (146individualCIvs 37batchedCIruns in the window). The one-off fix that never became a rule is the same failure mode ADR-0308’s 17-pipeline sweep documents. - Cross-repo triggers were unfiltered. The notification-catalog guard’s
resources.repositoriestriggers on pitlab-docs and docker-stacks carried nopathsfilter, so every docs blog post and every docker-stacks Renovate image bump re-ran a guard those pushes cannot affect: 51 runs/19h — a quarter of all pool traffic — from one pipeline.
Decision
Section titled “Decision”- Every CI
trigger:block setsbatch: true. Applied to all 72 ansible pipelines (docker-stacks and docs already conformed). Safe estate-wide because every pipeline re-applies latest declared state at run time (git reset --hard origin/main, orcheckout: selfat the batch head) — a batched run can never drop a commit.trigger: nonepipelines trivially conform; a future per-commit-artifact pipeline may opt out with a comment. - Cross-repo triggers carry
pathsfilters derived from the validator’s own file reads. The notification-catalog guard now scopes pitlab-docs todocs/**minusdocs/blog/**(kb anchors resolve against the docs tree; blog posts hold none and are the highest-frequency docs push) and docker-stacks tostacks/** + scripts/** + pipelines/**(rule-template kb annotations and pushover call sites; group_vars image bumps cannot introduce either). The derivation is recorded beside the trigger; the weekly Saturday drift run remains the backstop, so a too-narrow filter degrades to weekly detection, never silence. pipewaitaccepts coalesced runs by default. Exact and ancestry acceptance (git merge-base --is-ancestor <my-sha> <run-sha>) are always on — ancestry is sound under any checkout semantics, since a run labelled with a descendant of my commit checked out a tree containing it. Start-time acceptance (run started after the wait began) stays opt-in behind--batched: it alone assumes run-time re-sync toorigin/<branch>, true of every current pipeline (verified 2026-08-04) but not future-proof. Proven against live ADO: an older SHA ancestry-matched covering run 7100; a bogus SHA matched nothing.
Alternatives considered
Section titled “Alternatives considered”- A second pool agent — treats the symptom (queue latency) while leaving the volume; rejected as the primary fix, remains open as a capacity follow-up.
- Consolidating the always-on guards into one pipeline — sound (one queue slot per push instead of three), but structural; deferred to its own Issue under Epic #2166 rather than blocking the mechanical wins. Since realised — the five always-on ansible guards are now parallel jobs in
ansible-ci; see ADR-0312. [skip ci]discipline for no-op commits — already available, but relies on per-commit human judgement; batching and scoping fix the structure instead.- Auto-detecting batched pipelines in pipewait (query the definition’s YAML for
batch: true) — extra API surface for no gain once ancestry is default-on and start-time is the only conditional path.
Consequences
Section titled “Consequences”- A burst of N pushes costs ~2 runs per affected pipeline instead of N; docs/stacks pushes no longer tax the notification-catalog guard unless they touch what it reads.
- “Wait for my SHA” tooling must accept covering runs — handled centrally in
pipewait; callers need no flag. A waiter without a local repo cannot ancestry-match and may seeDISCOVERY_TIMEOUTon a coalesced push — the discovery log hints at this explicitly. - A filter mistake on a cross-repo trigger surfaces at the weekly drift run rather than the offending commit — the accepted trade for a quarter of pool traffic; the filters are derived from validator code, not intuition, to keep that risk small.
- Enforcement is review-tier (pipeline-diff review + the conformance sweep at adoption); no machine gate yet enumerates triggers for
batch: true. If drift is observed, promote to a machine check invalidate_pipeline_path_coverage.py. - Observed 2026-08-04:
batch: truedoes not appear to coalesce cross-repo triggers. With the pool saturated by a longcontrol01-sync, fouransible-ciruns sat queued simultaneously (7139, 7140, 7141, 7143) — three of themindividualCIfrom separate sibling-repo pushes (38f44326,f1886b64,9a6893f7), including two pitlab-docs pushes that would have coalesced had batching applied to them. The reading is thattrigger.batchgoverns the pipeline’s own-repo CI trigger, while eachresources.repositoriestrigger queues independently; ADO’s docs do not state this either way, so it is recorded as an observation, not a documented guarantee. Practical effect is small — each sibling push still costs one run instead of the pre-consolidation fan-out — but do not diagnose “batching is broken” from a queue of same-pipeline runs whose reasons areindividualCIwith distincttriggerInfo.ci.sourceShavalues. The path filters, not batching, are what bound cross-repo cost.