@@ -236,11 +236,82 @@ Applies to modules whose primary interface is Bolt tasks (`tasks/`):
236236<% if has_windows -%>
237237## Windows specifics
238238
239- - **Package management**: Chocolatey / MSI / Windows installer providers; not apt/yum.
240- - **Services**: managed via the Windows SCM (`sc.exe` / the service provider), no systemd.
241- - **Tasks**: PowerShell tasks are `< name > .ps1`; mind execution policy and quoting.
242- - **Paths**: Puppet installs under `C:\Program Files\Puppet Labs\Puppet`; use backslash paths and
243- `windows`-appropriate file resources. Line endings and case-insensitive paths matter in specs.
239+ Windows modules have a meaningfully different surface from Linux. Read these before touching
240+ iis/sqlserver/registry/scheduled_task/dsc-style modules.
241+
242+ ### Platform basics
243+
244+ - **Package management**: Chocolatey, MSI, and the Windows installer providers -- not apt/yum. Gate
245+ provider-specific behaviour on `$facts['os']['family'] == 'windows'`.
246+ - **Services**: managed via the Windows SCM (`sc.exe` / the `service` provider), no systemd. Service
247+ names are case-insensitive; account for `LocalSystem`/`NetworkService`/named-account `logon` config.
248+ - **Paths**: Puppet installs under `C:\Program Files\Puppet Labs\Puppet` (ruby at
249+ `...\puppet\bin\ruby.exe`). Use backslash paths in resources and double them in Ruby string
250+ literals. Paths are case-insensitive and `\`-separated; normalise before comparing in providers so
251+ specs stay idempotent. CRLF line endings and BOM handling matter in file specs.
252+ - **Permissions**: there is no POSIX `mode`; model ACLs with the `puppetlabs/acl` module, not
253+ `file { mode => '0644' }`.
254+
255+ ### DSC resource patterns
256+
257+ Applies to the generated `puppetlabs/dsc`-family modules (built from PowerShell DSC Resources):
258+
259+ - Types and providers under `lib/puppet/type/dsc_*.rb` and `lib/puppet/provider/dsc_*` are
260+ **machine-generated by the DSC builder -- never hand-edit them**; regenerate from the upstream PS
261+ module instead.
262+ - Resource and property names are mangled: PowerShell `MyResource`/`PropertyName` become
263+ `dsc_myresource` with `dsc_propertyname` parameters (lowercased, `dsc_`-prefixed).
264+ - Embedded CIM instances (e.g. `MSFT_Credential`) are passed as hashes; credentials use
265+ `{ 'user' => ..., 'password' => Sensitive('...') }` and **must** be wrapped in `Sensitive()` so the
266+ value never lands in logs or the catalog in clear text.
267+ - Idempotency is delegated to the resource's `Test-TargetResource`; the provider invokes it through
268+ the persistent PowerShell host (ruby-pwsh). Don't add a parallel `exec` to "fix" drift a DSC
269+ resource already manages.
270+
271+ ### PowerShell exec idioms
272+
273+ - Prefer a real type/provider or a DSC resource over shelling out. When you must run PowerShell, use
274+ the `puppetlabs/powershell` `exec` provider: `exec { 'x': provider => 'powershell', ... }`.
275+ - Always make `exec` conditional and idempotent with `onlyif`/`unless` PowerShell that sets a non-zero
276+ exit code on the "needs change" path -- never an unconditional `command`.
277+ - Providers reuse a long-lived PowerShell process via `ruby-pwsh` (`Pwsh::Manager`) for performance;
278+ prefer it over spawning `powershell.exe` per call.
279+ - Quoting cmd/PowerShell nesting is error-prone: prefer here-strings or `-EncodedCommand` (base64) for
280+ non-trivial scripts, and pass `-NonInteractive -ExecutionPolicy Bypass` so runs don't hang or get
281+ blocked by machine policy.
282+
283+ ### scheduled_task semantics
284+
285+ Applies to the `puppetlabs/scheduled_task` type:
286+
287+ - Default to the modern `taskscheduler_api2` provider; the legacy `win32_taskscheduler` provider
288+ exists for back-compat only -- don't reintroduce it for new work.
289+ - `trigger` is an array of hashes (`schedule`, `start_time`, `every`, `day_of_week`, ...). Most
290+ idempotency bugs come from trigger comparison: times are interpreted in the target's local timezone
291+ and round-tripped, so assert a second `apply` is a no-op in acceptance.
292+ - Set `compatibility` deliberately when a trigger type needs a newer Task Scheduler schema; a mismatch
293+ silently drops or rewrites triggers.
294+
295+ ### Native-extension Ruby gotchas
296+
297+ - Windows providers often depend on native gems (`win32-service`, `win32-dir`, `win32-security`,
298+ `ffi`) that only load on Windows. Guard `require`s and Windows-only code paths with
299+ `Puppet.features.microsoft_windows?` so the file still loads on the Linux CI runner.
300+ - **Unit specs run on Linux** in CI even for Windows modules. Stub the Win32/FFI calls and
301+ `microsoft_windows?` rather than invoking the real API, and assert the *commands generated* and
302+ *state parsed* -- the same rule as any provider spec.
303+ - WinRM/UTF-16, CRLF, and backslash-path handling are common spec foot-guns; normalise in the code,
304+ not the test.
305+
306+ ### Litmus / acceptance on Windows
307+
308+ - Windows targets are **not** Docker containers. Litmus provisions real VMs (vmpooler / ABS / cloud),
309+ so the Linux `litmus:provision_list[default]` Docker flow does not apply. Provision explicitly, e.g.
310+ `bundle exec rake 'litmus:provision[vmpooler, windows-2022-x86_64]'`.
311+ - The agent connects over **WinRM**, which is slower than SSH and occasionally flaky -- keep acceptance
312+ manifests tight and idempotent.
313+ - Reboots are real and sometimes required (installers, feature changes); orchestrate them with the
314+ `puppetlabs/reboot` module rather than ad-hoc `exec` restarts.
244315
245316<% end -%>
246317## Review policy (outcome-based)
0 commit comments