docs: Steward playbook-authoring conventions reference
CI / lint (push) Successful in 3s
CI / unit (push) Successful in 8s
CI / integration (push) Successful in 2m22s
CI / publish (push) Successful in 4s

A self-contained guide (docs/reference/playbook-authoring.md) to the contract
between a playbook and Steward's run UI — the `# description:` comment,
vars/vars_prompt → fill-in fields, secret naming, hosts: all targeting,
managed credentials, and idempotency. Includes a "what Steward reads" summary
table, the metadata/extensibility note (only `# description:` today; reserved
`# steward:<key>:` namespace for future keys), and an annotated example.
Meant to be fed to another session authoring Steward-friendly playbooks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 11:26:56 -04:00
parent e5f6a11f94
commit b32fce1d74
+169
View File
@@ -0,0 +1,169 @@
# Authoring Steward-friendly Ansible playbooks
This is the contract between a playbook and Steward's run UI. Follow it and a
playbook drops into Steward with a description, fill-in variable fields, correct
targeting, and credentials supplied automatically — no per-playbook wiring.
It applies to **any** playbook in a configured source (bundled, the writable
local source, or a git source), including third-party ones.
---
## 1. Describe what it does — `# description:`
Steward shows a one-line description when a playbook is selected in the run form.
```yaml
---
# description: Reclaim disk on Docker/Swarm nodes by pruning unused images and build cache.
- name: Docker system prune
hosts: all
...
```
- Format: a comment line `# description: <text>` anywhere in the file. **First
match wins.** Case-insensitive on the `description:` key.
- Why a comment and not a key: Ansible rejects unknown *play* keys (you can't add
`description:` to a play), so a comment is the portable place. It survives
`ansible-playbook` untouched.
- Fallback: if there's no `# description:` comment, Steward uses the **first
play's `name:`**. So always give your play a meaningful `name:` even without
the comment.
- Keep it to one readable line. Longer "how to use" notes can go in additional
normal comments — Steward only reads the `description:` line.
## 2. Declare tunables in `vars:` — they become fill-in fields
Every **scalar** entry in a play's `vars:` block becomes an editable field in the
run form, with the default shown as the input's placeholder.
```yaml
vars:
prune_all_images: false # → checkbox-ish text field, placeholder "default: false"
keep_last_days: 7 # → field, placeholder "default: 7"
registry_url: "" # → field, placeholder "no default"
```
- **Blank field = use the default.** Steward only sends fields the operator
actually fills, so an untouched field falls through to the playbook/inventory
default rather than overriding it.
- Only scalars (string/int/float/bool) surface as fields. Lists/dicts are
skipped — set those in the playbook or via inventory group/host vars.
- Values are delivered as **extra-vars** (`-e`), which are the **highest**
precedence in Ansible — they override the `vars:` defaults. (This is why the
default can be empty and still be safely overridden at run time.)
### `vars_prompt:` also works
Steward reads `vars_prompt` too. Use it when you want an explicit prompt or a
required value:
```yaml
vars_prompt:
- name: release_tag
prompt: "Which release to deploy?" # shown as the field label
# no default → Steward marks the field REQUIRED
- name: admin_password
prompt: "Admin password"
private: true # → masked field, never stored
```
## 3. Secrets — name them so they're masked and not persisted
A field is treated as **secret** (rendered masked, and its value is **never
written to the DB / run history**) when either:
- the variable name contains `password`, `passwd`, `secret`, `token`,
`api_key` / `apikey`, `private_key`, or `credential` (case-insensitive), **or**
- it's a `vars_prompt` entry with `private: true` (Ansible's default for
vars_prompt is private).
So name sensitive variables accordingly (`db_password`, `api_token`,
`vault_secret`) and they're handled safely with no extra config. Non-secret
run-time vars are persisted (so scheduled runs can reuse them); secret ones are
passed to the run only.
## 4. Target with `hosts: all`
Steward builds the inventory itself from the **target / group** the operator
picks in the run form (or the single host on a host page). Your play should:
```yaml
hosts: all # run against whatever Steward scoped to
```
- Don't hardcode hostnames or rely on a checked-in inventory for Steward runs
(Steward generates a fresh inventory per run).
- Per-host connection vars (`ansible_host`, plus anything you set on the target
in **Ansible → Inventory**) arrive as inventory host vars.
- The run form's **Limit** / **Tags** map to `--limit` / `--tags`.
## 5. Privileges & connection — don't put credentials in the playbook
Steward supplies SSH and become for you:
- Steady-state runs connect as the managed **`steward`** account using Steward's
managed key; that account has **passwordless sudo**. So just use
`become: true` where you need root.
- First-contact provisioning uses a one-time bootstrap user/password the
operator enters (never stored).
- Never embed SSH keys, passwords, or `ansible_user`/`ansible_ssh_pass` in the
playbook. Connection identity is global (Settings → Ansible) or per-target.
## 6. Be idempotent
Steward re-runs playbooks (updates, schedules, retries). Use modules that
converge (state-based) rather than ad-hoc `command:`/`shell:` where possible, so
re-runs are safe.
---
## What Steward reads from a playbook (summary)
| Source in the playbook | What Steward does with it |
|---|---|
| `# description: <text>` comment | Description shown on selection (first match) |
| first play `name:` | Description fallback |
| `vars:` scalar entries | Run-time fill-in fields (placeholder = default) |
| `vars_prompt:` entries | Run-time fields (required if no default) |
| secret-looking var name / `private: true` | Field masked + value not persisted |
| `hosts:` | Expected to be `all`; Steward provides the inventory |
Everything else (SSH user/key, become password, the inventory, `steward_token`
etc. for the agent playbooks) is injected by Steward at run time.
## Metadata convention & extensibility
Today the **only comment-based metadata** Steward parses is `# description:`.
Everything else is *structural* (`vars`, `vars_prompt`, `hosts`, the play
`name`). If we add more comment metadata later, it will use a reserved
**`# steward:<key>: <value>`** namespace (e.g. `# steward:category: maintenance`,
`# steward:confirm: true` for a run-confirmation gate) so it can't collide with
ordinary comments. **These extra keys are not implemented yet** — only
`# description:` is. Don't author playbooks that depend on `# steward:*` keys
until this doc says they're live.
## Minimal annotated example
```yaml
---
# description: Restart a systemd service and confirm it came back up.
- name: Restart a service
hosts: all
become: true
gather_facts: false
vars:
service_name: "" # required-ish: operator fills it in the run form
tasks:
- name: Validate input
ansible.builtin.assert:
that: service_name | default('') | length > 0
fail_msg: "Set service_name."
- name: Restart
ansible.builtin.systemd:
name: "{{ service_name }}"
state: restarted
- name: Confirm active
ansible.builtin.command: "systemctl is-active {{ service_name }}"
changed_when: false
```