Skip to main content

Real-World Example

This page shows a production-ready PowerShell module project that uses PowerShellBuild for its build pipeline. It covers the full lifecycle: local development, automated testing, and publishing to the PowerShell Gallery via GitHub Actions.

Project Structure

MyModule/
├── src/
│ ├── MyModule.psd1
│ ├── MyModule.psm1
│ ├── Public/
│ │ ├── Get-Widget.ps1
│ │ └── New-Widget.ps1
│ └── Private/
│ └── Invoke-WidgetHelper.ps1
├── tests/
│ ├── Get-Widget.Tests.ps1
│ └── New-Widget.Tests.ps1
├── docs/ # PlatyPS markdown (auto-generated by GenerateMarkdown)
├── .github/
│ └── workflows/
│ └── build.yml
├── psakeFile.ps1
├── build.ps1
├── PSScriptAnalyzerSettings.psd1 # Auto-detected by PowerShellBuild
└── requirements.psd1

requirements.psd1

Declare all PowerShell module dependencies. PSDepend installs these during bootstrap.

requirements.psd1
@{
psake = 'latest'
PowerShellBuild = 'latest'
Pester = 'latest'
PSScriptAnalyzer = 'latest'
platyPS = '0.14.2'
}

build.ps1

The bootstrap entry point that handles dependency installation and task dispatch.

build.ps1
#Requires -Version 5.1

[CmdletBinding()]
param(
[string[]]$Task = 'default',

[switch]$Bootstrap,

[hashtable]$Properties = @{}
)

Set-StrictMode -Version Latest

if ($Bootstrap) {
Write-Host 'Bootstrapping build dependencies...' -ForegroundColor Cyan

Get-PackageProvider -Name NuGet -ForceBootstrap | Out-Null
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted

if (-not (Get-Module -Name PSDepend -ListAvailable)) {
Install-Module -Name PSDepend -Scope CurrentUser -Force
}

Import-Module PSDepend
Invoke-PSDepend -Path "$PSScriptRoot/requirements.psd1" -Install -Import -Force
Write-Host 'Bootstrap complete.' -ForegroundColor Green
}

Import-Module psake -ErrorAction Stop

Invoke-psake `
-buildFile "$PSScriptRoot/psakeFile.ps1" `
-taskList $Task `
-properties $Properties `
-nologo

exit ([int](-not $psake.build_success))

psakeFile.ps1

A realistic build file with customized preferences, custom tasks alongside PowerShellBuild tasks, and a Deploy task that wraps the release workflow.

psakeFile.ps1
#Requires -Version 5.1

# --- Task dependency overrides (must be set before referencing PowerShellBuild tasks) ---
# Publish only requires a successful build, not the full test suite in this example.
# Tests are enforced in CI; the Publish task is only invoked from GitHub Actions.
$PSBPublishDependency = 'Build'

# ---
properties {
# General
$PSBPreference.General.SrcRootDir = "$PSScriptRoot/src"
$PSBPreference.General.ModuleName = 'MyModule'

# Build
$PSBPreference.Build.OutDir = "$PSScriptRoot/build"
$PSBPreference.Build.CompileModule = $true
$PSBPreference.Build.CompileDirectories = @('Enum', 'Classes', 'Private', 'Public')

# Test — Pester
$PSBPreference.Test.Enabled = $true
$PSBPreference.Test.RootDir = "$PSScriptRoot/tests"
$PSBPreference.Test.OutputFile = "$PSScriptRoot/build/TestResults.xml"
$PSBPreference.Test.OutputFormat = 'NUnitXml'
$PSBPreference.Test.ImportModule = $true

# Test — PSScriptAnalyzer
# SettingsPath defaults to ./PSScriptAnalyzerSettings.psd1 in the project root
$PSBPreference.Test.ScriptAnalysis.Enabled = $true
$PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel = 'Error'

# Test — Code Coverage
$PSBPreference.Test.CodeCoverage.Enabled = $true
$PSBPreference.Test.CodeCoverage.Threshold = 0.80
$PSBPreference.Test.CodeCoverage.OutputFile = "$PSScriptRoot/build/coverage.xml"
$PSBPreference.Test.CodeCoverage.OutputFileFormat = 'JaCoCo'

# Help
$PSBPreference.Help.DefaultLocale = 'en-US'
$PSBPreference.Docs.RootDir = "$PSScriptRoot/docs"

# Publish — API key supplied by CI; falls back to env var for local releases
$PSBPreference.Publish.PSRepository = 'PSGallery'
$PSBPreference.Publish.PSRepositoryApiKey = $env:PSGALLERY_API_KEY
}

