Skip to content

Cheat sheet — Modern PowerShell Toolchain & Module Skeleton

Companion to Module 01 — Modern Toolchain & Module Skeleton · CC BY 4.0 — print it, pin it, share it.

Last reviewed: 2026-07

Module scaffold — manifest, root module, exports

New-ModuleManifest ./Vigil/Vigil.psd1 `           # create the manifest (.psd1)
    -RootModule 'Vigil.psm1' `
    -ModuleVersion '0.1.0' `
    -FunctionsToExport @('Get-VigilEvent') `      # EXPLICIT exports — never '*'
    -PowerShellVersion '7.4'
Test-ModuleManifest ./Vigil/Vigil.psd1            # validate the manifest resolves
Import-Module ./Vigil/Vigil.psd1 -Force           # load your module (re-load with -Force)
Get-Command -Module Vigil                          # confirm only the intended functions export
Remove-Module Vigil                                # unload

A module is Vigil/Vigil.psd1 (manifest) + Vigil/Vigil.psm1 (root module that dot-sources Public//Private/ functions). FunctionsToExport is the boundary — list functions explicitly so the manifest is your public API, not everything you happened to define.

PSScriptAnalyzer — the linter/gate

Install-Module PSScriptAnalyzer -Scope CurrentUser         # once
Invoke-ScriptAnalyzer -Path ./Vigil -Recurse               # lint the module
Invoke-ScriptAnalyzer -Path ./Vigil -Recurse -Settings ./PSScriptAnalyzerSettings.psd1
Invoke-ScriptAnalyzer -Path . -Severity Error,Warning      # gate on these severities
Invoke-ScriptAnalyzer -Path . -EnableExit                  # exit non-zero on findings (CI gate)

PSScriptAnalyzerSettings.psd1:

@{
    Severity = @('Error','Warning')
    ExcludeRules = @()                 # keep empty; suppress at the line with justification instead
}

Suppress one finding at the source, with a reason — not by disabling the rule globally:

[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
    'PSAvoidUsingWriteHost','', Justification='interactive banner only')]
param()

Rules the copilot trips most: PSAvoidUsingInvokeExpression, PSAvoidUsingWriteHost, PSUseDeclaredVarsMoreThanAssignments, PSUseShouldProcessForStateChangingFunctions.

Pester v5 — one real test

Install-Module Pester -Scope CurrentUser -Force            # v5+
# Vigil.Tests.ps1
Describe 'Get-VigilEvent' {
    BeforeAll { Import-Module $PSScriptRoot/Vigil/Vigil.psd1 -Force }
    It 'reproduces the baseline summary byte-for-byte' {
        $out = Get-VigilEvent -Path $PSScriptRoot/data/sample.json | Out-String
        $out | Should -Be (Get-Content $PSScriptRoot/data/baseline.txt -Raw)
    }
}
Invoke-Pester -Path ./Vigil.Tests.ps1                       # run tests
Invoke-Pester -CI                                           # CI mode: exit code + test-results.xml
$c = New-PesterConfiguration; $c.Run.Exit = $true; Invoke-Pester -Configuration $c

The CI gate (GitHub Actions, pwsh)

jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - shell: pwsh
        run: |
          Set-PSResourceRepository PSGallery -Trusted        # PSResourceGet (PS 7.4+)
          Install-PSResource PSScriptAnalyzer, Pester -TrustRepository
          Invoke-ScriptAnalyzer -Path . -Recurse -EnableExit -Settings ./PSScriptAnalyzerSettings.psd1
          Invoke-Pester -CI

Strangler-fig migration — the order

1. Reproduce the loose script's output  → baseline.txt (ground truth)
2. Scaffold Vigil.psd1 + Vigil.psm1     → wrap hunt.ps1 UNCHANGED behind Get-VigilEvent
3. Prove Import-Module reproduces baseline byte-for-byte   (no refactor yet)
4. Add PSScriptAnalyzer + one Pester test → green
5. Wire CI → prove it FAILS on a planted Invoke-Expression, then fix
6. Refactor ONE slice behind the green gate → re-prove baseline + gate
7. Rollback is always the previous commit

pwsh 7 vs Windows PowerShell 5.1

$PSVersionTable.PSVersion            # 7.x = PowerShell 7 (pwsh); 5.1 = Windows PowerShell
pwsh -NoProfile -Command '…'         # run a scriptblock in a clean pwsh

Target pwsh 7: cross-platform, current, and where ForEach-Object -Parallel, ternary, and SecretManagement live. Windows PowerShell 5.1 is the copilot's default — and this track's anti-pattern.

The ADR skeleton (Module also delivers this)

# ADR-001: PowerShell module toolchain
## Context      — a loose hunt.ps1; no manifest, no gate; PSGallery doesn't mandate signing
## Options      — loose .ps1 · script module + gate · compiled binary module
## Decision     — script module (Vigil) + PSScriptAnalyzer + Pester gated in CI, pwsh 7
## Consequences — 5.1-only hosts need pwsh installed; the honest downside you accept

Comments

Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).