PowerShell in CI/CD
PowerShell in a pipeline needs three things the interactive console gives you for free: a non-zero exit code on failure, no interactive prompts, and no dependency on a profile.
Documentation: Microsoft Learn
Overview
Exit codes are the one that catches people. A PowerShell error does not set the exit code by itself, so a script that logs a failure and ends normally reports success to the build. Set $ErrorActionPreference = 'Stop' at the top and call exit 1 in your catch block, or the pipeline will go green while nothing worked.
Run with -NoProfile and -NonInteractive. A profile that works on your machine is not present on the runner, and any cmdlet that prompts will hang the job until it times out.
Pin your module versions with -RequiredVersion. A pipeline that installs the latest version of a module is a pipeline that can break without anyone changing anything, and the failure will look unrelated to the real cause.
Secrets come from the CI system's secret store as environment variables. Never in the script, never in the repository. Also be careful what you echo, because build logs are usually readable by more people than the repository is.
Invoke-Pester -CI sets the exit code and emits test results in a format the CI system can display.
Inspect this example’s structure
Read the command and its supplied arguments. This example requires real PowerShell or its module; this diagram does not execute it.
$stage = @(
[pscustomobject]@{ Step = 1;Name = 'Analyse';Command = 'Invoke-ScriptAnalyzer -Path . -Severity Error' }
[pscustomobject]@{ Step = 2;Name = 'Test';Command = 'Invoke-Pester -CI' }
[pscustomobject]@{ Step = 3;Name = 'Publish';Command = 'Publish-Module -Path . -NuGetApiKey $env:KEY' })
$stage | Format-Table Step, Name, Command- Stage 1$stage
Positional values or no named parameters - Stage 2Format-Table
Positional values or no named parameters
An example to inspect
Preview the example. Sign in for the full lesson and activities.
$stage = @(
[pscustomobject]@{ Step = 1;Name = 'Analyse';Command = 'Invoke-ScriptAnalyzer -Path . -Severity Error' }
[pscustomobject]@{ Step = 2;Name = 'Test';Command = 'Invoke-Pester -CI' }
[pscustomobject]@{ Step = 3;Name = 'Publish';Command = 'Publish-Module -Path . -NuGetApiKey $env:KEY' })
$stage | Format-Table Step, Name, CommandWhat happens
This lays out the three stages in order. Analyse fails the build on error-severity findings before tests even run, which is the cheapest gate. Test uses -CI so the exit code and result file are handled. Publish reads its key from the environment rather than holding it, which is the only acceptable place for it.
Worth remembering
- Set $ErrorActionPreference = 'Stop' and exit 1 on failure, or the build stays green.
- Use -NoProfile and -NonInteractive on the runner.
- Pin module versions with -RequiredVersion.
- Secrets come from the CI secret store, and build logs are widely readable.
Primary documentation
Try it
Illustrative output · values vary by environment
Example reader
Inspect this page's example
Run supported commands against the practice data, or inspect the example without executing it.
Ready to inspect.
Lays out the three CI stages with the flags that make each behave.
Show reference output
Illustrative output · values vary by environment
Step Name Command ---- ---- ------- 1 Analyse Invoke-ScriptAnalyzer -Path . -Severity Error 2 Test Invoke-Pester -CI 3 Publish Publish-Module -Path . -NuGetApiKey $env:KEY