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.
| Status | Accepted |
| Date | 2026-07-04 |
| Epic | ADO #1476 (Service Test Plan Standard — fleet rollout), Issue #1486 |
Context
Section titled “Context”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.
Decision
Section titled “Decision”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:
- 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.
- 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.
- The only consumer already has the repo. Every consumer (deploy pipelines,
/vulnscan,/verify) runs on control01 where~/docker-stacksis present; an Ansible deploy to/usr/local/binwould buy nothing.
Alternatives considered
Section titled “Alternatives considered”- 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 thedocker-stackstree 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.
Consequences
Section titled “Consequences”- 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 onstacks/**), 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 onscripts/**. 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 inCLAUDE.mdalongside ADR-0346.