This file is for coding agents working in this repository.
- Use this guide for automated edits related to module intake, CI behavior, and compatibility test execution.
- Keep user-facing documentation in
README.mdand contributor process inCONTRIBUTING.md.
This harness exists to answer one question per module: does it work with Puppet Core? To
find out, the runner deliberately swaps OpenVox for Puppet Core and runs the module's own
tests — lib/module_tester/bootstrap.rb comments out the module's runtime gem declarations and
pins gem 'puppet', '= <version>' from the Puppet Core source.
Because of this, a module declaring openvox (not puppet) in metadata.json/Gemfile is
the NORMAL, expected input — most modern Vox Pupuli modules do, since their upstream CI
targets OpenVox. An openvox declaration is not an "OpenVox-only" incompatibility, and it
must not be pre-emptively marked incompatible.
- A missing
puppetrequirement inmetadata.jsonsurfaces only as a metadata warning (conditionally_compatibleunder the defaultwarnmode — seelib/module_tester/metadata.rbandlib/module_tester/classifier.rb). It is never a blocking failure or an exclusion. - A module is genuinely OpenVox-only (→ use the
mark-incompatibleskill) only when it cannot produce a usable result on Puppet Core, i.e. one of:- it has a hard runtime check that refuses non-OpenVox distributions (e.g.
puppet-choriaraising "Choria only supports OpenVox"), or - its purpose is to install/bootstrap OpenVox packages, so even a passing test describes no
valid Puppet Core use case (e.g.
puppet-openvox_bootstrap).
- it has a hard runtime check that refuses non-OpenVox distributions (e.g.
- Merely declaring
openvoxin metadata/Gemfile, when the provider/manifests otherwise run under Puppet Core, is not sufficient to mark incompatible. When in doubt, add the module and let a run produce the evidence — a warning is a pass (green), only failures are red.
config/modules.json: module definitions used by local runs and CI matrix generation.config/modules.schema.json: schema for module config validation.config/beaker/setfiles/: Beaker host definition files (one per acceptance target, e.g.el9.yml).scripts/validate_modules_config.py: local schema validation helper..github/workflows/compatibility-runner.yml: CI pipeline and matrix execution.profiles/puppet_profiles.json: profile constraints used by the runner.docs/architecture-flow.md: end-to-end architecture diagram and stage reference. Must be kept in sync with runner logic, classification rules, and CI workflow changes.
- A request may arrive as a GitHub issue URL instead of a repo name (the
add-moduleskill handles crawling it for the target repo/ref — see.claude/skills/add-module/SKILL.md). Resolve it to a concrete repo/ref before starting step 1; do not guess the target repo from the issue title alone if the body is ambiguous. - Inspect the target module repository first (do not skip this step). If the module is not present locally, agents MUST fetch and analyze the remote repository (e.g., via GitHub API or by cloning/downloading the repo) to discover acceptance tests and other required files. Acceptance test discovery must NOT be limited to the local workspace.
- Add a module object under
modulesinconfig/modules.json. - Insert Vox Pupuli modules first and alternative maintainers second. Keep all
voxpupuli/*entries grouped at the top and sorted alphabetically by repo name (the segment after the final/, lowercase, case-insensitive). Keep all non-voxpupulientries grouped at the bottom and sorted alphabetically by explicitid. This preserves a clean primary Vox Pupuli block while keeping alternative maintainer jobs easy to identify in the GitHub Actions UI. - Set
repo(required) andacceptance(required — see below), optionallyref,id,os, andprereqs. - Default behavior when omitted:
ref: treated asmainby runner logic.os: defaults toubuntu-latestin workflow behavior.
id: derived from repo name forvoxpupuli/*entries when omitted.
- Validate against schema before proposing completion.
- Do not hand-edit Available Acceptance Tests — it is auto-generated from
config/modules.jsonbyscripts/render_acceptance_audit.py(CI regenerates and commits it). The module'sacceptanceblock (status + reason) is the source of truth for that audit, so getting the disposition right in step 4 above is what populates the doc. You may runpython scripts/render_acceptance_audit.pylocally to preview the result.
Every module must declare an acceptance block with an explicit status — this is what lets the status ledger and dashboard distinguish "no acceptance tests exist" from "tests exist but the harness can't run them." Choose the status from what you found inspecting the upstream repo:
running— the repo has acceptance tests and we run them in CI. Set"enabled": trueand providetargets. (enabledmust betrueiffstatusisrunning.)"acceptance": { "enabled": true, "status": "running", "targets": [ { "name": "el9", "setfile": "el9" } ] }
blocked— acceptance tests exist upstream but cannot run in this harness due to a hard technical limitation (kernel params, multi-container topology, non-Docker OS, etc.). Set"enabled": falseand areason. Blocked modules are automatically excluded from the generatedKNOWN_COMPATIBLE.md(they have not had all available tests exercised) — setting thestatuscorrectly is what drives that; do not hand-editKNOWN_COMPATIBLE.md, it is generated.pending— acceptance tests exist upstream but are not yet wired into the harness (e.g. Windows-only targets we don't have runners for). Set"enabled": falseand areason.none— the upstream repo has no acceptance tests. Unit coverage alone is full coverage. Set"enabled": false; noreasonneeded."acceptance": { "enabled": false, "status": "none" }
blocked and pending require a reason, which is the source of truth for the acceptance-test audit. When a module is blocked/pending, add its reason here rather than only in prose docs.
If the upstream module is no longer maintained (deprecated/archived by its maintainer or on the Forge), set "deprecated": true on the module entry. This is orthogonal to compatibility — a deprecated module can still be fully compatible and remains in the test matrix; the flag only drives a STATUS.md. Omit the field (or set false) for maintained modules. Do not hand-edit STATUS.md.
- Primary block: all
voxpupuli/*entries, sorted by the last path segment ofrepo(for examplepuppet-archivefromhttps://github.com/voxpupuli/puppet-archive), compared case-insensitively. - Secondary block: all non-
voxpupulientries, sorted by explicitid, compared case-insensitively. - Every non-
voxpupulientry must setidexplicitly. - This applies to every entry — new additions, reorderings, duplicate-maintainer support, and cleanup edits.
- If you discover existing entries out of order during an edit, fix the order in the same change.
- When adding an alternative-maintainer version of a module already present from Vox Pupuli or another maintainer, append it to the non-
voxpupuliblock with a disambiguatingidthat clearly identifies the maintainer.
- Set
osonly when a module truly requires a specific runner image. - Use
windows-latestfor Windows-only modules/providers. - Use
macos-latestonly when explicitly required. - Omit
osfor general modules to keep Ubuntu as default. - Do not guess system package prerequisites.
- Determine
prereqsfrom repository evidence before finalizing a module addition. - Omit
idforvoxpupuli/*entries unless stable custom naming is needed in artifacts/reporting. - Set explicit
idfor every non-voxpupulientry. Use a maintainer-qualified value so matrix job names remain unambiguous when duplicate module names exist across maintainers. - Set
docker_modetosystemdon acceptance targets only when the module's acceptance tests assert service running/enabled state via systemd (e.g.is_expected.to be_running,is_expected.to be_enabled). Evidence: checkspec/acceptance/forbe_runningorbe_enabledmatchers on Service resources. - Default
docker_modeissshd— faster, more portable, and avoids privileged containers. Only escalate tosystemdwhen tests require it.
Before proposing a new module entry, agents must fetch and analyze the target repository at the selected ref (or default branch if ref is omitted). This includes searching for acceptance tests (e.g., files under spec/acceptance/ or similar) in the remote repository if not present locally. Agents must not assume the absence of acceptance tests based solely on the local workspace contents.
Minimum files/signals to inspect:
GemfileandGemfile.lockRakefileand custom rake tasks used by validate/spec/testmetadata.json.fixtures.ymlor.sync.yml(if present)spec/spec_helper.rb,spec/spec_helper_local.rb, and unit/integration specs underspec/- acceptance helpers/assets under
spec/acceptance/oracceptance/ - Any scripts invoked by tests (for example in
script/,tasks/, or CI config)
What to extract:
- Native/system tools and libraries required by tests or providers.
- OS-specific requirements (Linux/macOS/Windows) that imply package-manager entries.
- Commands in specs/rake tasks that call external binaries.
How to write prereqs:
- Add only package-manager keys supported by schema (
apt,dnf,yum,apk,brew,choco,pacman). - Include only non-empty unique package names.
- If repo evidence shows no system prereqs,
prereqsmay be omitted. - If evidence is inconclusive, run a scoped CI attempt and derive prereqs from failure logs before finalizing.
Evidence requirement in agent response:
- Summarize which repository files were inspected.
- State why
prereqswere added or omitted. - Call out any assumptions and required follow-up if evidence was partial.
Run:
python -m pip install jsonschema
python scripts/validate_modules_config.py --config config/modules.json --schema config/modules.schema.jsonExpected result:
OK: config/modules.json is valid against config/modules.schema.json
Also confirm:
- No duplicate
idvalues. - No unexpected module keys outside schema.
- Package lists in
prereqsare non-empty strings without duplicates.
When you need a narrow CI run, use workflow input modules_json with only new or changed entries, for example:
[{"repo":"https://github.com/voxpupuli/puppet-windowsfeature","ref":"master","os":"windows-latest"}]- Workflow validates
config/modules.jsonbefore matrix fan-out. - Matrix
runs-onfollows per-moduleoswhen set; otherwise Ubuntu default applies. - Cross-platform prereqs are installed by package-manager keys in
prereqs(such asapt,choco,brew). - Acceptance jobs always run on
ubuntu-latest; the SUT is a Docker container controlled byBEAKER_SETFILE. - When
PUPPET_CORE_API_KEYis set, the runner uses a two-stage isolation model:- Build stage (
build_sut_image): runsdocker buildwith the API key passed as a build arg. Puppet Core agent is installed and credentials are scrubbed from repo config files in the same Dockerfile layer so they never persist in the image. - Test stage (
acceptance): runs untrusted module test code (Beaker/rspec) with no secrets in the environment. The runner stripsPUPPET_CORE_API_KEY,PASSWORD,USERNAME, andBUNDLE_RUBYGEMS___PUPPETCORE__PUPPET__COMfrom the env before invoking tests. The setfile references the pre-built local image tag — no credentials are embedded in the setfile.
- Build stage (
- This design ensures third-party module test code cannot exfiltrate the API key.
- Without an API key, acceptance falls back to the public FOSS puppet-agent from
yum.puppet.com(capped at 8.10.0).
- Each acceptance target in
config/modules.jsonreferences asetfileby name (filename stem). - Corresponding YAML files live under
config/beaker/setfiles/(e.g.el9→config/beaker/setfiles/el9.yml). - The setfile defines the Docker image and platform for the Beaker SUT.
- At runtime, the runner builds a Docker image from the base setfile parameters (image, platform, docker_image_commands) plus Puppet Core install steps, then writes a clean setfile to
workspace/.beaker-setfiles/that references the locally-built image tag. - The rewritten setfile contains no secrets — only the image tag and platform metadata.
- When adding a new target OS, create the setfile first, then reference it in
modules.json.
docs/architecture-flow.md contains a Mermaid flow diagram and supporting reference tables that describe the end-to-end pipeline. Agents must update this file when making changes to any of the following areas:
| Area changed | What to update in architecture-flow.md |
|---|---|
Runner pipeline stages (lib/module_tester/runner.rb) |
Shared pipeline stage list; diagram node labels and order |
Bootstrap logic or Gemfile patching (lib/module_tester/bootstrap.rb) |
Gem swap branch in diagram; Bootstrap row in stage table; Gemfile conflict override in classification table |
Classifier state logic or precedence (lib/module_tester/classifier.rb) |
Classification precedence list; outcome state table |
Downgrade override rules (lib/module_tester/adapters.rb) |
Downgrade overrides table — add, remove, or update trigger conditions and reclassification outcome |
Guardrails checks (lib/module_tester/guardrails.rb) |
Guardrails row in stage table |
Acceptance adapter or Docker isolation model (lib/module_tester/adapters.rb, lib/module_tester/docker.rb) |
Two-Stage Docker Isolation Model section; Docker Container Modes table; S1/S2S/S2D node labels in diagram; FOSS fallback description |
CI workflow (github/workflows/compatibility-runner.yml) |
CI: Prepare section; diagram CI subgraph |
Reporting outputs (lib/module_tester/reporting.rb) |
Reporting section |
- Use
<br/>for line breaks inside Mermaid node labels — not\n. - Do not remove
classDefdeclarations or theclass/styleassignments at the bottom of the diagram block; they encode deliberate visual highlights formodules.json(datasource), gem swap (gemswap), and Docker isolation (isolation/TwoStage). - If a new architectural concept warrants a highlight, add a new
classDefand apply it consistently. - Keep the diagram and the prose tables in sync — if the diagram changes, the corresponding prose section must also change.
- Keep README user-focused and free from agent-operational instructions.
- Keep CONTRIBUTING focused on contributor process and schema rules.
- Put agent-specific process updates in this file.
Before ruling a module incompatible, re-read Project Purpose above. In particular, an
openvox-in-metadata.json declaration (with no puppet requirement) is a warning, not an
incompatibility — do not mark such a module incompatible on that basis alone. Reserve
"OpenVox-only" for the two genuine cases named there (hard runtime refusal of non-OpenVox, or a
module whose purpose is installing OpenVox).
When a module is determined to be incompatible:
- Add an entry to the table in KNOWN_INCOMPATIBLE.md with module name, Puppet Core version tested, status, and detailed reason
- Remove the module from
config/modules.jsonso it is no longer included in test runs - Include migration path guidance if applicable