Skip to main content

DocSmith for Ansible

Automating role documentation (using argument_specs.yml)

DocSmith is a documentation generator. It reads a role's meta/argument_specs.yml and produces up‑to‑date variable descriptions for the README.md as well as inline comment blocks for defaults/main.yml (or other role entry-point files). It works with roles in both stand‑alone form and within collections.



Logo: DocSmith for Ansible

⭐ Found this useful? Support open-source and star this project:

GitHub repository


Table of contents

Demo

Roles using DocSmith

Screenshots

Screenshot: DocSmith CLI, help   Screenshot: DocSmith CLI, validate; Results for foundata.sshd.run   Screenshot: DocSmith CLI, generate dry run; Results for foundata.sshd.run   Screenshot: DocSmith CLI, generate; Results for foundata.sshd.run   Screenshot: Part of a README.md ToC, generated with DocSmith   Screenshot: Part of a README.md's main content describing role variables, generated with DocSmith

Features

  • Uses the argument_specs.yml from Ansible's built-in role argument validation as the source for documentation in READMEs and entry-point files.
  • Checks argument specs for structural errors and inconsistencies with entry-point defaults/. See validation for checks and severity levels.
  • Provides read-only checks and exit codes for CI/CD pipelines and pre-commit hooks.
  • Supports Markdown and reStructuredText.
  • Converts Ansible markup, such as C(...), O(...), V(...) and M(...), in descriptions to the target format.

Installation

PyPI package version

DocSmith needs Python ≥ v3.11. It is available on PyPI and can be installed with the package manager of your choice.

Using uv (recommended):

uv tool install ansible-docsmith

Alternatively, choose one of the following commands:

pip install ansible-docsmith
pipx install ansible-docsmith

The minimum Python version is 3.11 (rather than the newer 3.12 baseline used for most of our projects) so DocSmith runs on Debian 12 "Bookworm".

Usage

