Skip to content

samhodgkinson/Powershell-Project-Base

Repository files navigation

PowerShell Module Base

A production-ready PowerShell module template. Clone, rename, and ship.

Gate Tool
Lint & style PSScriptAnalyzer (all severities)
Tests Pester 5 — JaCoCo coverage + JUnit XML
Security scan PSScriptAnalyzer security rules
Build Invoke-Build — monolithic .psm1 for distribution
Docs platyPS — markdown from comment-based help
CI GitHub Actions (Linux / Windows / macOS)
Publish PSGallery · GitHub Packages · ADO Artifacts

Quick Start

Dev Container (Recommended)

Open in VS Code and select "Reopen in Container". All tools install automatically.

Invoke-Build          # lint -> test -> build -> docs

Local

Set-PSRepository PSGallery -InstallationPolicy Trusted
Install-Module InvokeBuild, Pester, PSScriptAnalyzer, platyPS, Microsoft.PowerShell.PSResourceGet -Scope CurrentUser -Force
Invoke-Build

Rename the Template

# 1. Rename source directory and files
Rename-Item src/MyModule          src/YourModule
Rename-Item src/YourModule/MyModule.psd1  YourModule.psd1
Rename-Item src/YourModule/MyModule.psm1  YourModule.psm1

# 2. Generate a new GUID and paste it into YourModule.psd1
(New-Guid).Guid

# 3. Update .build.ps1 param: $ModuleName = 'YourModule'
# 4. Update tests: fix the Import-Module path in BeforeAll blocks

Repository Layout

.
├── src/MyModule/
│   ├── MyModule.psd1         manifest  (GUID, version, author, description)
│   ├── MyModule.psm1         dev loader (dot-sources Public/ + Private/)
│   ├── Public/               exported cmdlets — one .ps1 per function
│   └── Private/              internal helpers — not exported
├── tests/
│   ├── Unit/Public/          import full module, test exported behaviour
│   ├── Unit/Private/         dot-source the file directly, test internals
│   └── Integration/          end-to-end tests
├── docs/
│   ├── about_MyModule.md     module overview (hand-maintained)
│   └── Get-Example.md        per-cmdlet help (auto-generated by platyPS)
├── .build/                   build output (git-ignored)
│   └── MyModule/             monolithic module ready to publish
├── .devcontainer/            VS Code dev container (PowerShell 7.4)
├── .github/workflows/
│   ├── ci.yml                lint + test + security + build on every PR
│   └── publish.yml           publish on v*.*.* tag push
├── .vscode/
├── .claude/commands/
├── .build.ps1
└── PSScriptAnalyzerSettings.psd1

Common Commands

Invoke-Build              # default: Lint -> Test -> Build -> Docs
Invoke-Build Lint         # PSScriptAnalyzer
Invoke-Build Test         # Pester 5 + coverage -> .build/coverage.xml
Invoke-Build Manifest     # auto-update FunctionsToExport from Public/
Invoke-Build Build        # monolithic .psm1 -> .build/MyModule/
Invoke-Build Docs         # platyPS markdown -> docs/
Invoke-Build Clean        # remove .build/

How the Build Works

The source uses a dev loader (MyModule.psm1) that dot-sources all files in Public/ and Private/ at runtime — convenient for development.

