Skip to content

Commit abaa7b2

Browse files
imaqsoodclaude
andcommitted
(MODULES-11829) Add synced CLAUDE.md.erb (Linux + Windows) via moduleroot_init
Ship a module-shaped CLAUDE.md so CAT-supported modules get a conventions file Claude reads before touching code. Delivers both MODULES-11829 (the template + Linux specifics) and MODULES-11830 (Windows specifics). - Lives in moduleroot_init/ (not moduleroot/), so it is a one-time default: new modules get it via `pdk new module` / `pdk convert` and own it -- `pdk update` never overwrites it, and modules that already hand-maintain a CLAUDE.md are never clobbered. - OS-conditional via the module's metadata.json, read as @configs['module_metadata']['operatingsystem_support'] with rescue fallbacks (same convention as moduleroot_init/CHANGELOG.md.erb / README.md.erb). Renders "Linux specifics" and/or "Windows specifics"; empty/unknown metadata defaults to Linux. - Linux: package providers (apt/yum/dnf/zypper), systemd vs init, paths/SELinux, Litmus-on-Docker. Windows: Chocolatey/MSI + SCM services + ACL/paths, DSC resource patterns, PowerShell exec idioms, scheduled_task semantics, native-extension gotchas, and Windows Litmus/WinRM/reboot notes. - Common content: project layout, rake commands, rspec-puppet (facterdb via rspec-puppet-facts/on_supported_os), coverage, litmus/beaker (incl. spec_helper_acceptance.rb), Ruby style (rubocop strict, run under bundle exec), Gemfile gem sourcing + version-pin env vars, type/provider DSL gotchas, task-module conventions, metadata.json, CI/nightly logs, outcome-based review policy, and project rules paired with .claude runtime hooks. - Header frames it as a starting point to tailor per repo, not an immutable file. - Pure ASCII (PDK's diff engine raises invalid-byte-sequence on non-ASCII under a non-UTF-8 locale). Validated with PDK 3.4.0: `pdk new module` deploys the file (owned, customizable); `pdk update` leaves an existing copy untouched; renders Linux/Windows sections per metadata.json; ERB balanced, no leftover tags. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 34129f9 commit abaa7b2

1 file changed

Lines changed: 352 additions & 0 deletions

File tree

moduleroot_init/CLAUDE.md.erb

