fix(ansible): write DB inventory as static YAML, not dynamic --list JSON
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m20s
CI / publish (push) Successful in 1m6s

generate_inventory() emits Ansible's dynamic --list shape (all.hosts is a
LIST, vars under _meta) — valid only as an executable inventory script's
stdout. We were writing it to a static file and passing -i, so Ansible's yaml
plugin rejected it ("Invalid 'hosts' entry for 'all' group, requires a
dictionary, found ...list...") and fell back to implicit localhost → "no hosts
matched". Affected every steward:* scope run; surfaced on the first real
provision.

- New inventory_to_yaml(inv): convert the --list dict → a valid static YAML
  inventory (all.hosts dict keyed by host, groups under all.children, group
  vars preserved, injected per-host vars like steward_token retained).
- Wire it into runner.trigger_run, host_agent deploy + provision.
- executor writes the file as inventory.yml so the yaml plugin's extension
  check reliably claims it.
- generate_inventory unchanged (still the --list dict); conversion happens at
  write time. Unit tests added.

Scribe issue #885.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 09:28:31 -04:00
parent 9ce4cce5c5
commit a92d1995d5
5 changed files with 78 additions and 11 deletions
+4 -3
View File
@@ -7,12 +7,13 @@ manual, alert-triggered, and scheduled runs all take the exact same path.
from __future__ import annotations
import asyncio
import json
import uuid
from datetime import datetime, timezone
from steward.ansible import executor, sources as src_module
from steward.ansible.inventory_gen import fetch_scope_targets, generate_inventory
from steward.ansible.inventory_gen import (
fetch_scope_targets, generate_inventory, inventory_to_yaml,
)
from steward.models.ansible import AnsibleRun, AnsibleRunStatus
@@ -53,7 +54,7 @@ async def trigger_run(
elif inventory_scope.startswith("steward:"):
async with app.db_sessionmaker() as db:
targets = await fetch_scope_targets(db, inventory_scope)
inventory_content = json.dumps(generate_inventory(targets))
inventory_content = inventory_to_yaml(generate_inventory(targets))
elif inventory_scope.startswith("repo:"):
parts = inventory_scope.split(":", 2)
inventory_path = parts[2] if len(parts) == 3 else ""