# ---
# Entry points
# ---

task default -depends Test

# Import PowerShellBuild tasks
task Init -FromModule PowerShellBuild -Version '0.7.1'
task Clean -FromModule PowerShellBuild -Version '0.7.1'
task StageFiles -FromModule PowerShellBuild -Version '0.7.1'
task Build -FromModule PowerShellBuild -Version '0.7.1'
task Analyze -FromModule PowerShellBuild -Version '0.7.1'
task Pester -FromModule PowerShellBuild -Version '0.7.1'
task Test -FromModule PowerShellBuild -Version '0.7.1'
task Publish -FromModule PowerShellBuild -Version '0.7.1'

# ---
# Custom tasks
# ---

task BumpVersion -depends Init {
param([string]$BumpType = 'Patch')

$manifestPath = $PSBPreference.General.ModuleManifestPath
$manifest = Import-PowerShellDataFile $manifestPath
$current = [System.Version]$manifest.ModuleVersion

$next = switch ($BumpType) {
'Major' { [System.Version]::new($current.Major + 1, 0, 0) }
'Minor' { [System.Version]::new($current.Major, $current.Minor + 1, 0) }
'Patch' { [System.Version]::new($current.Major, $current.Minor, $current.Build + 1) }
}

Update-ModuleManifest -Path $manifestPath -ModuleVersion $next.ToString()
Write-Host "Version bumped $current -> $next" -ForegroundColor Green
}

task ValidateReadme -precondition { Test-Path "$PSScriptRoot/README.md" } {
$content = Get-Content "$PSScriptRoot/README.md" -Raw

$requiredSections = @('## Installation', '## Usage', '## Contributing')
foreach ($section in $requiredSections) {
if ($content -notmatch [regex]::Escape($section)) {
throw "README.md is missing section: $section"
}
}

Write-Host 'README.md validation passed.' -ForegroundColor Green
}

# Deploy = bump version + validate docs + publish
task Deploy -depends BumpVersion, ValidateReadme, Publish {
$manifest = Import-PowerShellDataFile $PSBPreference.General.ModuleManifestPath
Write-Host "MyModule v$($manifest.ModuleVersion) deployed to PSGallery." -ForegroundColor Green
}

PSScriptAnalyzerSettings.psd1

PowerShellBuild looks for PSScriptAnalyzerSettings.psd1 in the project root by default. Place your custom rules here and they will be picked up automatically — no need to set SettingsPath.

PSScriptAnalyzerSettings.psd1
@{
ExcludeRules = @(
'PSAvoidUsingWriteHost' # Write-Host is acceptable in build scripts
)
Severity = @('Error', 'Warning')
}

GitHub Actions Workflow

This workflow runs the full test suite on every push and pull request, and publishes to PSGallery when a tag is pushed.

.github/workflows/build.yml
name: Build

on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]

jobs:
test:
name: Test (PS ${{ matrix.ps-version }} on ${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
ps-version: ['7.4']

steps:
- uses: actions/checkout@v4

- name: Bootstrap dependencies
shell: pwsh
run: .\build.ps1 -Bootstrap

- name: Run tests
shell: pwsh
run: .\build.ps1 -Task Test

- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}
path: build/TestResults.xml

- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.os }}
path: build/coverage.xml

publish:
name: Publish to PSGallery
needs: test
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')

steps:
- uses: actions/checkout@v4

- name: Bootstrap dependencies
shell: pwsh
run: .\build.ps1 -Bootstrap

- name: Publish module
shell: pwsh
env:
PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }}
run: .\build.ps1 -Task Publish

Local Development Workflow

# First-time setup
.\build.ps1 -Bootstrap

# Day-to-day: run tests
.\build.ps1

# Check code quality only
.\build.ps1 -Task Analyze

# Rebuild from scratch
.\build.ps1 -Task Clean, Build

# Bump the patch version and publish a release
.\build.ps1 -Task Deploy

See Also