feat(ansible): steward:category + steward:confirm playbook metadata
CI / lint (push) Successful in 2s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m14s
CI / publish (push) Successful in 59s

Extend the playbook metadata convention with a namespaced `# steward:<key>:`
comment block:

- steward:category — free-text grouping label, shown as a badge in the browse
  list and on the run form.
- steward:confirm — true/yes/1/on marks a playbook destructive; the run form
  then requires a confirmation tick (required checkbox in the shared vars
  fragment) before it can launch.

sources.discover_playbook_meta() parses description + category + confirm (first
match per key; `# description:` still primary, `# steward:description:` alias).
discover_playbook_description() now delegates to it. The browse list reads
per-playbook meta to show category badges + descriptions; the run-form and
playbook-vars fragments render the badge + confirm gate.

Bundled playbooks tagged: docker_prune → category maintenance + confirm true;
provision/install/update → category host-agent.

Docs: docs/reference/playbook-authoring.md updated (keys now implemented) and a
quick reference added next to the code at steward/ansible/PLAYBOOK_CONVENTIONS.md.
Tests added for category/confirm/alias parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:35:35 -04:00
parent b32fce1d74
commit 42f7840c26
11 changed files with 192 additions and 39 deletions
+49 -19
View File
@@ -20,6 +20,52 @@ _SECRET_VAR_RE = re.compile(
# A playbook self-describes via a `# description:` magic comment (Ansible rejects
# unknown play keys, so a comment is the portable place). First match wins.
_DESCRIPTION_RE = re.compile(r"^\s*#\s*description:\s*(.+?)\s*$", re.I | re.M)
# Namespaced metadata: `# steward:<key>: <value>` (category, confirm, …).
_STEWARD_META_RE = re.compile(r"^\s*#\s*steward:(\w+):\s*(.+?)\s*$", re.I | re.M)
_TRUTHY = {"1", "true", "yes", "on"}
def _first_play_name(content: str) -> str:
import yaml
try:
plays = yaml.safe_load(content)
except yaml.YAMLError:
return ""
if isinstance(plays, list):
for play in plays:
if isinstance(play, dict) and play.get("name"):
return str(play["name"]).strip()
return ""
def discover_playbook_meta(content: str) -> dict:
"""Parse Steward's playbook metadata from magic comments.
Returns {description, category, confirm}. Sources:
- description: ``# description:`` (primary) or ``# steward:description:``;
falls back to the first play's ``name:``.
- category: ``# steward:category: <text>`` (free-text grouping label).
- confirm: ``# steward:confirm: true`` → require an explicit confirmation
in the run form before launching (for destructive playbooks).
First match wins for each key.
"""
steward: dict[str, str] = {}
for km in _STEWARD_META_RE.finditer(content):
steward.setdefault(km.group(1).lower(), km.group(2).strip())
m = _DESCRIPTION_RE.search(content)
if m:
description = m.group(1).strip()
elif steward.get("description"):
description = steward["description"]
else:
description = _first_play_name(content)
return {
"description": description,
"category": steward.get("category", ""),
"confirm": str(steward.get("confirm", "")).strip().lower() in _TRUTHY,
}
# Name of the always-present, read-only source of first-party playbooks shipped
# inside the app (maintenance tasks, host-agent install). Not operator-editable.
@@ -200,25 +246,9 @@ def delete_playbook(source_path: str, relative_path: str) -> tuple[bool, str | N
def discover_playbook_description(content: str) -> str:
"""A human-readable description of what a playbook does.
Reads a ``# description: ...`` magic comment (first match), so any playbook
can self-describe without touching its YAML structure. Falls back to the
first play's ``name:``. Returns "" if neither is present.
"""
m = _DESCRIPTION_RE.search(content)
if m:
return m.group(1).strip()
import yaml
try:
plays = yaml.safe_load(content)
except yaml.YAMLError:
return ""
if isinstance(plays, list):
for play in plays:
if isinstance(play, dict) and play.get("name"):
return str(play["name"]).strip()
return ""
"""A human-readable description of what a playbook does (see
discover_playbook_meta). Returns "" if none can be determined."""
return discover_playbook_meta(content)["description"]
def discover_playbook_variables(content: str) -> list[dict]: