DocSmith for Ansible

Source code

Licensing

Miscellaneous

README

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


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. Everything outside the markers is never touched.
  • 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

Basic usage:

## 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

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.
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).
  • 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 codeMeaning
0Success. Warnings and notices alone do not fail a run (unless --strict is used). generate --check returns 0 when the documentation is up to date.
1Validation 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.
2Command line usage error (unknown option, non-existing path).

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 }}` {#{{ prefix }}variables}
{% if entry_spec.options %}
{% for var_name, var_spec in entry_spec.options.items() %}
#### `{{ var_name }}` {#{{ prefix }}variable-{{ var_name }}}
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):

VariableTypeDescription
specsdictAll 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_namestrName of the Ansible role (directory name).
role_pathPathPath to the role directory.
entry_pointslist[str]All entry-point names, in spec file order.
anchor_nsstrAnchor 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_pointstrBackwards compatibility: name of the first entry point.
primary_specdictBackwards compatibility: spec of the first entry point (keys: short_description, description, author, version_added, options).
optionsdictBackwards compatibility: variables of the first entry point only.
has_optionsboolBackwards 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):

FilterDescription
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.