Files
computer-playbook/utils/annotations/task_gate.py
T
kevinveenbirkenbach fb977e975a test(lint): recognise compose-only task gates + skip comment / YAML metadata lines
The swarm-compat lints (`compose-chdir-in-task`, `compose-verb-in-task`,
`docker-compose-raw-call`) currently flag every offending line and only
accept per-line / file-head `# nocheck:` markers. That is noisy for
two patterns that are objectively safe:

1. Tasks gated by `when: DEPLOYMENT_MODE \!= 'swarm'` (or
   `== 'compose'`) never reach a swarm host - the compose verbs inside
   are legitimate.
2. YAML comment lines, handler-trigger list items, and human-readable
   metadata fields (`name:`, `msg:`, `title:`, `label:`, `description:`)
   contain the word `compose` as documentation, not as a shell call.

The lints now skip both classes automatically.

- utils/annotations/task_gate.py: new helper
  `is_task_compose_only_gated(lines, idx)`. Walks the surrounding
  task block via a line-based heuristic and matches `when:` against
  the compose-only DEPLOYMENT_MODE expressions. Documented as
  deliberately shallow (no YAML parse).
- tests/lint/ansible/shell/test_no_compose_chdir_in_shell_tasks.py and
  tests/lint/ansible/shell/test_no_compose_verb_in_shell_tasks.py:
  add a file-level `is_suppressed_in_head` skip, the
  `is_task_compose_only_gated` skip, and -- for the verb lint --
  three extra regex exclusions for notify-list items, YAML metadata
  fields, and quoted list items.
- tests/lint/ansible/shell/test_no_docker_compose_raw_call.py: skip
  YAML comment lines (bluesky and jenkins each had a `# … docker
  compose build …` doc-comment that was being flagged as a real call).
- tests/unit/utils/annotations/test_task_gate.py: 21 cases covering
  block-bounds + gate detection, including a `TestKnownLimitations`
  class that pins the heuristic's blind spots (folded-scalar `when:`,
  list-form `when:`, `in`/`not in`, unquoted literal, outer-`when:`
  inheritance into `block:` children) so a future YAML-aware refactor
  knows exactly which assertions to flip.
2026-06-14 19:08:45 +02:00

69 lines
2.4 KiB
Python

"""Detect Ansible task-level `when:` gates that exempt compose-only
shell calls from swarm-compat lints.
A task whose `when:` evaluates compose-only at deploy time (e.g.
`when: DEPLOYMENT_MODE != 'swarm'` or `when: DEPLOYMENT_MODE == 'compose'`)
never reaches a swarm host, so `compose <verb>` / `chdir: directories.instance`
inside that task body is legitimate. This module gives the lints a
heuristic to recognise that gate without parsing YAML.
The detection is line-based: the task block is the span between the
nearest `- name:` (at indent N) above the offending line and the next
top-level structural marker (`- name:` at indent <= N, or end of
file). Within that span we look for any `when:` clause whose textual
value matches a compose-only DEPLOYMENT_MODE expression.
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
_TASK_NAME_RE = re.compile(
r"^(?P<indent>\s*)-\s+(?:name|block|include_tasks|import_tasks|hosts)\s*:"
)
_WHEN_RE = re.compile(r"^\s*when\s*:\s*(?P<expr>.+?)\s*$")
_COMPOSE_ONLY_EXPR = re.compile(
r"DEPLOYMENT_MODE\s*!=\s*['\"]swarm['\"]"
r"|DEPLOYMENT_MODE\s*==\s*['\"]compose['\"]"
)
def _task_block_bounds(lines: Sequence[str], idx: int) -> tuple[int, int]:
"""Return (start, end_exclusive) of the task block containing *idx*.
*start* is the index of the nearest `- name:` (or `- block:`/`- include_tasks:`)
above *idx*. *end* is the index of the next sibling `- name:` at the same
indent, or `len(lines)` if none.
"""
start = -1
indent = ""
for i in range(idx, -1, -1):
m = _TASK_NAME_RE.match(lines[i])
if m:
start = i
indent = m.group("indent")
break
if start < 0:
return (0, len(lines))
for j in range(start + 1, len(lines)):
m = _TASK_NAME_RE.match(lines[j])
if m and len(m.group("indent")) <= len(indent):
return (start, j)
return (start, len(lines))
def is_task_compose_only_gated(lines: Sequence[str], idx: int) -> bool:
"""True iff the task body containing *idx* carries a `when:` clause
whose textual expression evaluates compose-only at deploy time."""
start, end = _task_block_bounds(lines, idx)
for k in range(start, end):
m = _WHEN_RE.match(lines[k])
if m and _COMPOSE_ONLY_EXPR.search(m.group("expr")):
return True
return False