ADR-013. When a module appeared is recorded, never computed
Status: accepted, 7 September 2026. Still in force.
Context
version_added answers a question a user cannot settle any other way: can I
call this module with the collection version I pinned? Every published module
page carries it, antsibull-docs renders it, and the Ansible community
inclusion checklist names it explicitly:
documentation and return sections use
version_added:containing the collection version for which an option, module or plugin was added
Until 0.4.0 the generator wrote the current collection version into that field. Every generation therefore rewrote the history of every module it produced. Measured on the four published tags on 7 September 2026:
tag |
what |
when it actually appeared |
|---|---|---|
|
|
|
|
|
|
|
|
|
The measurement was made by looking, for each name, for the first published tag that carries it. It settled three counts that had been assumed rather than known:
all 50 modules of 0.4.0 exist since
0.1.0, at the same path in all four tags. The issue that opened this work supposedinstance_server_infohad arrived in 0.2.0; that was wrong;all 271 options were born with their module;
8 return keys, all named
result, appeared in0.4.0. They were added beside the existing key, not in its place, so they are genuine additions rather than a rename.
The hand-written inventory plugin was correct throughout, carrying
version_added: 0.1.0. That contrast is what shows the defect belongs to the
generator and not to a decision anyone took.
Three sources could have supplied the fact, and two were measured unusable:
changelogs/changelog.yamldeclares modules per release only when a fragment says so. It names exactly one module under0.1.0, out of fifty. A partial record used as a complete one would have dated forty-nine modules wrong;git history is true by construction, but it makes generation depend on the repository rather than on its inputs, and the CI checkout that runs
mise run checkfetches no tags. A control reading it would have degraded silently, which is the failure this repository spends its time refusing.
Decision
The introduction version is recorded, never computed. It lives in
generator/ansible/introductions.yml, a journal read by the generator and by
nothing else.
The journal is a sequence of dated blocks. Its form carries its invariant:
a name appears in exactly one block. A name found in two blocks is an appearance being redated, and the loader refuses the file. The rule that a release never changes an existing
version_addedis therefore measured, not written in a comment;a block accepts only
modules,optionsandretours. An unknown key is refused rather than ignored, for the same reason an override refuses an unknown field: a typo would produce a silently inert record;an option or return dated on a module the journal does not know is an orphan, and the file is refused. This mirrors the orphan override check;
what is not yet dated receives
en_preparation, a value written by hand in the same file. That is a declared decision, not a guess, andscripts/introductions.pyconfronts it with the version the pending changelog fragments imply.
A module always publishes its version_added. An option or a return publishes
one only when it is later than its module: repeating the module’s date on
each of its options would print the same badge on every line of a page and
teach nothing.
Consequences
Adding a module is now two steps. The generator dates an unknown module
with en_preparation; the journal must then record it. A control lists what is
covered by en_preparation rather than dated, so the gap is visible rather
than silent.
The journal is a fourth thing to keep. It joins the contracts, the overrides and the goldens. It is small, it is append-only, and its diff is the record of what a release adds, which makes it worth reading rather than merely maintaining.
Nothing repairs the pages already published. Galaxy pages for 0.1.0 through 0.4.0 keep the wrong dates. They cannot be republished, and the correction starts at 0.5.0.
The two-step can be forgotten. en_preparation is set by hand, and a stale
value would stamp a new module with a version that never ships. The control
compares it with what the changelog fragments imply, and says plainly when
there are no fragments to compare against rather than passing in silence.
What this record does not decide
ADR-007 decides where a measured number lives, and it is why the counts above
are here rather than in a comment. ADR-005 decides that generated documentation
is refused rather than reported when it is unusable; a wrong version_added is
not unusable documentation, it is false documentation, and that gate did not
and could not see it.
Whether a module should also declare its Ansible attributes, and what
version_added those attributes carry, is not decided here.