Skip to content

Commit 3f2ec08

Browse files
authored
Merge pull request #358 from opsmill/chore/dev-docs-alignment
docs(dev): author dev/ guides, guidelines, knowledge, and seed ADRs
2 parents acc3ffe + cce3b33 commit 3f2ec08

18 files changed

Lines changed: 797 additions & 24 deletions

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ This file is the portable router: repo-wide facts every agent needs up front. De
1515
|-----------|-------------|
1616
| Python | >=3.10, <3.14 |
1717
| ansible-core | >=2.17.7rc1 (Python 3.10+) |
18-
| infrahub-sdk | >=1.5, <2.0 |
18+
| infrahub-sdk | >=1.19.0, <2.0 |
1919
| Linter/Formatter | Ruff (pinned in pyproject.toml) |
2020
| Tests | pytest, ansible-test sanity (Docker-based) |
2121
| Docs | Docusaurus + Jinja2 generation |

dev/README.md

Lines changed: 51 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,67 @@
1-
# dev/
1+
# Developer Documentation
22

3-
Developer knowledge base and AI agent resources for the `opsmill.infrahub` Ansible collection.
3+
Internal documentation for `opsmill.infrahub` collection contributors. For
4+
user-facing docs, see the [Docusaurus site](https://docs.infrahub.app/ansible/)
5+
generated from `docs/`.
46

5-
## Structure
7+
## Quick Navigation
68

7-
### knowledge/
9+
| I want to... | Go to |
10+
|------------------------------------|------------------------|
11+
| Understand how the collection works | `knowledge/` |
12+
| Follow coding and writing standards | `guidelines/` |
13+
| Do a specific task step by step | `guides/` |
14+
| Learn why we made a decision | `adr/` |
15+
| Read the project constitution | `constitution.md` |
16+
| Use agent commands | `../.agents/commands/` |
817

9-
Deep reference material extracted from the codebase.
18+
## Directory Guide
1019

11-
- [architecture.md](knowledge/architecture.md) — Collection structure, plugin types, data flow, key abstractions
12-
- [plugin-patterns.md](knowledge/plugin-patterns.md) — Ansible-specific conventions: boilerplate, docstrings, arg specs, conditional imports, state management
13-
- [infrahub-sdk-usage.md](knowledge/infrahub-sdk-usage.md) — InfrahubclientWrapper, InfrahubModule, processor classes, sync-only pattern
20+
- **constitution.md**: Pointer to the binding project principles in
21+
`.specify/memory/constitution.md`. The authoritative reference.
22+
- **knowledge/**: Descriptive reference. How the system works.
23+
- **guidelines/**: Prescriptive rules. How code and docs should be written.
24+
- **guides/**: Step-by-step procedures for specific tasks.
25+
- **adr/**: Architecture Decision Records. Why we chose what we chose.
26+
27+
## Current Knowledge
1428

15-
### guidelines/
29+
- [architecture.md](knowledge/architecture.md) — Collection layout, plugin types, data flow, key abstractions
30+
- [plugin-patterns.md](knowledge/plugin-patterns.md) — Ansible conventions: boilerplate, docstrings, arg specs, conditional imports, state management
31+
- [infrahub-sdk-usage.md](knowledge/infrahub-sdk-usage.md) — InfrahubclientWrapper, InfrahubModule, processor classes, sync-only pattern
32+
- [inventory-and-lookup.md](knowledge/inventory-and-lookup.md) — Dynamic inventory and GraphQL lookup plugins, end to end
33+
- [processors-and-arg-spec.md](knowledge/processors-and-arg-spec.md)`INFRAHUB_ARG_SPEC`, the `deepcopy` extension pattern, and the processor class hierarchy
1634

17-
Standards and conventions for contributing.
35+
## Current Guidelines
1836

19-
- [python.md](guidelines/python.md) — Ruff config, line length 120, rule selection, format settings
20-
- [testing.md](guidelines/testing.md) — Docker-based test execution, unit tests with mocks, sanity tests
37+
- [python.md](guidelines/python.md) — Ruff config, line length 120, type hints, dependencies
38+
- [testing.md](guidelines/testing.md) — Docker-based test execution, mocking, sanity/unit/integration
2139
- [documentation.md](guidelines/documentation.md) — Doc generation pipeline, Jinja2 templates, Docusaurus, Vale
40+
- [module-docstrings.md](guidelines/module-docstrings.md) — DOCUMENTATION/EXAMPLES/RETURN rules and doc fragments
41+
- [markdown.md](guidelines/markdown.md) — Markdown conventions, file naming, markdownlint/Vale/yamllint
2242
- [git-workflow.md](guidelines/git-workflow.md) — Branch model (develop/stable), PR conventions, CI, versioning
2343

24-
### guides/
44+
## Current Guides
2545

26-
Step-by-step how-tos for common tasks.
27-
28-
- [creating-a-module.md](guides/creating-a-module.md) — Add a new module + action plugin + tests + docs
46+
- [creating-a-module.md](guides/creating-a-module.md) — Add a new module + action/module_utils + tests + docs
2947
- [running-tests.md](guides/running-tests.md) — Invoke tasks, Docker Compose, pytest, troubleshooting
48+
- [adding-a-doc-fragment.md](guides/adding-a-doc-fragment.md) — When and how to add a shared doc fragment
49+
- [debugging-sanity-failures.md](guides/debugging-sanity-failures.md) — Reproduce and fix `ansible-test sanity` failures
50+
- [releasing-the-collection.md](guides/releasing-the-collection.md) — Cut a release: develop→stable, automated version bump, build, and Galaxy publish
51+
52+
## Current ADRs
53+
54+
- [0001-two-plugin-patterns.md](adr/0001-two-plugin-patterns.md) — Module-utils vs action plugin patterns
55+
- [0002-sdk-abstraction-wrapper.md](adr/0002-sdk-abstraction-wrapper.md) — Wrap the SDK behind `InfrahubclientWrapper`
56+
- [0003-sync-only-sdk.md](adr/0003-sync-only-sdk.md) — Synchronous-only SDK usage
57+
- [0004-docker-based-testing.md](adr/0004-docker-based-testing.md) — Run all test modes in Docker via a multi-stage image
58+
- [0005-doc-generation-pipeline.md](adr/0005-doc-generation-pipeline.md) — Generate plugin reference MDX from docstrings
3059

31-
### commands/
60+
## Agent Commands
3261

33-
Claude Code slash commands (available via `/add-module`, `/fix-bug`).
62+
Agent commands live at the repository root under
63+
[`../.agents/commands/`](../.agents/commands/):
3464

35-
- [add-module.md](commands/add-module.md) — Scaffold a new Ansible module
36-
- [fix-bug.md](commands/fix-bug.md) — Guided bug investigation and fix workflow
65+
- [add-module](../.agents/commands/add-module.md) — Scaffold a new Ansible module
66+
- [fix-bug](../.agents/commands/fix-bug.md) — Guided bug investigation and fix workflow
67+
- `speckit.*` — Spec-kit workflow commands (specify, plan, tasks, implement, …)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ADR-0001: Two Plugin Patterns (Module-Utils vs Action Plugin)
2+
3+
**Status**: Accepted
4+
**Date**: 2026-02-25
5+
**Source**: `.specify/memory/constitution.md` (Principle II) — backfilled from existing code
6+
7+
## Context
8+
9+
Every Ansible operation in this collection is exposed as a module under
10+
`plugins/modules/`, but the modules differ sharply in what they do. Some
11+
(`node`, `branch`) perform stateful CRUD that must be idempotent, support
12+
`check_mode`, and emit `--diff` output. Others (`query_graphql`,
13+
`artifact_fetch`, `artifact_generate`, `object_file_fetch`, `schema`) are
14+
read-mostly operations against the Infrahub API that do not map cleanly onto
15+
present/absent state. Forcing both into a single execution model would either
16+
saddle read-only queries with unused idempotency machinery or strip stateful
17+
modules of the state tracking they need.
18+
19+
## Decision
20+
21+
Support two distinct implementation patterns, chosen per module:
22+
23+
- **Module-utils pattern** — for stateful CRUD. The module stub in
24+
`plugins/modules/` builds an `AnsibleModule` from `deepcopy(INFRAHUB_ARG_SPEC)`
25+
and delegates to an `InfrahubModule` subclass in `plugins/module_utils/`
26+
(`NodeModule`, `BranchModule`). These subclasses implement `run()`, support
27+
`state` (present/absent), `check_mode`, and `--diff` via the base class's
28+
`_ensure_object_exists` / `_ensure_object_absent` and diff helpers.
29+
- **Action plugin pattern** — for read-mostly operations. The module stub
30+
carries documentation and the arg spec; an `ActionModule` in `plugins/action/`
31+
runs the real logic controller-side, instantiating `InfrahubclientWrapper`
32+
directly and returning a result dict.
33+
34+
**Decision criteria**: use the module-utils pattern for anything requiring
35+
idempotency and state management; use the action plugin pattern for read-only
36+
queries or operations that cannot be made idempotent.
37+
38+
## Consequences
39+
40+
- Contributors must classify a new module up front. The procedure is documented
41+
in [../guides/creating-a-module.md](../guides/creating-a-module.md) (Step 2a
42+
vs 2b), and the patterns are detailed in
43+
[../knowledge/plugin-patterns.md](../knowledge/plugin-patterns.md).
44+
- Both paths share the same SDK wrapper and credential handling, so divergence
45+
is contained to the execution model, not the API surface.
46+
- `node` and `branch` are the only module-utils modules today; the remaining
47+
modules use action plugins.
48+
49+
## Alternatives Considered
50+
51+
- **Single module-utils path for everything**: rejected — read-only queries gain
52+
no benefit from state/diff machinery and would need awkward no-op semantics.
53+
- **Single action-plugin path for everything**: rejected — action plugins have no
54+
built-in idempotency or `--diff` support, which `node`/`branch` require.
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# ADR-0002: SDK Abstraction Behind InfrahubclientWrapper
2+
3+
**Status**: Accepted
4+
**Date**: 2026-02-25
5+
**Source**: `.specify/memory/constitution.md` (Principle IV) — backfilled from existing code
6+
7+
## Context
8+
9+
This collection talks to Infrahub exclusively through the `infrahub-sdk`
10+
package. Without a discipline around how the SDK is used, every plugin —
11+
modules, action plugins, the inventory source, and the lookup plugin — would
12+
independently construct `InfrahubClientSync`, assemble its `Config`, translate
13+
SDK exceptions into Ansible errors, and reimplement node/branch/GraphQL calls.
14+
That duplication drifts as the SDK evolves and scatters error handling across
15+
the codebase.
16+
17+
## Decision
18+
19+
Route all Infrahub API access through a single wrapper, `InfrahubclientWrapper`,
20+
defined in `plugins/module_utils/infrahub_utils.py`.
21+
22+
- The wrapper builds the SDK `Config` (address, `api_token`, `default_branch`,
23+
`timeout`, `tls_insecure`) and owns the `InfrahubClientSync` instance. Callers
24+
never instantiate the SDK client directly.
25+
- It exposes intent-named methods — `fetch_single_node`, `fetch_nodes`,
26+
`create_node`, `save_node`, `delete_node`, `fetch_single_schema`,
27+
`fetch_branch`, `create_branch`, `delete_branch`, `execute_graphql`,
28+
`fetch_single_artifact`, `generate_artifact`.
29+
- Higher-level orchestration sits on top: the `InfrahubModule` base class for
30+
stateful modules, and the `InfrahubBaseProcessor` /
31+
`InfrahubNodesProcessor` / `InfrahubQueryProcessor` classes for the inventory
32+
and lookup plugins.
33+
- SDK exceptions are mapped to Ansible errors in one place via
34+
`handle_infrahub_exceptions_decorator` (`plugins/module_utils/exception.py`),
35+
not at each call site.
36+
37+
## Consequences
38+
39+
- A single seam absorbs SDK API changes; call sites stay stable.
40+
- New plugins inherit consistent credential handling, branch awareness, and
41+
error mapping for free. Usage is documented in
42+
[../knowledge/infrahub-sdk-usage.md](../knowledge/infrahub-sdk-usage.md) and
43+
the overall data flow in [../knowledge/architecture.md](../knowledge/architecture.md).
44+
- The wrapper is shared core code (~1500 lines); changes to it affect every
45+
plugin and warrant extra review.
46+
47+
## Alternatives Considered
48+
49+
- **Direct `InfrahubClientSync` use in each plugin**: rejected — duplicates
50+
config assembly and error handling, and couples every plugin to the SDK's
51+
surface.
52+
- **A thin pass-through that only constructs the client**: rejected — leaves
53+
exception mapping and node/branch/query orchestration to each caller, the very
54+
duplication this decision avoids.

dev/adr/0003-sync-only-sdk.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# ADR-0003: Synchronous-Only SDK Usage
2+
3+
**Status**: Accepted
4+
**Date**: 2026-02-25
5+
**Source**: `.specify/memory/constitution.md` (Principle IV) — backfilled from existing code
6+
7+
## Context
8+
9+
The `infrahub-sdk` ships both an asynchronous client (`InfrahubClient`) and a
10+
synchronous one (`InfrahubClientSync`). Ansible modules, action plugins, and
11+
inventory/lookup plugins all execute in a synchronous context — Ansible invokes
12+
them as blocking calls and expects a result dict (or populated inventory) when
13+
they return. Introducing `async`/`await` would mean managing an event loop
14+
inside each plugin and offers no benefit, since the collection issues API calls
15+
serially within a single task.
16+
17+
## Decision
18+
19+
Use `InfrahubClientSync` exclusively. The async `InfrahubClient` is never used.
20+
21+
- `InfrahubclientWrapper` constructs and holds an `InfrahubClientSync`; all
22+
wrapper methods block until completion.
23+
- The `infrahub-sdk` dependency is pinned `>=1.19.0,<2.0` with the `[all]` extras,
24+
which include synchronous support.
25+
- No plugin defines `async def` entry points or runs an event loop.
26+
27+
## Consequences
28+
29+
- Plugins stay simple — straight-line synchronous code with no loop management.
30+
- Aligns with Ansible's execution model; results are available on return.
31+
- Throughput is bounded by serial API calls; this is acceptable for the
32+
collection's workloads and is the lever tracked separately for inventory
33+
fetch optimization.
34+
- Contributors must select the sync variant of any new SDK call. The sync-only
35+
rule is reiterated in
36+
[../knowledge/infrahub-sdk-usage.md](../knowledge/infrahub-sdk-usage.md).
37+
38+
## Alternatives Considered
39+
40+
- **Async `InfrahubClient` with an event loop per plugin**: rejected — adds
41+
loop-management complexity to every plugin for no gain in a synchronous
42+
Ansible context.
43+
- **Mixed sync/async depending on plugin**: rejected — two client lifecycles and
44+
two error-handling paths for no clear benefit.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# ADR-0004: Docker-Based Test Execution
2+
3+
**Status**: Accepted
4+
**Date**: 2026-02-25
5+
**Source**: `.specify/memory/constitution.md` — backfilled from existing code
6+
7+
## Context
8+
9+
The collection's test suite spans three `ansible-test` modes — sanity, unit,
10+
and integration — each with its own tooling expectations: a pinned Python
11+
version, a built-and-installed copy of the collection on a custom
12+
`ANSIBLE_COLLECTIONS_PATH`, and (for integration) a live Infrahub to talk to.
13+
Running these directly on a contributor's machine is fragile: `ansible-test`
14+
is sensitive to the Python version, the collection must be built and placed at
15+
exactly `ansible_collections/opsmill/infrahub`, and host state leaks between
16+
runs. CI and local runs would drift unless the environment were pinned in one
17+
place.
18+
19+
## Decision
20+
21+
Run every test mode inside Docker, built from a single multi-stage `Dockerfile`
22+
and orchestrated by `docker-compose.yml` and the Invoke tasks in `tasks/tests.py`.
23+
24+
- The `Dockerfile` defines a `base` stage (Python `${PYTHON_VER}`, `uv`, the
25+
virtualenv on `PATH`) and three derived targets: `sanity`, `unittests`, and
26+
`integration`. Each target builds the collection with `ansible-galaxy
27+
collection build`, installs it, switches to the collection path, and runs the
28+
matching `ansible-test` command.
29+
- `docker-compose.yml` maps one service per target (`sanity`, `unit`,
30+
`integration`), sharing build args and threading through
31+
`ANSIBLE_SANITY_ARGS` / `ANSIBLE_UNIT_ARGS` / `ANSIBLE_INTEGRATION_ARGS` so
32+
extra `ansible-test` flags (e.g. `-vvv`) can be passed from the environment.
33+
- `invoke tests-sanity` / `tests-unit` / `tests-integration` each run
34+
`docker compose up --build --force-recreate --exit-code-from <service>
35+
<service>` with `PYTHON_VER` taken from the Invoke config, so the task's exit
36+
code reflects the test result.
37+
38+
## Consequences
39+
40+
- Tests are reproducible: the same image runs locally and in CI, so "works on
41+
my machine" failures around Python version or collection layout disappear.
42+
- Contributors do not manage a local `ansible_collections` tree or build step by
43+
hand — the image does it on every run.
44+
- The Python version is a single build arg (`PYTHON_VER`), making it cheap to
45+
test against multiple interpreters.
46+
- Running tests requires Docker; the procedure and troubleshooting live in
47+
[../guides/running-tests.md](../guides/running-tests.md) and the conventions in
48+
[../guidelines/testing.md](../guidelines/testing.md).
49+
50+
## Alternatives Considered
51+
52+
- **Run `ansible-test` directly on the host**: rejected — fragile against
53+
Python-version and collection-path drift, and pollutes host state between
54+
runs.
55+
- **`tox` / `nox` virtualenv matrix without containers**: rejected — still
56+
depends on host-level interpreters and does not isolate the integration
57+
target's Infrahub dependency the way a container network does.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# ADR-0005: Generated Plugin Reference Documentation
2+
3+
**Status**: Accepted
4+
**Date**: 2026-02-25
5+
**Source**: `.specify/memory/constitution.md` — backfilled from existing code
6+
7+
## Context
8+
9+
Each plugin already carries its full interface in standard Ansible docstrings —
10+
`DOCUMENTATION`, `EXAMPLES`, and `RETURN` — which `ansible-test sanity`
11+
validates. The user-facing reference site (the Docusaurus build under `docs/`,
12+
published to <https://docs.infrahub.app/ansible/>) needs the same information as
13+
MDX pages. Maintaining a second, hand-written copy of every parameter and return
14+
value would guarantee drift: the docstrings are the source `ansible-test`
15+
enforces, so any prose duplicate of them would silently fall out of date.
16+
17+
## Decision
18+
19+
Generate the plugin reference MDX from the docstrings; never hand-edit the
20+
output. The `invoke generate-doc` task (`tasks/docs.py`) is the single pipeline.
21+
22+
- `generate_docs` discovers plugin files under `plugins/{modules,inventory,
23+
lookup}/`, parses `DOCUMENTATION` / `EXAMPLES` / `RETURN` out of each, and
24+
renders them through the Jinja2 templates in `docs/_templates/`
25+
(`plugin.mdx.j2` and the readme template).
26+
- Output is written to `docs/docs/references/plugins/<plugin>_<type>.mdx` and the
27+
landing page `docs/docs/readme.mdx`, stamped with the collection version from
28+
`galaxy.yml` and the `requires_ansible` value.
29+
- A `mdx_safe` Jinja2 filter escapes JSX-reserved braces in prose (so a
30+
docstring `{var}` is not parsed as a JSX expression) while leaving Markdown
31+
code spans untouched.
32+
- `invoke docusaurus` then builds the static site (`npm run build`) from the
33+
generated MDX.
34+
35+
## Consequences
36+
37+
- The docstrings are the single source of truth; the reference site cannot drift
38+
from the validated interface.
39+
- Files under `docs/docs/references/plugins/` and `docs/docs/readme.mdx` are
40+
generated artifacts and must never be edited directly — fix the docstring in
41+
`plugins/modules/*.py` or the template in `docs/_templates/` and regenerate.
42+
- Changing a docstring obliges the author to run `invoke generate-doc`; CI also
43+
regenerates and commits the docs on release
44+
(`workflow-changelog-and-docs.yml`).
45+
- The rules and pipeline detail live in
46+
[../guidelines/documentation.md](../guidelines/documentation.md) and
47+
[../guidelines/module-docstrings.md](../guidelines/module-docstrings.md).
48+
49+
## Alternatives Considered
50+
51+
- **Hand-written reference pages**: rejected — duplicates the docstrings and
52+
drifts from the interface `ansible-test sanity` validates.
53+
- **`antsibull-docs` to build a standalone Ansible doc site**: rejected — the
54+
project standardised on a single Docusaurus site, so a bespoke Jinja2 → MDX
55+
step keeps all docs (reference and narrative) in one toolchain.

dev/constitution.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Constitution
2+
3+
The binding project principles live in
4+
[`.specify/memory/constitution.md`](../.specify/memory/constitution.md) — the
5+
single source of truth maintained through the spec-kit workflow. This pointer
6+
exists so `dev/` links resolve; do not duplicate the body here, to avoid drift.

dev/guidelines/documentation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ CI runs Vale automatically on documentation changes (`.github/workflows/workflow
147147

148148
## Markdown Linting
149149

150-
Configuration in `.markdownlint.yml`. Ensures consistent markdown formatting.
150+
Configuration in `.markdownlint.yaml`. Ensures consistent markdown formatting.
151151

152152
## What to Document
153153

0 commit comments

Comments
 (0)