Skip to content

ADR-0106: docker-stacks pipeline-gate scripts live in the docker-stacks repo, a scoped carve-out from RULE 6

This ADR records why the docker-stacks post-deploy pipeline-gate scripts live in docker-stacks/scripts/ and run repo-relative from the control01 ADO agent checkout, rather than in ansible/scripts/ deployed to /usr/local/bin as RULE 6 (every script lives in Ansible) otherwise requires. The carve-out was established in practice by successive gates but was unwritten — it lived only in a native-memory feedback note. It is now codified as the Pipeline-Gate Script Location Standard; this ADR captures the reasoning and the rejected alternative.

StatusAccepted
Date2026-07-04
EpicADO #1476 (Service Test Plan Standard — fleet rollout), Issue #1486

RULE 6 is a prime directive: every script lives in ansible/scripts/ with a deployment task that copies it to /usr/local/bin on its target host, committed before session end. It exists to give every script one source of truth and a rollback handle, and to stop hand-scp’d drift. The Script Standard makes that shape checkable.

The docker-stacks deploy pipelines grew a family of post-deploy gates that do not fit that mould. smoke_gate.py (ADR-0067) asserts every just-deployed container started and serves; loki_log_gate.py (ADR-0086) asserts each stack’s logs reach Loki; and the test-plan family testplan_run.py / testplan_gate.py / testplan_verify.sh / testplan_synthetic_gate.py (ADR-0102, ADR-0104, ADR-0105) run the per-service acceptance plans. Each is invoked repo-relative inside the pipeline YAML — cd ~/docker-stacks && python3 scripts/<gate>.py <services…> — against the ~/docker-stacks checkout on the control01 ADO agent. control01 is the agent (vsts.agent…control01), and the checkout is kept current by git pull at the start of each run.

These gates have none of the properties RULE 6 optimises for and all of the properties it would harm: their only consumer is the pipeline that checks out the repo and runs them directly, they must version atomically with the stack definitions they assert against, and a deployed /usr/local/bin copy would be a second source of truth to drift. The ADR-0102 as-built already noted this deviation from the standard’s earlier “deployed via Ansible” wording. The gap was that the carve-out remained unwritten — a reader of RULE 6 or the Script Standard would reasonably conclude the gates were misplaced.

A script that is a docker-stacks pipeline gate — invoked repo-relative from the control01 agent’s ~/docker-stacks checkout inside a docker-stacks pipeline stage — lives in docker-stacks/scripts/, with no Ansible deploy task and no /usr/local/bin copy. Every other script (host cron/timer/operational tooling) stays in ansible/scripts/ under RULE 6. The routing rule is codified in the Pipeline-Gate Script Location Standard, and the gates still meet the Script Standard shape rules apart from the deploy-to-/usr/local/bin clauses. Rationale:

  1. One source of truth. The pipeline checks out the repo and runs the script directly — there is no second deployed copy that can diverge from the copy the pipeline reasons about.
  2. Atomic co-versioning. A gate and the stack definitions it asserts against live in the same repo, so a stack change and its gate update land in one commit.
  3. The only consumer already has the repo. Every consumer (deploy pipelines, /vulnscan, /verify) runs on control01 where ~/docker-stacks is present; an Ansible deploy to /usr/local/bin would buy nothing.
  • Force the gates into ansible/scripts/ and deploy to /usr/local/bin (strict RULE 6) — rejected. It creates a second copy of each gate decoupled from the docker-stacks tree it asserts against (breaking atomic co-versioning), adds cross-repo coupling (a docker-stacks pipeline depending on an ansible playbook having deployed the script that gates docker-stacks), and introduces the exact drift RULE 6 exists to prevent. It costs a source-of-truth to satisfy the letter of a rule whose intent is already met.
  • Leave the carve-out unwritten (status quo) — rejected. The convention was load-bearing but existed only in a native-memory feedback note; a reader of RULE 6 would flag the gates as misplaced, and the next gate author would re-derive the reasoning or wrongly force it into Ansible. A recurring, load-bearing exception is a standard worth writing once.
  • A dedicated deploy repo/mechanism for all CI gates — rejected as premature. The gates already have a natural, correct home beside the stacks they gate; inventing a third location adds machinery without solving a problem the co-location does not already solve.
  • RULE 6 gains a written, scoped exception. The prime directive still holds for host tooling; the carve-out is narrow — location and deployment only — and pointer-linked from the RULE 6 wording in CLAUDE.md.
  • The routing decision is now checkable up front. The Pipeline-Gate Script Location Standard gives the next gate author a one-question decision rule, so placement is right the first time instead of reflexively Ansible.
  • The Script Standard’s “one home” claim is reconciled. script-standard.md now cross-references the carve-out so the two do not contradict.
  • The native-memory feedback note is superseded by the standard + this ADR — the doc corpus is now the source of truth, per RULE 9 (docs preferred over memory).
  • A blind spot remains: a gate’s shape (naming, shebang, lint) is not yet enforced by CI in the docker-stacks repo the way ansible/scripts/ aims to be. This inherits the Script Standard’s known thin-lint-coverage gap; closing it fleet-wide is out of scope here.
  • A trigger-coverage side effect surfaced later. Placing gates in scripts/ meant a change to a gate never re-ran the pipeline that runs it (which triggered only on stacks/**), so a gate fix went un-revalidated — a systemic silent-non-validation gap found in Epic #2063. That is closed by ADR-0270: every pipeline running a gate now triggers on scripts/**. This ADR’s location decision stands; ADR-0270 is its trigger-coverage complement.
  • A THIRD category was missing from the routing rule, and it was already in production. This ADR framed placement as a two-way question — host tooling vs pipeline gate — and both answers assume the code runs on a host. Application code that is a containerised service, mounted into a stock image, runs on neither, and the estate had three instances of it (obs-*-exporter, incident-webhook, topcharts_generate.py) with no rule describing them. Added 2026-08-08 by ADR-0346, which restates Prime Directive 6 as a routing question with three homes. This ADR’s carve-out stands unchanged; 0346 is its third branch.
  • Correction (2026-08-08): the original RULE 6 wording pointed at “the control01 agent checkout” as the only consumer. The CI pool has been plural since ADR-0330 — gates run on runner01/runner02 — which is precisely why a pipeline-invoked script is never deployed to one named agent (ADR-0333). Corrected in CLAUDE.md alongside ADR-0346.