Invoke-Build Build produces a monolithic .psm1 in .build/MyModule/:

  1. Concatenates all Private/*.ps1 files
  2. Concatenates all Public/*.ps1 files
  3. Appends Export-ModuleMember with an explicit function list
  4. Copies the manifest and writes the exact FunctionsToExport list

The built module is what gets published — never the src/ directory.

FunctionsToExport in src/MyModule/MyModule.psd1 is intentionally '*' (exports everything during development). The Manifest task and the Build task both auto-discover Public/ and write the precise list into the manifest, so you never maintain it by hand.

Docs

docs/
├── about_MyModule.md    ← hand-maintained: module overview, keywords, examples
└── Get-Example.md       ← auto-generated by platyPS from comment-based help

Edit comment-based help in the source .ps1 files, then re-run Invoke-Build Docs to update the markdown. Do not hand-edit the auto-generated cmdlet files — changes will be overwritten.

Publishing

All three publish targets are wired in .build.ps1 and triggered by publish.yml when you push a version tag.

git tag v1.0.0
git push origin v1.0.0

PowerShell Gallery (Public)

Feed URL: (default, no registration needed)

$env:PSGALLERY_API_KEY = 'your-key'   # from powershellgallery.com/account/apikeys
Invoke-Build Build, Publish

Required GitHub secret: PSGALLERY_API_KEY


GitHub Packages (Public or Private)

Feed URL: https://nuget.pkg.github.com/OWNER/index.json

$env:GITHUB_TOKEN = 'ghp_...'
$env:GITHUB_OWNER = 'samhodgkinson'
Invoke-Build Build, PublishGitHub

Install from GitHub Packages:

$cred = [pscredential]::new(
    'YOUR_USERNAME',
    (ConvertTo-SecureString 'ghp_...' -AsPlainText -Force)
)
Register-PSResourceRepository -Name GitHubPackages `
    -Uri 'https://nuget.pkg.github.com/OWNER/index.json' `
    -Trusted -Credential $cred
Install-PSResource -Name MyModule -Repository GitHubPackages -Credential $cred

Required GitHub secret: GITHUB_TOKEN — auto-provided in Actions, no manual secret needed.


Azure DevOps Artifacts (Private)

Organisation-scoped feed URL:

https://pkgs.dev.azure.com/ORG/_packaging/FEED/nuget/v3/index.json

Project-scoped feed URL:

https://pkgs.dev.azure.com/ORG/PROJECT/_packaging/FEED/nuget/v3/index.json
$env:ADO_PAT  = 'your-pat'
$env:ADO_ORG  = 'your-org'
$env:ADO_FEED = 'your-feed'
Invoke-Build Build, PublishADO

Install from ADO Artifacts:

$cred = [pscredential]::new(
    'PAT',
    (ConvertTo-SecureString 'your-pat' -AsPlainText -Force)
)
Register-PSResourceRepository -Name ADOFeed `
    -Uri 'https://pkgs.dev.azure.com/ORG/_packaging/FEED/nuget/v3/index.json' `
    -Trusted -Credential $cred
Install-PSResource -Name MyModule -Repository ADOFeed -Credential $cred

Required GitHub secrets: ADO_PAT Required GitHub variables: ADO_ORG, ADO_FEED

The publish-ado job is skipped when ADO_ORG is not configured — ADO is opt-in.

Architecture Notes

  • One file per function in Public/ and Private/. Keeps diffs focused and code review easy.
  • FunctionsToExport = '*' in src — works for development. Build writes the explicit list.
  • Pester 5 New-PesterConfiguration only — no legacy hashtable syntax. UseBreakpoints = $false uses the profiler-based coverage runner (faster).
  • PSScriptAnalyzer runs all rulesPSScriptAnalyzerSettings.psd1 configures style. CI fails on any finding.
  • No CodeQL — CodeQL does not support PowerShell. PSScriptAnalyzer security rules run as a dedicated security CI job.
  • docs/ split: about_MyModule.md is hand-maintained; cmdlet .md files are auto-generated by platyPS and should not be edited directly.

Testing Conventions

  • tests/Unit/Public/FunctionName.Tests.ps1 — imports the full module
  • tests/Unit/Private/HelperName.Tests.ps1 — dot-sources the file directly
  • BeforeAll / AfterAll for module import (not BeforeEach)
  • Context blocks: "When given valid input", "When given invalid input"
  • 100% branch coverage target on all public functions

When Making Changes

  1. Add function to Public/ or Private/
  2. Write tests in tests/Unit/
  3. Invoke-Build Lint — fix all findings
  4. Invoke-Build Test — all tests must pass
  5. Invoke-Build Build, Docs — verify build and regenerate docs
  6. Bump ModuleVersion in MyModule.psd1
  7. Update CHANGELOG.md
  8. Tag: git tag v1.x.x && git push origin v1.x.x

About

No description, website, or topics provided.

Resources

License

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors