Proposal: document role "return variables" in argument_specs.yml (like module RETURN)

Hello everyone,

I’d like to propose way for roles to document the variables they set, rendered as a table on the role’s documentation page the same way modules document their return values today.

I initiated this in antsibull-docs#491, where @felixfontein pointed out that this is an ecosystem decision that needs buy-in from the Core team, not something antsibull-docs can decide on its own.

The problem

Modules can declare RETURN: and users get a nice, typed “Return Values” table in the docs, plus RV() cross-reference markup. Roles have no equivalent. A role that sets facts/variables (via set_fact) has no structured, typed way to document them.

A weak workaround I found is writing bullet lists into the notes: section of meta/argument_specs.yml.

Here are some examples of return variables that we have in our roles:

bootloader:

notes:
  - "B(Variables):"
  - >
    • C(bootloader_reboot_required) — V(true) means the host must reboot to
    apply the role's changes. The role sets this when O(bootloader_reboot_ok)
    is V(false) and it made changes.
  - >
    • C(bootloader_facts) — boot information for all kernels. The role returns
    this when O(bootloader_gather_facts) is V(true). Each entry is a dict with
    C(kernel), C(args), C(root), and C(default) keys.

ha_cluster:

notes:
  - "B(Variables):"
  - >
    • C(ha_cluster_facts) — the exported current cluster configuration,
    populated when O(ha_cluster_export_configuration) is V(true). The exported
    dictionary may not be complete and manual modification is expected; most
    notably you must set O(ha_cluster_hacluster_password). Depending on the
    C(pcs) version installed on managed nodes, certain variables may not be
    present in the export.
  - "The following variables are present in the export:"
  - "• C(ha_cluster_enable_repos) — RHEL and CentOS only."
  - "• C(ha_cluster_enable_repos_resilient_storage) — RHEL and CentOS only."
  - >
    • C(ha_cluster_manage_firewall) — requires C(python3-firewall) to be
    installed on managed nodes.
  - >
    • C(ha_cluster_manage_selinux) — requires C(python3-policycoreutils) to be
    installed on managed nodes.
  - "• C(ha_cluster_cluster_present) — whether a cluster is configured."
  - "• C(ha_cluster_start_on_boot) — whether cluster services start on boot."
  - "• C(ha_cluster_install_cloud_agents) — RHEL and CentOS only."

podman:

notes:
  - "B(Variables):"
  - >
    • C(podman_version) — the version string of the podman version in use.
    Useful in templates, for example
    C(podman_version is version("4.5", ">=")).
  - >
    • C(podman_subuid_info) — the subuid ranges for the users managed by the
    role, extracted from C(getsubids) if available, or from F(/etc/subuid).
    A dict keyed by user name; each value is a dict with C(start) (int) and
    C(range) (int).
  - >
    • C(podman_subgid_info) — the subgid ranges for the groups managed by the
    role, extracted from C(getsubids) if available, or from F(/etc/subgid).
    A dict keyed by group name; each value is a dict with C(start) (int) and
    C(range) (int).
  - >
    • C(podman_use_new_toml_formatter) — whether the role used the new TOML
    formatter when rendering configuration files.

This has no type/returned/sample columns, no anchors, no RV() targets, and is easy to let drift out of date.

Proposed approach

Allow an optional return: key per entry point in meta/argument_specs.yml, using the same structure as module RETURN::.

Here’s an example of how the bootloader role return would look like:

argument_specs:
  main:
    short_description: ...
    options: ...
  return:
    bootloader_reboot_required:
      description:
        - V(true) means the host must reboot to apply the role's changes.
        - The role sets this when O(bootloader_reboot_ok) is V(false) and it
          made changes.
      type: bool
      returned: always
      sample: true
    bootloader_facts:
      description:
        - Boot information for all kernels.
      type: list
      elements: dict
      returned: When O(bootloader_gather_facts) is V(true).
      contains:
        kernel:
          description: Path to the kernel image.
          type: str
        args:
          description: The kernel command-line arguments.
          type: str
        root:
          description: The root device for this boot entry.
          type: str
        default:
          description: Whether this is the default boot entry.
          type: bool
      sample:
        - kernel: /boot/vmlinuz-6.9.0
          args: "ro crashkernel=1G-4G:192M"
          root: UUID=1aefe582
          default: true

Key points:

  • Fully optional — roles without a return: block are completely unaffected.
  • Rendered as a dedicated table on the role page. I’d suggest titling it “Return Variables” rather than modules’ “Return Values”, since a role sets variables rather than returning a structured result.
  • RV(fqcn#role:entrypoint:name) markup already supports roles (groundwork was added in antsibull-docs #113 “in case they are ever added”), so references would resolve once the data exists.

Where the changes would land

I’ve already prototyped this end to end. The notable finding: the data path mostly works already.

  1. ansible-core — ansible-doc -t role --json already passes a return: key through untouched (the role doc builder copies the whole entry-point spec), so no change is strictly required for the data to reach doc tooling. Two things would be nice-to-have and are really the Core team’s call: (a) rendering the section in ansible-doc’s text output, and (b) officially documenting/“blessing” the return: key in the role arg-spec format.
  2. antsibull-docs — needs to parse and render the return: block. Prototyped in antsibull-docs#491.
  3. ansible-lint — its role-arg-spec.json schema currently has additionalProperties: false and no return property, so it rejects the key. This would need an optional return property added, otherwise roles using it fail schema[role-arg-spec].

I ealize why the decision to avoid adding return: for roles was made - they don’t return values the way modules do, however in linux-system-role it’s a common practice for the roles to set Variables for users to see the current configuration, or whether reboot is needed, or other extra info. I think it’s the time to add this support.

If there’s agreement on the direction, I’m happy to open PRs with the implementation across the three repos (antsibull-docs, ansible-lint schema, and any ansible-core doc/text-output changes).

What do you think? Do you have any concerns about adding return: for roles?

Thank you

2 Likes

To support this proposal, I’ve put together a working proof-of-concept across the whole toolchain. It’s intentionally split per repo, and the cross-cutting ones are drafts — I don’t want anything merged ahead of consensus here; they’re meant to make the proposal concrete and reviewable.

How the data flows: a role declares an optional return: block per entry point in meta/argument_specs.yml, using the same format as module RETURN. ansible-doc -t role --json already passes this key through untouched (the entry-point spec is copied verbatim), so most of the work is in the consumers.

PRs

  1. antsibull-docs — render the table → Render a "Return Variables" table for roles by spetrosi · Pull Request #492 · ansible-community/antsibull-docs · GitHub
    Adds return to the role schema and renders a “Return Variables” section on the role page (reusing the existing return-values macro, so nested contains: becomes a sub-table and RV() references resolve). Titled “Return Variables” rather than modules’ “Return Values”, since a role sets variables rather than returning a structured result.
  2. ansible-lint — accept the key → feat: allow return values in role argument_specs schema by spetrosi · Pull Request #5213 · ansible/ansible-lint · GitHub
    Adds an optional return property (+ a recursive return_value definition) to role-arg-spec.json. Without this, schema[role-arg-spec] rejects any role using return:.
  3. ansible-core — show it in ansible-doc → github .com/ansible/ansible/pull/87686
    Renders a RETURN VALUES: section in ansible-doc -t role text output (the JSON already carries it). No change to arg-spec validation — unknown keys are already ignored there.
    I can post only 2 URLs in a comment as a new Forum user, so I had to break the above URL.
  4. ansible-documentation — document the key → github .com/ansible/ansible-documentation/pull/4062
    I can post only 2 URLs in a comment as a new Forum user, so I had to break the above URL.
    Documents the return: key and its sub-fields in the role argument-spec “Specification format” section, and adds a return: block to the sample. (The ansible-lint PR #5213 links its schema hints at this page and is marked blocked on it.)

As you already state, I’m not sure ‘return’ is proper way as roles don’t really return anything. They CAN define new variables, at different scopes, but so can any include/import.

Also roles have an additional configuration that would prevent such variables from existing outside of the role’s context.

The scopes and precedence of the variables can make things even more ‘fun’, you only look at set_facts, which is limited to host scope, but include_vars, _facts modules, add_host, role vars/defaults/inline parameters, and other actions can all add variables to the play, some will obey the role private configuration, some bypass it.

Another thing to consider is ‘when’ things are defined, roles, import_role, include_role and dependencies all activate at different stages and incorporate certain variable sources at different times during the play’s runtime, not reflecting this in the documentation for each variable will be misleading, if a user reads var_x and var_y are both documented, but only var_y is available when they import the role, but before execution, will lead to much frustration.

That aside we also have to look at backwards compatibility, while ansilbe-doc is permissive by design, other tools are much stricter about document definitions and will break when an older version encounters the expanded document spec.

I’m not against having a way to document the effects roles have, including variable declaration, but I find this to approach to be a bit to narrow and does not reflect the flexibility that roles have.

Are you refering to DEFAULT_PRIVATE_ROLE_VARS, the include_role public parameter, and the import_role public parameter?

(Maybe the roles 2.0 proposal could have something about returning values from a role in a structured way, maybe similarly to how GitHub Actions and GitHub shared workflows can return values as ‘outputs’.)

the ‘taskify includes’ would also overlap with ‘return values/effects’ documentation

In the system roles case, the “return variables” are either set via set_fact or set in ansible_facts. I guess we could have some extra information for each variable listed in the “Return Variables” table which specifies how the variable is set - set_fact, ansible_facts, etc.

you can also set the same variable multiple ways, most commonly defaults/ + another

My initial suggestion was pretty light - just add support for the return key to make docs generated look prettier and more consistent with module docs.
However, as it was pointed out, it’s a more complex issue that includes how and when roles return variables. And I am all for finding a good and supported way to allow roles return variables to user.

With modules it’s easy - it always returns some yaml output. Doesn’t matter if it failed or not.

With roles, there is no explicit way to return variables from role. Instead, roles can use Ansible common ways to set variables, each having different scope and precedence.

I’d prefer staying with a single bulletproof method for roles to set variables - via set_fact module. Then users can decide what to do with the returned variable - reset it with set_fact: cacheable: true, or save to a file, or save to local facts in /etc/ansible/facts.d/.
In this case we don’t need to go great lenghts in describing which

In one of the roles we needed to save facts into a file at /etc/ansible/facts.d/pre_ipu.fact to make facts persist between ansible runs and even reboots. I think we can treat it as an exception, in this case the role can document that it saves facts into this file and why. But it also should set the fact with set_fact to make it available within the play.

About backward compatibility, an extra return: key in argument_specs.yml doesn’t break Ansible variables validation, ansible-lint argues aboun incorrect schema that’s all. For other tools - the return: key is optional.

This is still a workaround to how roles currently don’t have an explicit way to return vars. To really fix this, I guess the only way is to introduce Roles 2.0 mentioned above, and there add a new mechanism to return vars to _role[‘return’][‘varname’].

Just use GitHub - foundata/ansible-docsmith: Automating Ansible role documentation (using argument_specs.yml) · GitHub - you can add what ever you like to return entry point in argument spec and using docsmith templates render it into readme.md for the role.

1 Like

Just use GitHub - foundata/ansible-docsmith: Automating Ansible role documentation (using argument_specs.yml) · GitHub

The main goal was to build pretty HTMLs that will be identical to what official Ansible docs have, and eventually publish collection documentation to Collection Index — Ansible Community Documentation
I am afraid we won’t be able to do this when using custom keys in argument_specs.yml

Do not be afraid. You do not need html, you actually need rst - see reStructuredText Primer — Sphinx documentation

It is much more easy to generate compared to html, you actually can create jinja rst template and generate whatever you want using information from argument_specs file.

I tried ansible-docsmith and seems like it can’t do this: per its README, a template receives only supported keys, so a return: block isn’t part of the exposed data and is dropped before any template runs.

I want to make use of officially supported tools, then documentation will be generated from argument_specs.yml automatically and then can be published on official Ansible docs portal. If I do a custom argument_specs.yml, it won’t be able to read it.

I might be wrong here, but I’ve never seen any role documentation on ansible docs portal. All role docs are in README.md file for the role, even ansible galaxy follows this convention. But if you want to use argument_specs not only for validation, but also for document generation (so you do not have to repear yourself in README.md) you will have to use non ansible-* tools, from my point of view

Ah I am talking about roles as part of collections, and collections documentation lives in Collection Index — Ansible Community Documentation
E.g. community.sops has the documentation for the community.sops.install role there

I completely agree that roles-v2 is the right way to solve this long term. If we were starting from scratch, a full rework makes total sense.
But we also know a huge architectural shift to roles-v2 would tak a lot of time. And would break forward and backward compatibility.

Right now we are asking people to write roles with a pretty frustrating limitation.
Users end up burying variables in unstructured notes just to get them documented.

My idea here is really just meant to be a bridge. Something we can use right now while the bigger roles-v2 design takes shape in the background.

The nice thing is that it does not break anything. Older tools will just ignore the new key. It is totally backward and forward compatible.
I know it is not perfect. There is no explicit option to define where the variable is stored in the state. But we can easily work around that by just mentioning the location in the plain text description.

It feels like a small trade for a much better reading experience.

By the way, I missed out one important thing - I am talking here about generating documentation for roles within collections, not for legacy roles. Tooling for generating docs for a collection is newer so I use this.
So for my collection I’ll do this:

# antsibull makes the config files it needs: conf.py, antsibull-docs.cfg.
mkdir -p "$WORK/site"
antsibull-docs sphinx-init --use-current --squash-hierarchy \
    --dest-dir "$WORK/site" fedora.linux_system_roles

cd "$WORK/site"

# 1. antsibull turns argument_specs.yml into RST (same command as its build.sh).
# The RST is a build intermediate (Sphinx input below); it is not copied out.
chmod og-w rst   # antsibull-docs wants this directory writable only by its owner
antsibull-docs --config-file antsibull-docs.cfg collection \
    --cleanup everything --use-current --squash-hierarchy \
    --dest-dir rst fedora.linux_system_roles

# 2. Sphinx renders the RST to the styled HTML site.
sphinx-build -M html rst build -q -c .

You can find a list of all of them here: Index of all Roles — Ansible Community Documentation

Unfortunately Galaxy doesn’t show this documentation at all (it renders README.md instead).

While I agree that we will likely have to wait a long time for roles-v2, the current roles-v2 proposal (since its beginning) was opt-in for roles, so that the proposal does not break existing (v1) roles.

I agree, this would be quite a good thing right now.

Everything limited to ansible.builtin.set_fact (with and without cacheable: true) can be documented with your approach. The cacheable: true can be put into ansible_facts, which the rendering can separate into two sections, similar to how module return values are split into facts and regular return values.

1 Like

Since roles don’t return anything in the conventional sense, I think overloading that term is confusing and paints ansible-core into a corner if roles ever do get a more conventional way to return variables (and probably cause additional confusion then). Naming it something more accurate would seem better to me, but the ability to document ansible_facts / set_fact + cacheable variables seems hyperspecific and not as useful as I’d hope.

I use the role argspec as the method of documenting variables provided by the role as well as input since it has the perk of actually validating variables. Here’s a way you could do it using the argspec.

roles/
├── defines_defaults
│   ├── defaults
│   │   └── main.yml
│   └── meta
│       ├── argument_specs.yml
│       └── main.yml
└── _defines_defaults_helper
    ├── meta
    │   └── argument_specs.yml
    └── tasks
        └── main.yml
roles/defines_defaults/defaults/main.yml
defines_defaults_datetime: "{{ ansible_facts.date_time }}"
defines_defaults_foo: "{{ ansible_facts._foo }}"
roles/defines_defaults/meta/argument_specs.yml
argument_specs:
  main:
    short_description: Example of a vars-only role.
    description:
      - Example of a role argument spec documenting and validating variables derived from tasks.
    options:
      input:
        description:
          - Example of a variable that must be specified by the user to use the role.
        required: True
        type: str
      defines_defaults_datetime:
        description:
          - This variable is defined in defaults/ using ansible_facts.
          - If this doesn't have an obvious default to document, the logic or samples can be described here.
        type: dict
      defines_defaults_foo:
        description:
          - This variable is defined in defaults/ using ansible_facts.
        type: str
roles/defines_defaults/meta/main.yml
dependencies:
  - _defines_defaults_helper
roles/_defines_defaults_helper/meta/argument_specs.yml
argument_specs:
  main:
    short_description: Helper role that runs tasks.
    description:
      - Helper role that runs tasks.
    options:
      input:
        description:
          - Example of a variable that must be specified by the user to use the role.
        required: True
        type: str
roles/_defines_defaults_helper/tasks/main.yml
- setup:
    gather_subset:
      - "!all"
      - "!min"
      - "date_time"

- set_fact:
    _foo: bar
    cacheable: True

Since roles don’t return anything in the conventional sense, I think overloading that term is confusing and paints ansible-core into a corner if roles ever do get a more conventional way to return variables (and probably cause additional confusion then).

If roles ever do get a way to return variables, this new way can reuse the return: key that’s it.
I don’t think we bring any extra future burden by supporting return key to document variables that roles set.

Having a return key doesn’t cause any issues during Ansible’s argument validation as it’s simply ignored, so the change is backward-compatible.
Role won’t enforce any extra validation of the return content, there’s no need to validate it because this is not something that users can do mistakes with.