Lines changed: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
1+
<%-
2+
# OS detection from the module's metadata.json (exposed by PDK as
3+
# @configs['module_metadata']). Falls back safely when metadata is absent
4+
# (e.g. `pdk new module` before the interview) -- default to Linux content.
5+
md = (@configs['module_metadata'] rescue {}) || {}
6+
os_support = (md['operatingsystem_support'] rescue []) || []
7+
os_names = os_support.map { |o| ((o['operatingsystem'] || '').to_s.downcase rescue '') }
8+
linux_names = %w[redhat centos oraclelinux scientific sles suse opensuse debian ubuntu rocky almalinux amazon fedora virtuozzolinux gentoo archlinux]
9+
has_windows = os_names.include?('windows')
10+
# Empty/unknown metadata (e.g. a fresh `pdk new module`) defaults to the Linux section. Modules that
11+
# support only non-Linux, non-Windows platforms (AIX/Solaris/Darwin) render neither OS section and
12+
# rely on the OS-agnostic guidance above -- rather than being mislabelled as Linux.
13+
has_linux = os_names.empty? || os_names.any? { |n| linux_names.include?(n) }
14+
-%>
15+
# CLAUDE.md
16+
17+
This file provides guidance to Claude Code (claude.ai/code) and human contributors when
18+
working with code in this repository.
19+
20+
> **Starting point, not a straitjacket.** This file ships from
21+
> [puppetlabs/pdk-templates](https://github.com/puppetlabs/pdk-templates) as a baseline of conventions
22+
> common to CAT-supported modules. **Tailor it to this repo** -- add the specifics Claude actually
23+
> needs: what the manifests/types/providers/functions here do, module-specific gotchas, and anything
24+
> learned while working in the codebase. A generic framework alone is not enough to guide good changes.
25+
>
26+
> PDK adds this file when a module is first created (`pdk new module`, or `pdk convert` for a module
27+
> that doesn't have one yet) and then leaves it alone -- it is **not** re-synced or overwritten on
28+
> `pdk update`. It is yours to edit freely; keep it current as the module grows.
29+
30+
For generic Puppet/PDK workflow that *is* derivable from the files themselves (the full PDK command
31+
reference, CI matrix mechanics, README badges), see the PDK docs and the README. The notes below are
32+
the conventions Claude should treat as binding before touching code, plus the things that are not
33+
obvious from a single file.
34+
35+
---
36+
37+
## Project layout
38+
39+
A CAT-supported Puppet module uses some subset of this structure -- read what actually exists before
40+
assuming. Modules come in a few shapes: **manifest modules** (classes/defined types), **type/provider
41+
modules** (custom resources in `lib/puppet/`), and **task modules** (Bolt tasks, often no manifests).
42+
43+
```
44+
manifests/ # Puppet DSL: classes and defined types (.pp)
45+
lib/puppet/type/ # Custom resource type definitions (Ruby)
46+
lib/puppet/provider/<type>/ # Providers implementing each type per OS/tool
47+
lib/puppet/functions/ # Modern Puppet 4.x function API (Ruby)
48+
lib/facter/ # Custom facts
49+
lib/puppet_x/ # Shared helper libraries (parsing, utilities)
50+
functions/ # Puppet-language functions (.pp)
51+
tasks/ # Bolt tasks (<name>.{rb,sh,ps1} + <name>.json schema)
52+
plans/ # Bolt plans (.pp)
53+
files/ # Static files / shell helpers shipped with the module
54+
data/ + hiera.yaml # Module Hiera data (per-OS defaults)
55+
templates/ # ERB/EPP templates
56+
spec/ # Tests (see Testing below)
57+
metadata.json # Module metadata, dependencies, supported OS matrix
58+
REFERENCE.md # Generated by `puppet strings` -- do not hand-edit
59+
```
60+
61+
- Manifests orchestrate; for type/provider modules the real work lives in `lib/puppet/`.
62+
- Prefer per-OS defaults in `data/` (Hiera, keyed by `os.family`/`os.name`) over hard-coding in `params.pp`.
63+
- Regenerate reference docs: `bundle exec puppet strings generate --format markdown --out REFERENCE.md`.
64+
65+
---
66+
67+
## Common commands
68+
69+
```bash
70+
bundle install # Install dependencies
71+
bundle exec rake spec_prep # Install fixture modules from .fixtures.yml (before specs)
72+
bundle exec rake spec # Run all unit tests
73+
bundle exec rake parallel_spec # Run unit tests in parallel (faster on large suites)
74+
bundle exec rake lint # puppet-lint
75+
bundle exec rake lint_fix # Auto-fix lint offences
76+
bundle exec rake rubocop # Ruby style checks
77+
bundle exec rake validate # Syntax-check Ruby, Puppet manifests, and metadata
78+
bundle exec rake syntax # Puppet manifest + Hiera syntax check
79+
bundle exec rake metadata_lint # Validate metadata.json
80+
bundle exec rake release_checks # Full pre-release gate (validate + lint + spec)
81+
82+
# Run a single spec file / example
83+
bundle exec rspec spec/unit/puppet/provider/<type>/<provider>_spec.rb
84+
bundle exec rspec spec/unit/puppet/type/<type>_spec.rb:42 # by line number
85+
bundle exec rspec spec/.../foo_spec.rb -e "creates the resource" # by description
86+
```
87+
88+
Acceptance tests (Litmus; require Docker or provisioned targets):
89+
90+
```bash
91+
bundle exec rake litmus:provision_list[default]
92+
bundle exec rake litmus:install_module
93+
bundle exec rake litmus:acceptance:parallel
94+
```
95+
96+
---
97+
98+
## Testing conventions
99+
100+
### rspec-puppet (unit, for manifests)
101+
102+
- Every class/defined type should have `it { is_expected.to compile.with_all_deps }` -- a clean
103+
compile across the supported OS matrix is the baseline gate.
104+
- Drive OS variants with `on_supported_os` (reads `metadata.json`) rather than hand-listing facts.
105+
- Facts come largely from **facterdb** (via `rspec-puppet-facts` / `on_supported_os`) -- you rarely
106+
need to hand-build fact hashes; override only the specific facts a test needs on top of the OS set.
107+
- Stub facts via the `:facts` hash or `spec/default_facts.yml`; do not rely on the host's real facts.
108+
- Provide Hiera test data through `spec/fixtures/hiera/` and a test `hiera.yaml` when behaviour is
109+
data-driven. Use `let(:params)` / `let(:pre_condition)` for class params and dependencies.
110+
- Fixture modules are declared in `.fixtures.yml` and installed by `rake spec_prep`.
111+
112+
### Type/provider unit specs
113+
114+
- `spec/unit/puppet/type/` -- attribute validation, `munge`/`validate`, `autorequire` wiring.
115+
- `spec/unit/puppet/provider/<type>/` -- CRUD behaviour. Stub the external CLI/syscalls; never shell
116+
out to the real tool. Assert the *commands generated* and the *state parsed*.
117+
- **Mocking framework varies per module** -- check `spec/spec_helper.rb`'s `mock_with`. Some modules
118+
use Mocha (`stub_everything`, `stubs`, `expects`); others use rspec-mocks (`allow`, `expect`,
119+
`receive`). Match the module's existing convention.
120+
121+
### Coverage
122+
123+
```bash
124+
COVERAGE=yes bundle exec rspec # SimpleCov report
125+
```
126+
127+
- `RSpec::Puppet::Coverage.report!` runs from the PDK-managed spec helper; the minimum percentage is
128+
set in `.sync.yml` / `config_defaults.yml`.
129+
- SimpleCov `track_files` and any extra coverage config belong in `spec/spec_helper_local.rb`
130+
(`lib/**/*.rb` for ruby modules, `tasks/**/*.rb` for task modules) -- not in `spec_helper.rb`.
131+
- Do not weaken the coverage gate to make a build pass.
132+
133+
### Litmus / beaker (acceptance)
134+
135+
- Acceptance specs live in `spec/acceptance/` and run against Litmus-provisioned targets. They must be
136+
idempotent: apply the manifest, then assert a second apply is a no-op
137+
(`apply_manifest(pp, catch_changes: true)`).
138+
- Keep acceptance coverage on real behaviour (package installed, service running, file present) using
139+
`serverspec` matchers -- not a re-test of unit-level logic.
140+
141+
### Spec helpers
142+
143+
- `spec/spec_helper.rb` is **PDK-managed -- do not edit**; it is overwritten on `pdk update`.
144+
- Put project-local unit setup in `spec/spec_helper_local.rb` (safe to edit).
145+
- `spec/spec_helper_acceptance.rb` (+ `spec/spec_helper_acceptance_local.rb`) drives the Litmus
146+
acceptance run; put acceptance-only helpers there, not in the unit spec helper.
147+
148+
---
149+
150+
## Ruby code style
151+
152+
- Style is enforced by `.rubocop.yml` (deployed from pdk-templates, `strict` profile). Run
153+
`bundle exec rake rubocop` (or `bundle exec rubocop -A` to autocorrect) before pushing -- always
154+
under `bundle exec` so you get the module's pinned RuboCop version; do not hand-tune cops in the synced file.
155+
- Conventions the profile enforces: `Layout/LineLength` max 200, `snake_case` naming, `%r{}` for regex
156+
literals, trailing commas on multiline arrays/args, and the `rubocop-performance` / `rubocop-rspec`
157+
extensions. Target Ruby version follows `.rubocop.yml`'s `TargetRubyVersion`.
158+
159+
---
160+
161+
## Gemfile, gem sources and version pinning
162+
163+
- The `Gemfile` is PDK-managed. It resolves `puppet`, `facter`, `hiera` (and `bolt`) gems through a
164+
configurable source -- do not hard-code gem versions in it.
165+
- Environment knobs: `PUPPET_GEM_VERSION`, `FACTER_GEM_VERSION`, `HIERA_GEM_VERSION`, `BOLT_GEM_VERSION`
166+
pin versions; `GEM_SOURCE` overrides the default RubyGems source; `GEM_SOURCE_PUPPETCORE` (set
167+
automatically when `PUPPET_FORGE_TOKEN` is present) selects the authenticated PuppetCore registry.
168+
- The `:system_tests` group holds Litmus/serverspec; `:development` holds rspec, lint, and coverage gems.
169+
- In code and specs, depend on the public Puppet API (`Puppet::ResourceApi`, `Puppet::Type`, `Facter`),
170+
never on a specific gem name, so the module works whichever runtime gem is resolved.
171+
172+
---
173+
174+
## Type & provider DSL gotchas
175+
176+
Applies to modules that ship custom resources under `lib/puppet/`:
177+
178+
- **`autorequire`** -- declare implicit ordering (e.g. a package before the service that uses it) in
179+
the type, rather than forcing users to write `require =>` everywhere.
180+
- **`prefetch`** -- providers listing existing resources implement `self.instances` and
181+
`self.prefetch(resources)`. `prefetch` must match discovered instances back to catalog resources by
182+
name/namevar so Puppet edits instead of recreating. Missing this causes duplicate-resource churn.
183+
- **`self.instances`** -- return only fully populated instances; partial instances break resource
184+
purging (`resources { 'x': purge => true }`).
185+
- **Idempotency** -- getters read live state, setters change it. A second run with no manifest change
186+
must report zero changes. This is the single most common review rejection.
187+
- **Resource API vs legacy** -- prefer `Puppet::ResourceApi.register_type` for new types. If a module
188+
has already migrated (check the type files), do not reintroduce the legacy `Puppet::Type.newtype` pattern.
189+
- **Boolean munging** -- Puppet booleans arrive as `:true/:false`, `true/false`, or `'true'/'false'`.
190+
Normalise explicitly before passing to a shell command.
191+
- **`commands` / `optional_commands`** -- declare required binaries with `commands` (Puppet fails at
192+
apply time if missing); use `optional_commands` for tools that may be absent.
193+
194+
---
195+
196+
## Task module conventions
197+
198+
Applies to modules whose primary interface is Bolt tasks (`tasks/`):
199+
200+
- Each task is `<name>.{rb,sh,ps1}` plus a `<name>.json` metadata/parameter schema.
201+
- Ruby tasks use the Puppet ruby shebang `#!/opt/puppetlabs/puppet/bin/ruby` and read parameters as
202+
JSON from `$stdin`. When unit-testing, stub `$stdin`/`JSON.parse` before `require`-ing the file so
203+
the script-level dispatch block does not run on load.
204+
- Cross-platform tasks delegate to per-OS implementations (e.g. shell helpers in `files/`).
205+
206+
---
207+
208+
## metadata.json conventions
209+
210+
- `metadata.json` is the source of truth for the supported OS matrix; `on_supported_os` in specs and
211+
the CI platform list both derive from it. Update it when adding/removing platform support.
212+
- Bump `version` per [SemVer](https://semver.org): breaking change -> major, feature -> minor, fix -> patch.
213+
- Keep `dependencies` ranges current (e.g. `stdlib >= 9.0.0 < 10.0.0`); validate with `rake metadata_lint`.
214+
- Do not hand-edit machine-managed fields (`pdk-version`, `template-url`, `template-ref`).
215+
- `requirements` pins the supported Puppet range (typically `>= 8.0.0 < 9.0.0`); keep code and specs in step.
216+
217+
---
218+
219+
## CI & nightly logs
220+
221+
- CI lives in `.github/workflows/`. Many Puppetlabs modules use the shared workflows from
222+
[`puppetlabs/cat-github-actions`](https://github.com/puppetlabs/cat-github-actions) -- typically
223+
`ci.yml` (spec + acceptance on PRs to `main`), `nightly.yml` (scheduled runs against nightly
224+
builds), and `release.yml` / `release_prep.yml` -- but check what the module actually defines.
225+
- **Nightly failures:** check the module's **Actions -> "nightly"** runs on GitHub for per-job logs
226+
first. Nightlies often fail on infrastructure/provisioning rather than module code, so confirm the
227+
failure is code-related before changing anything.
228+
229+
---
230+
231+
<% if has_linux -%>
232+
## Linux specifics
233+
234+
- **Package providers**: behaviour differs by family -- `apt`/`dpkg` (Debian/Ubuntu), `yum`/`dnf`/`rpm`
235+
(RedHat/CentOS/Rocky/AlmaLinux/Oracle/Amazon), `zypper` (SLES/openSUSE). Don't assume one; gate on
236+
`$facts['os']['family']` and test each supported family.
237+
- **Service management**: modern targets are `systemd`; older ones use SysV `init`/`upstart`. Use the
238+
service provider rather than calling the init system directly.
239+
- **Paths**: Puppet ships under `/opt/puppetlabs/puppet` (ruby at `/opt/puppetlabs/puppet/bin/ruby`);
240+
config under `/etc`. Be SELinux-aware on RedHat-family targets (file contexts, `restorecon`).
241+
- **Acceptance**: Litmus provisions Linux targets as Docker containers; keep test manifests
242+
container-friendly (avoid hard reboots / kernel-level changes where possible).
243+
244+
<% end -%>
245+
<% if has_windows -%>
246+
## Windows specifics
247+
248+
Windows modules have a meaningfully different surface from Linux. Read these before touching
249+
iis/sqlserver/registry/scheduled_task/dsc-style modules.
250+
251+
### Platform basics
252+
253+
- **Package management**: Chocolatey, MSI, and the Windows installer providers -- not apt/yum. Gate
254+
provider-specific behaviour on `$facts['os']['family'] == 'windows'`.
255+
- **Services**: managed via the Windows SCM (`sc.exe` / the `service` provider), no systemd. Service
256+
names are case-insensitive; account for `LocalSystem`/`NetworkService`/named-account `logon` config.
257+
- **Paths**: Puppet installs under `C:\Program Files\Puppet Labs\Puppet` (ruby at
258+
`...\puppet\bin\ruby.exe`). Use backslash paths in resources and double them in Ruby string
259+
literals. Paths are case-insensitive and `\`-separated; normalise before comparing in providers so
260+
specs stay idempotent. CRLF line endings and BOM handling matter in file specs.
261+
- **Permissions**: there is no POSIX `mode`; model ACLs with the `puppetlabs/acl` module, not
262+
`file { mode => '0644' }`.
263+
264+
### DSC resource patterns
265+
266+
Applies to the generated `puppetlabs/dsc`-family modules (built from PowerShell DSC Resources):
267+
268+
- Types and providers under `lib/puppet/type/dsc_*.rb` and `lib/puppet/provider/dsc_*` are
269+
**machine-generated by the DSC builder -- never hand-edit them**; regenerate from the upstream PS
270+
module instead.
271+
- Resource and property names are mangled: PowerShell `MyResource`/`PropertyName` become
272+
`dsc_myresource` with `dsc_propertyname` parameters (lowercased, `dsc_`-prefixed).
273+
- Embedded CIM instances (e.g. `MSFT_Credential`) are passed as hashes; credentials use
274+
`{ 'user' => ..., 'password' => Sensitive('...') }` and **must** be wrapped in `Sensitive()` so the
275+
value never lands in logs or the catalog in clear text.
276+
- Idempotency is delegated to the resource's `Test-TargetResource`; the provider invokes it through
277+
the persistent PowerShell host (ruby-pwsh). Don't add a parallel `exec` to "fix" drift a DSC
278+
resource already manages.
279+
280+
### PowerShell exec idioms
281+
282+
- Prefer a real type/provider or a DSC resource over shelling out. When you must run PowerShell, use
283+
the `puppetlabs/powershell` `exec` provider: `exec { 'x': provider => 'powershell', ... }`.
284+
- Always make `exec` conditional and idempotent with `onlyif`/`unless` PowerShell that sets a non-zero
285+
exit code on the "needs change" path -- never an unconditional `command`.
286+
- Providers reuse a long-lived PowerShell process via `ruby-pwsh` (`Pwsh::Manager`) for performance;
287+
prefer it over spawning `powershell.exe` per call.
288+
- Quoting cmd/PowerShell nesting is error-prone: prefer here-strings or `-EncodedCommand` (base64) for
289+
non-trivial scripts, and pass `-NonInteractive -ExecutionPolicy Bypass` so runs don't hang or get
290+
blocked by machine policy.
291+
292+
### scheduled_task semantics
293+
294+
Applies to the `puppetlabs/scheduled_task` type:
295+
296+
- Default to the modern `taskscheduler_api2` provider; the legacy `win32_taskscheduler` provider
297+
exists for back-compat only -- don't reintroduce it for new work.
298+
- `trigger` is an array of hashes (`schedule`, `start_time`, `every`, `day_of_week`, ...). Most
299+
idempotency bugs come from trigger comparison: times are interpreted in the target's local timezone
300+
and round-tripped, so assert a second `apply` is a no-op in acceptance.
301+
- Set `compatibility` deliberately when a trigger type needs a newer Task Scheduler schema; a mismatch
302+
silently drops or rewrites triggers.
303+
304+
### Native-extension Ruby gotchas
305+
306+
- Windows providers often depend on native gems (`win32-service`, `win32-dir`, `win32-security`,
307+
`ffi`) that only load on Windows. Guard `require`s and Windows-only code paths with
308+
`Puppet.features.microsoft_windows?` so the file still loads on the Linux CI runner.
309+
- **Unit specs run on Linux** in CI even for Windows modules. Stub the Win32/FFI calls and
310+
`microsoft_windows?` rather than invoking the real API, and assert the *commands generated* and
311+
*state parsed* -- the same rule as any provider spec.
312+
- WinRM/UTF-16, CRLF, and backslash-path handling are common spec foot-guns; normalise in the code,
313+
not the test.
314+
315+
### Litmus / acceptance on Windows
316+
317+
- Windows targets are **not** Docker containers. Litmus provisions real VMs (vmpooler / ABS / cloud),
318+
so the Linux `litmus:provision_list[default]` Docker flow does not apply. Provision explicitly, e.g.
319+
`bundle exec rake 'litmus:provision[vmpooler, windows-2022-x86_64]'`.
320+
- The agent connects over **WinRM**, which is slower than SSH and occasionally flaky -- keep acceptance
321+
manifests tight and idempotent.
322+
- Reboots are real and sometimes required (installers, feature changes); orchestrate them with the
323+
`puppetlabs/reboot` module rather than ad-hoc `exec` restarts.
324+
325+
<% end -%>
326+
## Review policy (outcome-based)
327+
328+
PRs are judged on outcomes, not line counts:
329+
330+
- **Idempotency and a clean compile across the supported OS matrix are mandatory.** A change that
331+
passes unit specs but isn't idempotent in acceptance is not done.
332+
- New behaviour ships with the test that proves it (unit for logic, acceptance for real-system effect).
333+
- Don't reduce coverage or weaken the coverage gate to make a build pass.
334+
- Keep changes scoped to the ticket; flag unrelated cleanup separately.
335+
- Reference the driving `MODULES-*` (or relevant) ticket in the PR description.
336+
337+
---
338+
339+
## Project rules
340+
341+
- At the start of a session, review the repository structure and relevant README/REFERENCE before changing anything.
342+
- Always read the files relevant to the task before suggesting or making a change.
343+
- Never merge a pull request.
344+
- Never work directly on `main` or `master`.
345+
- Never push a branch without explicit instruction.
346+
- Never delete a file without permission -- this applies even after a blanket "yes to all".
347+
- Never output, log, save, or hardcode security-sensitive values -- passwords, tokens, API keys,
348+
private keys, secrets, or credentials of any kind. Do not write them to files, commit messages, or responses.
349+
350+
> These are guidance, not enforcement. For anything that must hold every run (secret hygiene, no
351+
> direct pushes to `main`, the coverage gate), back it with a runtime hook in the repo's `.claude/`
352+
> settings -- prose alone can be treated as a suggestion and skipped.

0 commit comments

Comments
 (0)