Turn comments into help screens: Ansible playbooks and roles
ansible-helpreads the comments, argument specs, and role metadata a repo already has, and prints what each playbook and role does, what it takes, and what is still open. For engineers who run or maintain playbooks somebody else wrote.
To learn what a role expects, you read its meta/argument_specs.yml, its defaults/, the play that calls it, and every comment that says "do not remove this".
In the demo repo that ships with this README, the cert_rotation role keeps 7 of those comments in 5 different directories.
ansible-help cert_rotation prints all of them, with the inputs table and the header's intent, on one screen.
The documentation is already in the files; the work this tool does is reading it back out.
Install
pipx install ansible-help # or: python -m pip install ansible-help
ansible-help # run from anywhere inside an Ansible repo
ansible-help <playbook-or-role> # show one entry
ansible-help -a <playbook-or-role> # also every other comment and the README section
ansible-help --check # report comments and docs that drifted from the code
ansible-help --help # the guide: what it reads, why, how to add docs it picks up
House-style header support, play argument specs, -a, --check, and the --help guide (below) are newer than the 1.0.5 release on PyPI.
Until the next release ships, install from a checkout to get it:
Prereqs: Python 3.10+.
git clone https://github.com/fullsteam-corp-shared/pypi-ansible-help.git
cd pypi-ansible-help
python -m pip install -e . # console script ansible-help -> ansible_help.cli:main
cd examples/demo-repo && ansible-help # try it on the demo repo
That is the entire setup.
The package has no runtime dependencies, no configuration file, and two flags besides help: -a / --all and --check.
-h prints the option list; --help prints a guide to what the tool reads, why, how to use it, and how to add docs it picks up, ending with where to extend cli.py.
Why this exists
Comment headers in this house style have 14 fixed sections, and they are the first thing in every file.
To ansible-help 1.0.5 they were noise: it read the first sentence of rotate_certs.yml: renew the ... as rotate_certs., found no Inputs: section, and printed 57 lines of raw header with no inputs table.
The low-leverage fix is a second, shorter docblock written for the tool, which drifts from the real header within a release.
The high-leverage fix is a parser that reads the header people already maintain, so the help screen and the code review read the same text.
$ ansible-help playbooks/rotate_certs.yml # ansible-help 1.0.5
rotate_certs
PURPOSE
Provides the operator entry point for certificate renewal, so nobody renews a certificate by
hand on one host and forgets the others.
INTENDED BEHAVIOR
Dry-run by default: report which certificates are inside the renewal window and stop. With
cert_dry_run=false, request new certificates, install them, and reload nginx once per host.
Where it looks
ansible-help walks up from the current directory until it finds ansible.cfg, and treats that directory as the repo root.
Without an ansible.cfg it stops with error: Could not locate repo root (expected ansible.cfg).
It then looks in these places, in this order:
| Order | Where | When |
|---|---|---|
| 1 | playbooks/*.yml and roles/*/ under the repo root |
always |
| 2 | each entry of collections_path in ansible.cfg (: or ; separated; relative entries resolve against the repo root, ~ expands) |
when collections_path is set |
| 3 | ~/.ansible/collections |
only when collections_path is unset, matching Ansible's default |
| 4 | ./collections |
when it exists, for older repo-local layouts |
In rows 2 to 4, playbooks and roles are read from <root>/ansible_collections/<ns>/<coll>/playbooks/ and .../roles/.
Row 2 is the one most repos depend on: the tool reads the same collections_path that ansible-playbook does, so a name that resolves for one resolves for the other.
Each item is listed under one name, and show mode accepts a few more:
| Item | Listed as | Also accepted by show mode |
|---|---|---|
| Local playbook | playbooks.<file>.yml |
playbooks.<file>, the file path, and the bare <file> when no role has that name |
| Local role | roles.<name> |
<name> |
| Collection playbook or role | <ns>.<coll>.<name> |
nothing else |
Whichever name you type for a local playbook, show mode prints the path ansible-playbook accepts, so the usage line can be pasted as is:
$ ansible-help scale_web
Playbook: playbooks/scale_web.yml
ansible-playbook playbooks/scale_web.yml [-e "[replicas=]"]
replicas OPTIONAL Defaults to 3. Aliases: count. Inferred from set_fact input-resolution task (heuristic).
ansible-help playbooks.scale_web.yml prints the same lines.
A bare name is tried only after every playbook and role form has failed, so a role named scale_web would win.
What the list shows
Run with no arguments, ansible-help lists every playbook and role it can resolve, sorted, with the one-line description it would print in show mode.
$ ansible-help
For more information on a specific playbook or role, type ansible-help <name>
Playbooks:
---------------
acme.ops.backup_db Take a compressed logical backup of one database.
acme.ops.purge_cache Purge the CDN cache for the web tier.
playbooks.deploy_web.yml Deploy a release of the web tier to one environment.
playbooks.provision_web.yml Provision a new web host and serve static files from one document root.
playbooks.restart_web.yml
playbooks.rotate_certs.yml Renew the web tier's TLS certificates before they expire.
playbooks.scale_web.yml
Roles
---------------
acme.ops.db_backup Dump, restore-test, and upload a database backup.
roles.cert_rotation Renew and install TLS certificates for the web tier.
roles.webserver Install and configure the nginx web tier.
| Left out | Rule | Why |
|---|---|---|
A collection playbook whose top docblock has a line cli_help_hidden: true (or cli_help_hidden=true) |
explicit opt-out | CI fixtures and internal plays stay runnable but unlisted |
A collection playbook that only does import_playbook and has no description |
wrapper-only, nothing to say | a templated target such as "{{ target_playbook }}.yml" cannot be followed |
| A local playbook that a collection playbook imports | listed once, under the collection name | the collection wrapper shows the local file's description, one hop deep |
Descriptions longer than the terminal are cut with ..., so a narrow terminal shows less of each line.
Docblocks: the original convention
A top-of-file # comment block is read as a docblock.
Its first sentence is the description, Name: lines start sections, and the input sections become a table.
Every convention here predates house-style support and still works; a file is only read the new way when it has a house header.
---
# Deploy a release of the web tier to one environment.
#
# Rolls hosts in batches behind the load balancer, so a bad release stops at the
# first batch instead of reaching every host.
#
# Inputs:
# - web_env (required, staging or production)
# - release_tag or tag (required, git tag to deploy)
# - batch_size (optional, defaults to 1, hosts updated per batch)
# - overwrite (optional, replace an existing release directory)
#
# Examples:
# ansible-playbook playbooks/deploy_web.yml -e "web_env=staging release_tag=v2.4.0"
# ansible-playbook playbooks/deploy_web.yml \
# -e "web_env=staging release_tag=v2.4.0 overwrite=true"
#
# Tags:
# --tags config
# --skip-tags restart
#
# Rollback:
# Re-run with the previous release_tag. Old release directories are kept.
$ ansible-help playbooks/deploy_web.yml
Deploy a release of the web tier to one environment
Rolls hosts in batches behind the load balancer, so a bad release stops at the
first batch instead of reaching every host.
ansible-playbook playbooks/deploy_web.yml [-e "[web_env=] [release_tag=] [batch_size=] [overwrite=true]"]
web_env REQUIRED Staging or production
release_tag REQUIRED Alias: tag. Git tag to deploy
batch_size OPTIONAL Defaults to 1. Hosts updated per batch
overwrite OPTIONAL Replace an existing release directory
Examples:
ansible-playbook playbooks/deploy_web.yml -e "web_env=staging release_tag=v2.4.0"
ansible-playbook playbooks/deploy_web.yml -e "web_env=staging release_tag=v2.4.0 overwrite=true"
Options (tags):
--tags config
--skip-tags restart
Rollback:
Re-run with the previous release_tag. Old release directories are kept.
| Docblock element | Read as | Printed as |
|---|---|---|
| First sentence-like line | description, up to the first . followed by a space or the line end, so 24.04, README.md, and 3.5 do not cut it short |
the first output line, trailing . dropped |
==== banners, ns.coll.name identifier lines, single-word headings, CLI_HELP_INPUTS / END_CLI_HELP_INPUTS / Canonical CLI lines |
skipped when picking the description | |
Inputs:, Required inputs:, Command Line Arguments:, Parameters: |
the first of these present | the inputs table and the ansible-playbook ... [-e "..."] line |
Examples:, Example:, Run:, Run from repo root (.env and tool resolution rely on $PWD): |
ansible-playbook commands, wrapped lines joined |
Examples: |
Tags: |
lines starting -- |
Options (tags): |
Any other Name: section |
context | printed under its own heading |
cli_help_hidden: true |
list opt-out | nothing; see the list rules above |
Input lines take two forms, and a section should use one of them:
| Form | Example | Row |
|---|---|---|
| Bullet with qualifiers | - release_tag or tag (required, git tag to deploy) |
release_tag REQUIRED Alias: tag. Git tag to deploy |
Name, qualifiers, then - description (bullet optional) |
db_name (required) - Database to dump. |
db_name REQUIRED Database to dump |
Qualifiers are comma separated: required or optional (an entry is REQUIRED unless it says optional), defaults to <value>, and free text that becomes detail.
An input named overwrite is shown as [overwrite=true] in the invocation line.
When a playbook has no docblock
A play argument spec, when the playbook has one, comes before the input section (see Play argument specs). With no input section, show mode falls back to heuristics, in this order:
| Heuristic | Trigger | Detail column says |
|---|---|---|
| Role argument specs | the play's only task is ansible.builtin.include_role |
the role's meta/argument_specs.yml options |
set_fact input resolution |
a task sets <name>_input and matches item.key | lower == '...' or in [...] |
Inferred from set_fact input-resolution task (heuristic). |
include_role var mapping |
selectattr('key', 'match', '(?i)^name$') in a thin wrapper |
Mapped to include_role vars from CLI/runtime vars (heuristic). |
roles: list and play vars: |
none of the above found an input, and the play lists roles or sets literal vars | each listed role's argument_specs options, then its meta/main.yml dependencies' options (each row ending From role <name>), then play vars as Defaults to <value> (set by the play) |
The last row only fills a table that would otherwise be empty, so a playbook that already printed inputs prints the same ones.
A trailing comment on a play var line becomes part of its row: common_action: plan # override with -e 'common_action=apply' reads Defaults to plan (set by the play): override with -e 'common_action=apply'.
Play vars whose names start with _ are play-private helpers and are not listed as inputs.
Vars that an include_role task in the play passes itself are dropped, since the caller cannot set them.
$ ansible-help playbooks/restart_web.yml
Playbook: playbooks/restart_web.yml
ansible-playbook playbooks/restart_web.yml [-e "[web_env=] [web_port=] [web_worker_count=]"]
web_env REQUIRED Environment the host serves, staging or production
web_port OPTIONAL Defaults to 8080. Port nginx listens on behind the load balancer
web_worker_count OPTIONAL Defaults to 4. nginx worker processes per host
$ ansible-help playbooks/scale_web.yml
Playbook: playbooks/scale_web.yml
ansible-playbook playbooks/scale_web.yml [-e "[replicas=]"]
replicas OPTIONAL Defaults to 3. Aliases: count. Inferred from set_fact input-resolution task (heuristic).
Roles
A role's description is meta/main.yml galaxy_info.description when present.
Otherwise it is the first line of the docblock in tasks/main.yml (or meta/main.yml), and meta/argument_specs.yml short_description only when both are missing.
Inputs come from meta/argument_specs.yml options first, including multi-line, folded, and list descriptions, then from a docblock input section.
A role that documents no inputs of its own gets the options of its meta/main.yml dependencies, followed depth first, with each row ending in From role <name>.
Examples come from the docblock; with none, the tool prints a generic ansible-playbook <playbook> -e <first input>=<value> line.
$ ansible-help roles.webserver
Install and configure the nginx web tier
ansible.builtin.include_role name=webserver vars: [[web_env=] [web_port=] [web_worker_count=]]
web_env REQUIRED Environment the host serves, staging or production
web_port OPTIONAL Defaults to 8080. Port nginx listens on behind the load balancer
web_worker_count OPTIONAL Defaults to 4. nginx worker processes per host
Renders one server block per host and reloads nginx only when the rendered
configuration changed.
Examples:
ansible-playbook playbooks/deploy_web.yml -e "web_env=staging release_tag=v2.4.0"
House-style headers
A file is read as house style when its top comment block contains at least 2 of the 14 section headings, each followed by a dashed underline.
For a role, the header is looked for in tasks/main.yml, then meta/main.yml, and the description prefers galaxy_info.description, then argument_specs short_description, then the header title.
Everything else goes through the docblock parser above, so a repo can migrate one file at a time.
| Header part | Printed as | Notes |
|---|---|---|
<file> (context): responsibility. title line |
the description | file prefix removed, first letter capitalized |
PURPOSE |
the paragraph under the description | |
| Inputs table | the ansible-playbook or include_role line and table |
from a play argument spec (below), argument_specs, bullet lines under INPUTS, the heuristics above, or the roles in a play's roles: list and their meta/main.yml dependencies; literal play vars: become optional inputs with the play's default, and vars an include_role task passes itself are dropped |
Any ansible-playbook ... line, in any section |
Examples: |
house headers keep commands in MAINTENANCE, so the tool looks everywhere |
| Every other section, in header order | its own sentence-case heading (Intended behavior:, Implementation overview:, Key elements:, ...), body verbatim, after the examples; INPUTS prints as Inputs (from header): |
a section whose body is only UNKNOWN, NOT APPLICABLE, or REQUIRES FURTHER REPOSITORY CONTEXT is not printed |
$ ansible-help playbooks/rotate_certs.yml
Renew the web tier's TLS certificates before they expire
Provides the operator entry point for certificate renewal, so nobody renews a certificate by
hand on one host and forgets the others.
ansible-playbook playbooks/rotate_certs.yml [-e "[cert_domain=] [cert_renew_days_before=] [cert_dry_run=]"]
cert_domain REQUIRED Domain the certificate is issued for
cert_renew_days_before OPTIONAL Defaults to 21. Renew when fewer than this many days remain
cert_dry_run OPTIONAL Defaults to true. Report what would be renewed and change nothing
Examples:
ansible-playbook playbooks/rotate_certs.yml -e "cert_domain=shop.example.com"
ansible-playbook playbooks/rotate_certs.yml -e "cert_domain=shop.example.com cert_dry_run=false"
Intended behavior:
Dry-run by default: report which certificates are inside the renewal window and stop. With
cert_dry_run=false, request new certificates, install them, and reload nginx once per host.
Implementation overview:
One play over the web group runs the cert_rotation role, which checks expiry, renews, installs,
and notifies the reload handler.
Inputs (from header):
cert_domain is required. cert_renew_days_before and cert_dry_run have role defaults.
Outputs and side effects:
In dry-run, prints the certificates due for renewal and changes nothing. Otherwise writes new
certificate and key files under /etc/ssl/web and reloads nginx.
Key elements:
The play "Rotate web tier certificates" and its cert_rotation role.
Callers and consumers:
Operators run this with ansible-playbook. A scheduled job is planned but does not exist yet.
Internal dependencies:
roles/cert_rotation.
External dependencies:
The ACME endpoint named in roles/cert_rotation/vars/main.yml, and nginx on every web host.
Constraints and assumptions:
The web hosts must reach the ACME endpoint on 443. Run against one environment at a time.
Maintenance:
Keep these operator commands aligned with the role defaults:
ansible-playbook playbooks/rotate_certs.yml -e "cert_domain=shop.example.com"
ansible-playbook playbooks/rotate_certs.yml \
-e "cert_domain=shop.example.com cert_dry_run=false"
Hazards and open items:
playbooks/rotate_certs.yml:69
⚠ serial: 1 is NOT a leftover from debugging. Renewing every host at once reloads every nginx
at once, and the load balancer has no healthy backend for the length of the reload.
The header's INTEGRATION AND LINKAGE, DATA FLOW, and DEPENDENCY RELATIONSHIPS sections hold only UNKNOWN or NOT APPLICABLE, so they are the three left out.
Compare it with the 1.0.5 output under Why this exists: same file, 67 lines instead of 57, with the inputs table and examples the old output never found and without the placeholder sections it printed.
Hazards and open items
In a house-style playbook or role, comments that start with a status marker are collected under Hazards and open items:, each with its path:line.
For a role, the tool reads every YAML file under tasks/, handlers/, defaults/, vars/, and meta/.
For a playbook, it reads the playbook file itself.
Files without a house header get no hazards section, which keeps their output as it was.
| Marker | Means |
|---|---|
⚠ (including ⚠ STILL OPEN) |
a hazard, or a guard that must not be removed |
✅ (✅ ANSWERED, ✅ HALF-ANSWERED) |
a question closed, with the evidence |
PARKED |
a known gap, deferred on purpose |
DELIBERATE |
looks like an omission, is not |
NOT OPTIONAL |
a step the system is broken without |
A marker only counts at the start of a comment line, so a ✅ inside a task's msg: string is not a hazard.
An entry runs until a blank # line, the next marker, a banner, or code.
$ ansible-help cert_rotation
Hazards and open items:
roles/cert_rotation/tasks/main.yml:70
⚠ STILL OPEN (2026-09-30): hosts with no certificate yet fail the task above instead of being
treated as due. Decide whether first issuance belongs in this role before relying on it for a
new host.
roles/cert_rotation/tasks/renew.yml:7
✅ ANSWERED (2026-09-22): the ACME client keeps the old key when asked to, so a renewal does not
invalidate any pinned public key. Verified against the staging endpoint.
roles/cert_rotation/tasks/renew.yml:18
NOT OPTIONAL: nginx reads the key as a non-root worker. Without this mode the reload succeeds
and the next TLS handshake fails.
roles/cert_rotation/handlers/main.yml:2
DELIBERATE: reload, not restart. A restart drops in-flight connections; a reload picks up the
new certificate without dropping them.
Play argument specs: validate_argspec
ansible-core 2.20 added argument validation for plays, as a tech preview.
A play opts in with the validate_argspec keyword, and its spec lives in <playbook>.meta.yml next to the playbook.
ansible-playbook validates the play's variables against that spec before the first task, so the spec cannot quietly disagree with what a run accepts.
ansible-help reads the same file, so the inputs table it prints is the one ansible-core enforces.
---
- name: provision web host
hosts: web
validate_argspec: true
roles:
- webserver
tasks:
- name: Create the document root
ansible.builtin.file:
path: "{{ web_doc_root }}"
state: directory
mode: "0755"
---
argument_specs:
provision web host:
description: Provision a new web host and serve static files from one document root.
options:
web_env:
description: Environment the host serves, staging or production.
type: str
required: true
choices: [staging, production]
web_doc_root:
description: Directory nginx serves static files from.
type: str
default: /srv/www
$ ansible-help playbooks/provision_web.yml
Playbook: playbooks/provision_web.yml
Provision a new web host and serve static files from one document root.
ansible-playbook playbooks/provision_web.yml [-e "[web_env=] [web_doc_root=] [web_port=] [web_worker_count=]"]
web_env REQUIRED Environment the host serves, staging or production
web_doc_root OPTIONAL Defaults to /srv/www. Directory nginx serves static files from
web_port OPTIONAL Defaults to 8080. Port nginx listens on behind the load balancer
web_worker_count OPTIONAL Defaults to 4. nginx worker processes per host
Validated by ansible-core 2.20+ before the first task: validate_argspec "provision web host" in playbooks/provision_web.meta.yml
| In the files | Read as |
|---|---|
validate_argspec: true |
the spec named after the play's name, as ansible-core does |
validate_argspec: <name> |
that spec; a templated value ({{ ... }}) cannot be resolved without a run, so it is skipped |
<playbook>.meta.yml, then .meta.yaml, then .meta |
the meta file, in ansible-core's lookup order |
The selected spec's options |
the inputs table, ahead of a header INPUTS section or a docblock Inputs: section; rows for the roles in the play's roles: list follow, because -e still sets them |
The spec's description, else the meta file's top-level description |
the description, when the header or docblock gives none; list mode uses it too |
A selected spec that is missing or has no options mapping |
a Warning: line under the table, because ansible-playbook stops with a parser error at that play |
| The meta file itself | never listed as a playbook; with -a, its comments are printed after the playbook's |
--check reports a bad selector as argspec-missing on its validate_argspec line, and an option with no description as spec-no-description.
A play spec's options count as defined, so a role var that the play spec declares is not reported as undeclared-input.
Playbooks without validate_argspec print exactly what they printed before.
Every other comment: -a
The default screen is for running a playbook; the comments that explain why a task looks the way it does stay in the file.
ansible-help -a <playbook-or-role> (or --all, before or after the name) prints the default output unchanged, then appends two parts.
All comments: is every comment in the files, verbatim, under its path:line: for a playbook, the playbook file; for a role, every YAML file under tasks/, handlers/, defaults/, vars/, and meta/.
After it come the README docs that describe the entry, under From <path>:.
| In the files | With -a |
|---|---|
A run of # lines |
printed verbatim from its first line; a bare # keeps the paragraph break, banner lines are dropped |
| A trailing comment on a code line | printed with its code line, so the note keeps its subject |
| The house header and marked comments | skipped, because they are already on screen |
Lint directives (# noqa, # yamllint, # nosec, # pylint, # type:, # pragma, # fmt:) |
dropped; they are for tools, not readers |
README.md next to the playbook, then the repo-root README.md |
the section whose heading names the playbook file, down to the next heading at its level or above; # lines inside a fenced block are not headings |
A role's README.md |
printed in full |
In the demo playbook every header line and the one marked comment are already on screen, so All comments: holds a single inline comment, followed by the rotate_certs.yml section of playbooks/README.md.
The first 62 lines are the default output shown above:
$ ansible-help -a playbooks/rotate_certs.yml
Hazards and open items:
playbooks/rotate_certs.yml:69
⚠ serial: 1 is NOT a leftover from debugging. Renewing every host at once reloads every nginx
at once, and the load balancer has no healthy backend for the length of the reload.
All comments:
playbooks/rotate_certs.yml:74
gather_facts: false # the role reads expiry with openssl on the host, so facts are never used
From playbooks/README.md:
## rotate_certs.yml
Run it once without `cert_dry_run=false` and read the list of certificates due. Then run it
again with `cert_dry_run=false` inside a change window:
```bash
ansible-playbook playbooks/rotate_certs.yml -e "cert_domain=shop.example.com cert_dry_run=false"
```
A host that has never had a certificate fails the expiry check. First issuance is still an open
question in the `cert_rotation` role, so issue that host's first certificate by hand.
A file without a house header skips nothing, so its docblock appears twice: once parsed at the top, once verbatim under All comments:.
Find drift: --check
Help text read from comments is only as good as the comments, and comments drift: a renamed input stays in the docblock, a deleted file stays in a note, a header section stays UNKNOWN.
ansible-help --check reads the same files the help screens come from and reports each place where the comments or docs no longer match the code.
It exits 1 on any finding and 0 when clean, so it can gate a pull request the same way a linter does.
With no target it checks every local and collection playbook and role.
ansible-help --check <playbook-or-role> checks one entry; a playbook target also checks the roles in its roles: list.
Each finding is one compiler-style line, <path>:<line>: <kind>: <message>, so editors and CI annotators can jump to it.
| Kind | Reported when |
|---|---|
undeclared-input |
a role's YAML files or templates read a var with | default(...), and nothing in the repo defines it (vars and defaults keys, inventory keys, set_fact, register, loop_var, vars: blocks, any role's argument_specs, any play argument spec) and it is not an Ansible magic var: only -e or inventory can set it, and argument_specs does not declare it. Reported once per var per role |
documented-unused |
an input named in a house INPUTS bullet, a docblock Inputs: / Parameters: bullet, or the backticked first column of a parameters table in the playbook's README section appears in no YAML or .j2 code |
stale-path |
a comment names a file path, or a bare *.yml file name, that exists nowhere in the git repo; state/, .tofu-data/, .terraform/, node_modules/, and virtualenvs are skipped without being opened |
spec-no-description |
an option in a role's argument_specs or a play argument spec has no description |
argspec-missing |
a play's validate_argspec selects a spec that <playbook>.meta.yml does not have, or that has no options mapping, or the meta file is missing; ansible-playbook stops with a parser error at that play |
header-missing-section, header-placeholder |
a house header lacks one of the 14 sections, or a section still says UNKNOWN or REQUIRES FURTHER REPOSITORY CONTEXT; NOT APPLICABLE is an answer and is not reported |
The demo repo is not clean, on purpose: its playbooks are stubs, so two docblocks list inputs that no task reads, and two headers still have an unanswered section.
$ ansible-help --check
Checked 10 playbook(s), 3 role(s)
collections/ansible_collections/acme/ops/playbooks/backup_db.yml:11: documented-unused: backup_bucket is documented as an input in the docblock but no code uses it
collections/ansible_collections/acme/ops/playbooks/backup_db.yml:12: documented-unused: retention_days is documented as an input in the docblock but no code uses it
playbooks/deploy_web.yml:9: documented-unused: release_tag is documented as an input in the docblock but no code uses it
playbooks/deploy_web.yml:11: documented-unused: overwrite is documented as an input in the docblock but no code uses it
playbooks/rotate_certs.yml:45: header-placeholder: INTEGRATION AND LINKAGE is still a placeholder (UNKNOWN)
roles/cert_rotation/tasks/main.yml:43: header-placeholder: INTEGRATION AND LINKAGE is still a placeholder (REQUIRES FURTHER REPOSITORY CONTEXT)
6 finding(s)
A clean entry prints the count and exits 0:
$ ansible-help --check roles.webserver
Checked 0 playbook(s), 1 role(s)
0 finding(s)
check.py checks those exit codes along with the output: 1 for the whole demo repo and for --check playbooks/rotate_certs.yml, 0 for --check roles.webserver.
The loop
Every block of output in this README, and every line of output in the figures, is a capture from a real run against examples/demo-repo/.
check.py re-runs them and fails when anything drifted.
python3 examples/demo-repo/expected/check.py # re-run every capture; check figure text and README blocks
python3 examples/demo-repo/expected/check.py --update # accept new output after an intended change
python3 images/build_figures.py # redraw the figures from the captures
python3 -m build # the package build CI runs on every PR
After a change to src/ansible_help/cli.py, run check.py first.
A changed capture is either the change you meant, which you accept with --update and then rebuild the figures, or a regression in a file format you did not mean to touch.
The legacy files in the demo repo (deploy_web.yml, backup_db.yml, the heuristic playbooks, webserver) are the backward-compatibility fixture: their captures should not move when house-style parsing changes.
Releases are automated.
One workflow, version-control.yml, runs action-version-control v2, which picks its step from the pull request event.
On a pull request to main it checks that every commit is a conventional commit.
Each merge rebuilds a release/next pull request from the merged commits, with the changelog and the version in pyproject.toml and src/ansible_help/__init__.py bumped.
Merging release/next tags the release, creates the GitHub Release, and publishes to PyPI.
ci.yml runs the tests and the package build on every pull request.
Folder structure
pypi-ansible-help/
├── README.md # this file; also the PyPI long description
├── pyproject.toml # package metadata, console script, version
├── CHANGELOG.md # generated by the release workflow
├── .changes/ # per-version changelog fragments
├── .github/workflows/ # ci (tests, build), version-control (commit gate, release PR, PyPI)
├── src/ansible_help/
│ ├── __init__.py # __version__
│ └── cli.py # the entire tool
├── examples/demo-repo/ # a small Ansible repo every README capture comes from
│ ├── ansible.cfg # collections_path = ./collections
│ ├── playbooks/ # docblock, house-style, no-docblock, and validate_argspec playbooks; README.md for -a
│ ├── roles/ # webserver (docblock), cert_rotation (house style)
│ ├── collections/ # acme.ops: wrappers, a hidden playbook, a role
│ └── expected/ # captured output and check.py
└── images/ # animated SVG figures and the script that draws them
Make it your own
Treat this as a reader for whatever conventions your repo already follows.
Four places to extend it: add a section name to the docblock tables in cli.py when your team uses a different heading for inputs; add a marker to STATUS_MARKER_RE when your team flags hazards with another word; add a prefix to LINT_DIRECTIVE_RE when -a prints a tool directive your repo uses; copy examples/demo-repo/ and its check.py pattern into your own repo so your playbooks' help output is checked the same way this README is.
ansible-help --help ends with where in cli.py a new input source or a new --check finding goes.
What transfers when the tool changes is the comments: a header and a marked hazard still document the file for the next person who opens it, with or without ansible-help.
Where it can still fail
- It reads files, not runs. Variables set in inventory,
group_vars, or at runtime are invisible. Treat the inputs table as what the author documented, andansible-playbook --syntax-checkplus the role'sargument_specsvalidation as the truth. - YAML is read line by line, with regular expressions, not with a YAML parser. Unusual indentation, flow-style mappings, or anchors in
argument_specs.ymlandmeta/main.ymlcan be missed. If an input is missing from the table, check that file's layout first. - Mixing the two input-line forms in one section reorders the table. Plain bullets keep their place at the top, and lines in the
name (required) - descriptionform are appended after them. Use one form per section to control the order. - Imports are followed one hop. A wrapper that imports a wrapper, or imports a templated path, shows its own docblock or nothing.
cli_help_hidden: trueonly hides collection playbooks. Local playbooks underplaybooks/are always listed.- A playbook's hazards and
-acomments come only from the playbook file. Markers and comments in the roles it runs show up underansible-help <role>andansible-help -a <role>, not under the playbook. -afinds playbook docs only under a heading that names the file. A section titled "Certificate rotation" is not matched; putrotate_certs.ymlin the heading, asplaybooks/README.mdin the demo repo does.--checkmust be the first argument.ansible-help --check cert_rotationworks;ansible-help cert_rotation --checkexits 2 witherror: unsupported arguments.--checkexits 1 for an unresolvable target as well as for findings. A CI step cannot tell the two apart by exit code; the target error goes to stderr aserror: could not resolve playbook or role target: <name>.undeclared-inputtreats a var defined anywhere in the repo as defined. A var that an unrelated role or an inventory file happens to define hides a real gap in this role'sargument_specs. Read the role's| default(...)reads yourself when an input looks missing.documented-unusedis word-based. A documented name that appears anywhere in YAML or template code, as a key, in a string, or in another role, counts as used, so it misses an input that only shares a name with something else.stale-pathmatches by path suffix or file name across the whole git repo. A comment namingvars/main.ymlpasses as long as anyvars/main.ymlexists, even in another role. The repo is the nearest directory with.git, so inside a monorepo that is the whole monorepo.- A playbook target checks only the roles in its
roles:list. Roles a play reaches throughinclude_roleorimport_roleare not checked with it;ansible-help --check playbooks/restart_web.ymlchecks 0 roles. Run--checkon the role, or with no target. - Play argument validation is a tech preview in ansible-core 2.20. The keyword or the meta file layout can still change upstream.
ansible-helpdoes not read a.meta.jsonfile, and skips avalidate_argspecvalue that is templated. - List mode clips descriptions to the terminal width. Set
COLUMNSfor a stable width in scripts, ascheck.pydoes.
License
MIT, as declared in pyproject.toml.
Built so the help screen and the code review read the same comments.
Metadata
Release files for ansible-help 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ansible_help-1.1.0.tar.gz | 77.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ansible_help-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 120.2 kB
Release files / ansible_help-1.1.0.tar.gz
| Download URL | ansible_help-1.1.0.tar.gz |
|---|---|
| Size | 77.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
61f0edbc055e168ced3f94185b0fdcdd19eba71ca46bf19da1242f35c366fcca
|
|
BLAKE2b-256 checksum How to use checksums |
00995e8c480df56f218c4116d2262a63c3cb8e02b015b5f2b09ba730302c5044
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / ansible_help-1.1.0-py3-none-any.whl
| Download URL | ansible_help-1.1.0-py3-none-any.whl |
|---|---|
| Size | 42.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eef6258f340649006d527e19752fff4823d04269fb20e356eb414908bb043e08
|
|
BLAKE2b-256 checksum How to use checksums |
598f747f572af8250d7ef2e472b350631b1647e8d8f56edb95d8da265bab5631
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|