Skip to content

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.

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:

  1. Repo-wide guards fired on every ansible push. ansible-secret-scan (all paths — deliberate), ansible-control01-pull (all paths), and ansible-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.
  2. No ansible pipeline batched. ADR-0054 applied batch: true to 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 (146 individualCI vs 37 batchedCI runs in the window). The one-off fix that never became a rule is the same failure mode ADR-0308’s 17-pipeline sweep documents.
  3. Cross-repo triggers were unfiltered. The notification-catalog guard’s resources.repositories triggers on pitlab-docs and docker-stacks carried no paths filter, 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.
  1. Every CI trigger: block sets batch: 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, or checkout: self at the batch head) — a batched run can never drop a commit. trigger: none pipelines trivially conform; a future per-commit-artifact pipeline may opt out with a comment.
  2. Cross-repo triggers carry paths filters derived from the validator’s own file reads. The notification-catalog guard now scopes pitlab-docs to docs/** minus docs/blog/** (kb anchors resolve against the docs tree; blog posts hold none and are the highest-frequency docs push) and docker-stacks to stacks/** + 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.
  3. pipewait accepts 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 to origin/<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.
  • 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.
  • 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 see DISCOVERY_TIMEOUT on 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 in validate_pipeline_path_coverage.py.
  • Observed 2026-08-04: batch: true does not appear to coalesce cross-repo triggers. With the pool saturated by a long control01-sync, four ansible-ci runs sat queued simultaneously (7139, 7140, 7141, 7143) — three of them individualCI from separate sibling-repo pushes (38f44326, f1886b64, 9a6893f7), including two pitlab-docs pushes that would have coalesced had batching applied to them. The reading is that trigger.batch governs the pipeline’s own-repo CI trigger, while each resources.repositories trigger 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 are individualCI with distinct triggerInfo.ci.sourceSha values. The path filters, not batching, are what bound cross-repo cost.