Preparations

  1. Create meta/argument_specs.yml for Ansible's role argument validation if it does not exist. Add description: to your variables to make the generated documentation useful. A complete specification also improves argument validation.

  2. Add the mandatory MAIN markers to your role's existing README.md where the variable descriptions should appear. Each ansible-docsmith generate run replaces all content between them:

    <!-- ANSIBLE DOCSMITH MAIN START -->
    <!-- ANSIBLE DOCSMITH MAIN END -->
    

    Optionally, add TOC markers inside a hand-written table of contents (ToC). These generate list entries for only the DocSmith-managed variable documentation:

    <!-- ANSIBLE DOCSMITH TOC START -->
    <!-- ANSIBLE DOCSMITH TOC END -->
    

    Alternatively, add TOC-FULL markers to generate a complete ToC of all README headings, including hand-written ones:

    <!-- ANSIBLE DOCSMITH TOC-FULL START -->
    <!-- ANSIBLE DOCSMITH TOC-FULL END -->
    

    Headings with an explicit anchor (like ## Usage<a id="usage"></a>) are linked exactly; for other headings, the anchor is derived from the heading text and validate emits a notice, as the derivation cannot be guaranteed to match your rendering platform for exotic titles.

The entry-point variable files in your role's defaults/ directory need no additional preparation. DocSmith adds or replaces formatted inline comment blocks above the variables defined there.

The marker contract in short:

  • All content between a START and END marker pair is owned by DocSmith and gets replaced on every generate run. Outside content is preserved unless you enable a whole-file Markdown formatter.
  • A lone marker is always an error, no matter the type: a START without its END (or vice versa) fails validation instead of guessing where the managed section ends. This applies to MAIN, TOC, TOC-FULL and the role-named markers in collection READMEs alike.
  • A missing README is fine: generate creates one from a basic skeleton, markers included. However, an existing README without the mandatory MAIN markers is a hard validation error, so DocSmith cannot accidentally overwrite hand-written content in a README that was never prepared for it.

Example files:

  • Markdown: README.md
  • reStructuredText: README.rst (difference to Markdown: .. comments, .. contents:: **Table of Contents** directive)

Generate or update documentation

The path defaults to the current directory. Project settings are discovered automatically; explicit CLI options override them.

# Safely preview changes without writing to files. No modifications are made.
ansible-docsmith generate /path/to/role --dry-run

# Check whether the documentation is up to date without writing files:
# exit code 1 (and a diff) if a run would change anything, 0 otherwise.
# Useful for CI/CD pipelines and pre-commit hooks.
ansible-docsmith generate /path/to/role --check

# Generate / update README.md and comments in entry-point files (like defaults/main.yml)
ansible-docsmith generate /path/to/role

# Show help
ansible-docsmith --help
ansible-docsmith generate --help

Advanced parameters:

# Generate / update only the README.md, skip comments for variables in
# entry-point files (like defaults/main.yml).
ansible-docsmith generate /path/to/role --no-defaults

# Generate / update only the comments in entry-point files (like defaults/main.yml),
# skip README.md
ansible-docsmith generate /path/to/role --no-readme

# Do not document nested options ("dict attributes") in the comments of
# entry-point files (like defaults/main.yml)
ansible-docsmith generate /path/to/role --no-defaults-comments-nested

# Verbose output for debugging
ansible-docsmith generate /path/to/role --verbose

Variables without defaults

Use generate --defaults-include-missing to append an owned comment section for required and optional top-level variables absent from the entry-point defaults mapping and without an explicit specification default:. Nested options stay under their parent; example values are not invented.

# ANSIBLE DOCSMITH MISSING START
#
# The following variables have no default values. They are documented
# here as comments for easier discovery:
#
# Server hostname.
#
# - Type: str
# - Required: Yes
# app_host:
#
# ANSIBLE DOCSMITH MISSING END

The option is off by default. Empty and comment-only files are supported. Missing defaults/<entry-point>.yml files and their directory are created only when there are eligible variables; existing .yaml files are reused. An explicit specification default: null is still a default, not a placeholder.

DocSmith regenerates this section in specification order. Removing a variable from the spec or assigning it outside the markers removes its placeholder. Keep assignments outside the section: malformed markers or active YAML inside it stop generation before any files are written.

--no-defaults-include-missing removes the owned section on the next generation, retaining the file. --no-defaults leaves defaults files entirely untouched. Set defaults_include_missing = true under [generate] to enable the feature through project configuration. It also applies to every role in a collection.

Project configuration

Use ansible-docsmith.toml or .ansible-docsmith.toml in the selected role or collection directory. Exactly one file is loaded: having both is an error. --config PATH selects an alternative file, relative to the caller's directory, and bypasses discovery. --no-config skips configuration entirely; it cannot be combined with --config.

There is no inheritance, ancestor search, user configuration or pyproject.toml support. A collection uses its root settings for every role, ignoring role-local files. When invoking a role directly, use --config to reuse the collection's configuration. CLI options take precedence over file settings, then built-in defaults. Unknown keys, invalid values and malformed files fail with exit 2, including when the formatter is disabled.

All behavior settings are shown below with their built-in defaults. Settings for generate and validate are independent; format and verbose apply to both.

format = "auto"
verbose = false

[generate]
readme = true
defaults = true
defaults_comments_nested = true
defaults_include_missing = false
readme_toc_list_bulletpoints = "auto"
dry_run = false
check = false
markdown_formatter = true
# template_readme = "templates/readme.md.j2"

[validate]
readme = true
argument_specs = true
strict = false

format accepts auto, markdown or rst; TOC bullets accept auto, * or -. Template paths in TOML are relative to that config file; --template-readme paths are relative to the caller's directory.

Positive/negative CLI options override configured booleans, including --no-check, --no-dry-run, --no-strict and --no-verbose. Check always implies dry-run.

ansible-docsmith generate
ansible-docsmith generate --dry-run
ansible-docsmith generate --check
ansible-docsmith validate
ansible-docsmith generate --config ./alternative.toml

Markdown formatter

Formatting is off unless [markdown_formatter] declares a command. DocSmith formats the whole merged README, including hand-written content, before comparing or writing. Generate, check and dry-run use the same result; the original file is never formatted merely to make a comparison pass. YAML defaults and reStructuredText files never enter this hook.

For rumdl, put a reviewed .rumdl.toml beside your DocSmith configuration:

[markdown_formatter]
command = [
  "rumdl", "fmt", "--config", "{config_dir}/.rumdl.toml",
  "--deny-config-warnings", "--no-cache", "--stderr",
  "--stdin-filename", "{readme}", "-",
]
mode = "stdin"
timeout_seconds = 30

The hook is formatter-independent. For example, an installed Prettier can use:

[markdown_formatter]
command = [
  "prettier", "--config", "{config_dir}/.prettierrc.json",
  "--no-editorconfig", "--parser", "markdown",
  "--stdin-filepath", "{readme}",
]

Keep Prettier's embeddedLanguageFormatting set to off to avoid formatting embedded code. Install and pin your chosen formatter separately; DocSmith does not install it.

The command is an argument array, not a shell string. There is no shell expansion or implicit project executable lookup. Bare executables use PATH; execution uses the README's parent directory as the working directory. Placeholders are:

Placeholder Value
{readme} Absolute intended README path, for filename context only; never an output argument
{config_dir} Absolute directory of the selected DocSmith configuration
{path} Disposable README.md outside the project, available only in file mode

Use {{ and }} for literal braces. Other placeholders, conversions and format specifications are rejected.

  • mode = "stdin" (default): the candidate goes to stdin; UTF-8 stdout is the formatted document. Diagnostics belong on stderr.
  • mode = "file": the command must reference {path} and edit that temporary file. Stdin is closed; stdout/stderr are diagnostics. For rumdl, use the command above without --stderr --stdin-filename {readme} -, adding {path} instead. Name formatter configuration explicitly because the temporary file is outside the project.

timeout_seconds defaults to 30 and must be positive and finite. Exit 0 is required; wrapper scripts must wait for their children. Missing commands, timeouts, nonzero exits, empty/invalid output or changed markers, headings or explicit anchors fail generation. Formatters must be deterministic and preserve DocSmith structure; verify that generate, standalone formatting and check agree after changing tool versions or policy.

Trust: discovered commands run with your permissions and environment, including during --check and --dry-run. This is not a sandbox, so review repository configuration, formatter plugins and scripts before running them. --no-markdown-formatter suppresses the hook; --no-readme excludes it entirely. Loading configuration, validate, help and version never execute it.

Collections

generate and validate also accept a collection path. All roles found via roles/*/meta/argument_specs.yml are then processed like single roles. Additionally, DocSmith maintains role-named marker sections in the collection's README.md (or README.rst), so the collection README can reference the role documentation without manual upkeep:

### My role: foo

<!-- ANSIBLE DOCSMITH TOC foo START -->
<!-- ANSIBLE DOCSMITH TOC foo END -->

### My role: bar

<!-- ANSIBLE DOCSMITH TOC-FULL bar START -->
<!-- ANSIBLE DOCSMITH TOC-FULL bar END -->

<!-- ANSIBLE DOCSMITH MAIN bar START -->
<!-- ANSIBLE DOCSMITH MAIN bar END -->
  • TOC <role> lists the role's variable documentation, TOC-FULL <role> lists all headings of the role's README. Both link into roles/<role>/README.* using relative paths.
  • MAIN <role> embeds the role's complete variable documentation directly in the collection README. All anchors get a <role>- prefix so several embedded roles cannot collide on variable names. If a TOC <role> section exists in the same (Markdown) document, it links to the embedded documentation instead of the role's README.
  • Role-named sections are opt-in per role: roles without markers are simply not referenced in the collection README (validate emits a notice listing them). Markers referencing an unknown role produce a warning.

If a formatter is configured, it runs once per Markdown role README and once for the collection README after all its sections are merged. READMEs without complete markers are not formatted.

ansible-docsmith generate /path/to/collection
ansible-docsmith validate /path/to/collection
ansible-docsmith generate /path/to/collection --check

Validate argument_specs.yml and /defaults

Validate the argument specifications and their consistency with role entry-point files in defaults/:

ansible-docsmith validate /path/to/role

Checks include:

  • Errors for variables present in defaults/ but missing from argument_specs.yml, or variables with a default: in the specification but missing from the entry-point files.
  • Warnings for unknown keys in argument_specs.yml and invalid Ansible markup in descriptions, such as M() without a fully qualified collection name (FQCN).
  • Warnings for required top-level variables assigned in the corresponding defaults file, including null values. A role default may satisfy Ansible's required check without caller input.
  • Notices for non-required variables listed in the specification but absent from an otherwise populated entry-point defaults file, and names suggesting secrets (such as *_password or *_token) without no_log: true.

Use --strict to make warnings fail validation with exit code 1, for example in CI/CD pipelines or pre-commit hooks. Notices do not fail validation.

ansible-docsmith validate /path/to/role --strict

To validate selected parts of a role:

# Skip the README checks (markers and ToC). Useful when only maintaining
# comments in entry-point files.
ansible-docsmith validate /path/to/role --no-readme
# Skip the argument_specs.yml checks (consistency, unknown keys, ...).
# The file must still be parseable YAML.
ansible-docsmith validate /path/to/role --no-argument-specs

# Show help
ansible-docsmith --help
ansible-docsmith validate --help

# Verbose output for debugging
ansible-docsmith validate /path/to/role --verbose

Exit codes

All commands use conventional exit codes, so DocSmith can be wired into scripts, CI/CD pipelines and pre-commit hooks without output parsing:

Exit code Meaning
0 Success. Warnings and notices alone do not fail a run (unless --strict is used). generate --check returns 0 when the documentation is up to date.
1 Validation or processing error (like missing MAIN markers, inconsistencies between argument_specs.yml and defaults/). Also: warnings when validate --strict is used, and pending changes when generate --check is used.
2 Command line or configuration error (unknown option/key, non-existing path, malformed TOML or ambiguous discovery).

Notices are informational and never affect the exit code. A typical gate:

ansible-docsmith validate /path/to/role --strict && \
    ansible-docsmith generate /path/to/role --check

Custom templates

You can customize the generated output by providing your own Jinja2 template. The rendered content will be inserted between the ANSIBLE DOCSMITH MAIN START and END markers in the role's README. Name the file *.md.j2 for Markdown or *.rst.j2 for reStructuredText, matching the README format of the role.

# Use a custom template for README generation
ansible-docsmith generate /path/to/role --template-readme /path/to/custom-template.md.j2

# Combined with other options
ansible-docsmith generate /path/to/role --template-readme ./templates/my-readme.md.j2 --dry-run

Template files must use the .j2 extension (for example, simple-readme.md.j2) and follow Jinja2 syntax. This Markdown example documents top-level options from every entry point. It uses the built-in anchor convention so generated ToC entries and option links can resolve:

{% set multiple_entry_points = specs | length > 1 %}
{% for entry_point, entry_spec in specs.items() %}
{% set short_anchors = (not multiple_entry_points) or entry_point == "main" %}
{% set prefix = anchor_ns ~ ("" if short_anchors else entry_point ~ "-") %}
## Role variables: `{{ entry_point }}`<a id="{{ prefix }}variables"></a>

{% if entry_spec.options %}
{% for var_name, var_spec in entry_spec.options.items() %}
### `{{ var_name }}`<a id="{{ prefix }}variable-{{ var_name }}"></a>

Type: `{{ var_spec.type }}`

{{ var_spec.description | format_description }}

{% endfor %}
{% else %}
This entry point has no configurable variables.
{% endif %}
{% endfor %}

Copy the built-in readme/default.md.j2 template for a starting point that also includes nested options, defaults and other option metadata, with conditional sections.

Available template variables (the context contract):

Variable Type Description
specs dict All entry points in spec file order: entry point name → normalized spec. This is the recommended way to render complete documentation (the built-in templates iterate it).
role_name str Name of the Ansible role (directory name).
role_path Path Path to the role directory.
entry_points list[str] All entry-point names, in spec file order.
anchor_ns str Anchor namespace to prepend to every anchor and internal link the template generates. Empty for role READMEs; <role>- when the content is embedded into a collection README via MAIN <role> markers. Use it like the built-in templates do, or embedded content of several roles may collide.
primary_entry_point str Backwards compatibility: name of the first entry point.
primary_spec dict Backwards compatibility: spec of the first entry point (keys: short_description, description, author, version_added, options).
options dict Backwards compatibility: variables of the first entry point only.
has_options bool Backwards compatibility: whether the first entry point defines variables.

Each entry in an options dictionary maps a variable name to its normalized specification with the keys type, required, default, description, choices, elements, options (nested sub-options, same structure), version_added, no_log and aliases.

Available Jinja2 filters (signatures show the optional arguments):

Filter Description
format_description(value) Formats a description (string or list of paragraphs) for regular display, including Ansible markup conversion.
format_table_description(value, variable_name=None, max_length=250, anchor_prefix="variable-") Formats a description for a table cell: markup conversion, HTML stripping, single-line folding and truncation with a […] link to #<anchor_prefix><variable_name>. Set max_length=0 to disable truncation.
format_default(value, table=False) Formats a default value as inline code (N/A for None). Pass true inside Markdown table cells so pipes get escaped.
code_escape(value, table=False) Renders a value as inline code, handling backticks correctly. Pass true inside Markdown table cells.
ansible_escape(value) Escapes Ansible Jinja2 syntax ({{ }}) so it renders literally (Markdown only).
csv_escape(value) Escapes double quotes for csv-table cells (reStructuredText templates only).

Stability: the variables and filters documented above are the supported contract and only change with a major release (deprecated values like options may then disappear). Anything else you might discover by reading DocSmith's internals, including undocumented context values, filter internals and module layout, can change in any release.

Templates can also generate playbook examples and author information between the markers. The following example uses the compatibility context to document only the first entry point as a task snippet for a playbook. It renders JSON-compatible defaults with Jinja2's tojson filter, whose output is also valid YAML, and leaves variables without a non-null default commented out. Supply values for required variables before running the example:

## Example playbook

```yaml
- ansible.builtin.include_role:
    name: "{{ role_name }}"
    tasks_from: "{{ primary_entry_point }}"
    defaults_from: "{{ primary_entry_point }}"
{% if options.values() | rejectattr('default', 'none') | list %}
  vars:
{% endif %}
{% for var_name, var_spec in options.items() %}
{% if var_spec.default is not none %}
    {{ var_name }}: {{ var_spec.default | tojson }}
{% else %}
    # {{ var_name }}: # {{ var_spec.description | format_description | replace('\n', ' ') }}
{% endif %}
{% endfor %}
```

## Author information

{% if primary_spec.author %}
{% for author in primary_spec.author %}

- {{ author }}
{% endfor %}
{% endif %}

Copyright (c) 2025, 2026, foundata GmbH (https://foundata.com)

This project is licensed under the GNU General Public License v3.0 or later (SPDX-License-Identifier: GPL-3.0-or-later), see LICENSES/GPL-3.0-or-later.txt for the full text.

The REUSE.toml file provides detailed licensing and copyright information in a human- and machine-readable format. This includes parts that may be subject to different licensing or usage terms, such as third-party components. The repository conforms to the REUSE specification. You can use reuse spdx to create a SPDX software bill of materials (SBOM).

REUSE status

Trademarks

Third-party trademarks used in this repository:

  • Ansible®, Fedora® and Red Hat® are trademarks of Red Hat, Inc., registered in the United States and other countries.
  • Debian® is a trademark of Software in the Public Interest, Inc., registered in Germany and the United States.
  • Ubuntu® is a trademark of Canonical Ltd., registered in Germany, the European Union and the United States.

Their use here is purely descriptive and does not imply any affiliation with or endorsement by the trademark holders.

Own and licensed trademarks used in this repository:

  • foundata® is a trademark of IPAM GmbH, registered in Germany and the European Union, licensed to foundata GmbH.

Author information

This project was created and is maintained by foundata. If you like it, you might buy us a coffee.

The Ansible DocSmith project is not associated with Red Hat nor the Ansible project.

Metadata

Release files for ansible-docsmith 2.3.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for ansible-docsmith 2.3.0
File Size Uploaded
ansible_docsmith-2.3.0.tar.gz 84.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for ansible-docsmith 2.3.0
File Interpreter ABI Platform
ansible_docsmith-2.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 171.8 kB

Release files / ansible_docsmith-2.3.0.tar.gz

Download URL ansible_docsmith-2.3.0.tar.gz
Size 84.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f03509b1a1a8f5e6bf8a40cf3a80e048f0643d54bf6a9ba32747811129578375
BLAKE2b-256 checksum
How to use checksums
a20c5bd56da4de28a752f88ddda6fffbd7274fc04c30bef41f383caddf91fe21
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / ansible_docsmith-2.3.0-py3-none-any.whl

Download URL ansible_docsmith-2.3.0-py3-none-any.whl
Size 86.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ad4aba2988cb229941a8317b8b533ada1f355f3bfb519176b160404cae6ddbb6
BLAKE2b-256 checksum
How to use checksums
13e8b16ef2bc2e11fe42b7bd7e274ea3f49f9c42aa5f5fe3dc8284443edeccd0
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

2.4.0

2 release files

This release

2.3.0 This release

2 release files

2.2.0

2 release files

2.1.0

2 release files

2.0.2

2 release files

2.0.1

2 release files

2.0.0

2 release files

1.0.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page