# ADR-019 — Tagging standard for targeted, predictable runs ## Status Accepted (2026-06-06). Resolves TODO 3.7 ("Define a tagging standard that lets us target runs without over-tagging") and TODO 3.11 ("Deliberate tagging strategy"). ## Context boma wants to run playbooks **targeted** — a single service, a single layer, or a single cross-cutting concern — **transparently and predictably**: a reader should know from a `--tags` invocation exactly what it will and won't touch. CLAUDE.md already requires tag-filterable tasks, but no vocabulary or convention existed, and the TODO explicitly warns against the opposite failure mode: **over-tagging**. ## Decision ### Two-tier tagging **Tier 1 — role/service tag (mechanical).** The tag equals the role name, applied once at the role-import level: ```yaml roles: - role: photoprism tags: [photoprism] ``` Ansible propagates it to every task in the role. Because one service = one role (ADR-004), this single rule covers both the *layer/role* and *single-service* targeting axes with zero per-task burden. Role-less lifecycle playbooks (e.g. `bootstrap.yml`) carry a single playbook-identity tag instead. **Tier 2 — concern tag (curated).** A small **closed list** of cross-cutting concern tags, applied per-task/block **only where a task genuinely belongs to that concern**. ### The closed concern list A concern earns a tag only if it (a) appears in 2+ roles, (b) is worth running as a slice on its own, and (c) doesn't overlap confusingly with another. | Tag | Covers | |-----|--------| | `packages` | apt package install/management | | `users` | accounts, groups, sudo | | `firewall` | nftables rulesets & port definitions (ADR-002) | | `hardening` | security baseline — sshd config, fail2ban, auditd, sysctl | | `logging` | Alloy / log-shipping config (ADR-018) | | `monitoring` | metric exporters / health checks | | `config` | render templated config/compose files to disk — **no restart** | | `deploy` | bring services up / restart (`compose up -d`) | | `proxy` | reverse-proxy + TLS registration (Traefik routes, Authentik) | The `config`/`deploy` split lets you re-render and diff configuration (`--tags config`) without bouncing services, then restart deliberately (`--tags deploy`). `backup` and `secrets` are intentionally omitted until the roles needing them exist. ### `always` / `never` - **`always`** — reserved for cheap preflight assertions (vault unlocked, OS is Debian 13, required vars present), so even `--tags config` runs its safety guards. - **`never`** — reserved for destructive/expensive opt-in tasks, each paired with a descriptive tag (e.g. `tags: [never, force_pull]`); they run only when named. ### Predictability principle: tags are union-only `--tags a,b` runs tasks tagged a **OR** b — Ansible has no native AND. boma therefore targets **one axis at a time**: either a role/service *or* a concern, never an intersection like "photoprism's firewall only." If that's ever needed, just run `--tags photoprism` (idempotent and fast). Designing for intersection is the over-tagging trap; we decline it on purpose. ### Terraform / Proxmox VM tags (metadata only) Every Terraform-managed VM carries exactly three Proxmox tags: | Tag | Value | Purpose | |-----|-------|---------| | env | `staging` \| `production` | which environment | | role/group | `docker_hosts`, `proxmox_hosts`, … | matches the inventory group | | managed-by | `terraform` | distinguishes IaC VMs from hand-made ones | These are **pure metadata for transparency** (glanceable in the Proxmox UI). They do **not** drive run-targeting and do **not** feed inventory — `scripts/tf_to_inventory.py` keeps building groups from the `group` output field, the single source of truth. ## Enforcement `tests/tags.yml` is the single source of truth for the allowed concern/special/ opt-in/playbook tags. `scripts/check-tags.py` (run by `make lint`, covered by `tests/test_check_tags.py`) scans `roles/` and `playbooks/` and fails on any tag outside `{role directory names} ∪ {tests/tags.yml entries}`. ## Extending the vocabulary To add a concern tag: (1) add it to `tests/tags.yml`; (2) add a row to the concern table above with a one-line justification showing it passes the litmus test (cross-cutting, 2+ roles, distinct). That is the whole gate — lightweight, but it leaves a paper trail. ## Consequences - Targeted runs are predictable: only two kinds of tags exist, one of them mechanical. - Over-tagging is structurally resisted (closed list + lint enforcement). - Intersection targeting is unavailable by design. - Authors must keep role tags = role names; the linter enforces it. ## Related ADR-002 (security baseline / firewall), ADR-004 (one service = one role), ADR-009 (TF↔Ansible handoff / inventory), ADR-018 (logging).