Skip to content
74 changes: 73 additions & 1 deletion .github/workflows/Action-Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,68 @@ jobs:
CONCLUSION: ${{ steps.action-test.conclusion }}
run: tests/Show-Status.ps1

ActionTestPester5RangeConstraint:
name: Action-Test - [Pester 5.x Range Constraint]
runs-on: ubuntu-latest
outputs:
Outcome: ${{ steps.action-test.outcome }}
Conclusion: ${{ steps.action-test.conclusion }}
Executed: ${{ steps.action-test.outputs.Executed }}
Result: ${{ steps.action-test.outputs.Result }}

steps:
# Need to check out as part of the test, as its a local action
- name: Checkout repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Action-Test [Pester 5.x Range Constraint]
uses: ./
id: action-test
with:
WorkingDirectory: tests/4-PesterVersionConstraints/Pester5Range
Version: '[5.0.0,6.0.0)'
TestResult_TestSuiteName: Pester5RangeConstraint

- name: Status
shell: pwsh
env:
OUTCOME: ${{ steps.action-test.outcome }}
CONCLUSION: ${{ steps.action-test.conclusion }}
run: tests/Show-Status.ps1

ActionTestPester6ExactConstraint:
name: Action-Test - [Pester 6.0.0 Exact Constraint]
runs-on: ubuntu-latest
outputs:
Outcome: ${{ steps.action-test.outcome }}
Conclusion: ${{ steps.action-test.conclusion }}
Executed: ${{ steps.action-test.outputs.Executed }}
Result: ${{ steps.action-test.outputs.Result }}

steps:
# Need to check out as part of the test, as its a local action
- name: Checkout repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Action-Test [Pester 6.0.0 Exact Constraint]
uses: ./
id: action-test
with:
WorkingDirectory: tests/4-PesterVersionConstraints/Pester6Exact
Version: '6.0.0'
TestResult_TestSuiteName: Pester6ExactConstraint

- name: Status
shell: pwsh
env:
OUTCOME: ${{ steps.action-test.outcome }}
CONCLUSION: ${{ steps.action-test.conclusion }}
run: tests/Show-Status.ps1

CatchJob:
name: Catch Job - Aggregate Status
needs:
Expand All @@ -314,6 +376,8 @@ jobs:
- ActionTest2StandardPrescriptFile
- ActionTest2StandardNoSummary
- ActionTest3Advanced
- ActionTestPester5RangeConstraint
- ActionTestPester6ExactConstraint
if: always()
runs-on: ubuntu-latest
steps:
Expand All @@ -323,7 +387,7 @@ jobs:
persist-credentials: false

- name: Display Aggregated Results as a Table
uses: PSModule/GitHub-Script@1ee97bbc652d19c38ae12f6e1e47e9d9fbd12d0a # v1.8.0
uses: PSModule/GitHub-Script@8083ec1f733f00357ee4d0db0c6056686e483bc0 # v1.9.0
env:
ACTIONTEST1SIMPLE_OUTCOME: ${{ needs.ActionTest1Simple.outputs.Outcome }}
ACTIONTEST1SIMPLE_CONCLUSION: ${{ needs.ActionTest1Simple.outputs.Conclusion }}
Expand Down Expand Up @@ -361,5 +425,13 @@ jobs:
ACTIONTEST3ADVANCED_CONCLUSION: ${{ needs.ActionTest3Advanced.outputs.Conclusion }}
ACTIONTEST3ADVANCED_EXECUTED: ${{ needs.ActionTest3Advanced.outputs.Executed }}
ACTIONTEST3ADVANCED_RESULT: ${{ needs.ActionTest3Advanced.outputs.Result }}
ACTIONTESTPESTER5RANGECONSTRAINT_OUTCOME: ${{ needs.ActionTestPester5RangeConstraint.outputs.Outcome }}
ACTIONTESTPESTER5RANGECONSTRAINT_CONCLUSION: ${{ needs.ActionTestPester5RangeConstraint.outputs.Conclusion }}
ACTIONTESTPESTER5RANGECONSTRAINT_EXECUTED: ${{ needs.ActionTestPester5RangeConstraint.outputs.Executed }}
ACTIONTESTPESTER5RANGECONSTRAINT_RESULT: ${{ needs.ActionTestPester5RangeConstraint.outputs.Result }}
ACTIONTESTPESTER6EXACTCONSTRAINT_OUTCOME: ${{ needs.ActionTestPester6ExactConstraint.outputs.Outcome }}
ACTIONTESTPESTER6EXACTCONSTRAINT_CONCLUSION: ${{ needs.ActionTestPester6ExactConstraint.outputs.Conclusion }}
ACTIONTESTPESTER6EXACTCONSTRAINT_EXECUTED: ${{ needs.ActionTestPester6ExactConstraint.outputs.Executed }}
ACTIONTESTPESTER6EXACTCONSTRAINT_RESULT: ${{ needs.ActionTestPester6ExactConstraint.outputs.Result }}
with:
Script: tests/Test-ActionResults.ps1
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ If you specify `CodeCoverage_Enabled: true` here, it will enable coverage even i

1. **Prerequisite Setup**
- Installs required PowerShell modules if they're not present.
- Installs Pester at the latest version by default, or the version matching the `Version` constraint when set.
- Imports the exact installed Pester version so the version that runs is deterministic, even when the runner ships a different preinstalled Pester.
- Imports the modules so the testing framework is ready to use.
2. **Loading Inputs and Configuration**
- Loads a default Pester configuration.
Expand Down Expand Up @@ -262,6 +264,8 @@ jobs:
| **Input** | **Description** | **Default** |
|--------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------|
| `Path` | Path to where tests are located or a configuration file. | *(none)* |
| `Version` | Version of Pester to install (NuGet range, e.g. `[6.0.0,7.0.0)` for any 6.x). Empty installs the latest version. | *(none)* |
| `Prerelease` | Allow installing prerelease versions of Pester. | `false` |
| `ReportAsJson` | Output generated reports in JSON format in addition to the configured format through Pester. | `true` |
| `Prescript` | Script to be executed before the test run. This script is executed in the same context as the test run. | *(none)* |
| `Notice_Mode` | Controls when to show notices for test completion.<br><ul><li>`Full` - show on success and failure</li><li>`Failed` - show only on failure</li><li>`None` - disable notices</li></ul> | `Failed` |
Expand Down Expand Up @@ -312,8 +316,8 @@ jobs:
| `TestRegistry_Enabled` | Enable `TestRegistry`. | *(none)* |
| `Debug` | Enable debug output. | `false` |
| `Verbose` | Enable verbose output. | `false` |
| `Version` | Specifies the exact version of the GitHub module to install. | *(none)* |
| `Prerelease` | Allow prerelease versions if available. | `false` |
| `GitHubVersion` | Version of the GitHub module to install during init bootstrap (NuGet range, e.g. `[1.8.0,2.0.0)`). Empty installs latest. | *(none)* |
| `GitHubPrerelease` | Allow installing prerelease versions of the GitHub module. | `false` |
| `WorkingDirectory` | The working directory where the script runs. | `.` |

### Outputs
Expand Down
30 changes: 23 additions & 7 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,16 @@ inputs:
description: |
Path to where tests are located or a configuration file.
required: false
Version:
description: |
Version of the Pester module to install, using NuGet version-range syntax, e.g. '[6.0.0,7.0.0)' to allow any 6.x.
A bare version such as '6.4.0' installs that exact version. When empty, the latest available version is installed.
required: false
Prerelease:
description: |
Allow installing prerelease versions of Pester.
required: false
default: 'false'
ReportAsJson:
description: |
Output generated reports in JSON format in addition to the configured format through Pester.
Expand Down Expand Up @@ -219,11 +229,13 @@ inputs:
description: Enable verbose output.
required: false
default: 'false'
Version:
description: Specifies the version of the GitHub module to be installed. The value must be an exact version.
GitHubVersion:
description: |
Version of the GitHub module to install during the init bootstrap step, using NuGet version-range syntax.
A bare version such as '1.8.0' installs that exact version. When empty, the latest available version is installed.
required: false
Prerelease:
description: Allow prerelease versions if available.
GitHubPrerelease:
description: Allow installing prerelease versions of the GitHub module.
required: false
default: 'false'
WorkingDirectory:
Expand Down Expand Up @@ -285,9 +297,11 @@ runs:
using: composite
steps:
- name: Invoke-Pester (init)
uses: PSModule/GitHub-Script@1ee97bbc652d19c38ae12f6e1e47e9d9fbd12d0a # v1.8.0
uses: PSModule/GitHub-Script@8083ec1f733f00357ee4d0db0c6056686e483bc0 # v1.9.0
env:
PSMODULE_INVOKE_PESTER_INPUT_Path: ${{ inputs.Path }}
PSMODULE_INVOKE_PESTER_INPUT_Version: ${{ inputs.Version }}
PSMODULE_INVOKE_PESTER_INPUT_Prerelease: ${{ inputs.Prerelease }}
PSMODULE_INVOKE_PESTER_INPUT_Run_Path: ${{ inputs.Run_Path }}
PSMODULE_INVOKE_PESTER_INPUT_Run_ExcludePath: ${{ inputs.Run_ExcludePath }}
PSMODULE_INVOKE_PESTER_INPUT_Run_ScriptBlock: ${{ inputs.Run_ScriptBlock }}
Expand Down Expand Up @@ -334,9 +348,9 @@ runs:
ShowInfo: false
ShowOutput: true
Debug: ${{ inputs.Debug }}
Prerelease: ${{ inputs.Prerelease }}
Prerelease: ${{ inputs.GitHubPrerelease }}
Verbose: ${{ inputs.Verbose }}
Version: ${{ inputs.Version }}
Version: ${{ inputs.GitHubVersion }}
WorkingDirectory: ${{ inputs.WorkingDirectory }}
Name: Invoke-Pester
Script: |
Expand All @@ -349,6 +363,8 @@ runs:
working-directory: ${{ inputs.WorkingDirectory }}
env:
PSMODULE_INVOKE_PESTER_INPUT_ReportAsJson: ${{ inputs.ReportAsJson }}
PSMODULE_INVOKE_PESTER_INPUT_Version: ${{ inputs.Version }}
PSMODULE_INVOKE_PESTER_INPUT_Prerelease: ${{ inputs.Prerelease }}
PSMODULE_INVOKE_PESTER_INPUT_Notice_Mode: ${{ inputs.Notice_Mode }}
PSMODULE_INVOKE_PESTER_INPUT_StepSummary_Mode: ${{ inputs.StepSummary_Mode }}
PSMODULE_INVOKE_PESTER_INPUT_StepSummary_ShowTestOverview: ${{ inputs.StepSummary_ShowTestOverview }}
Expand Down
70 changes: 65 additions & 5 deletions src/Helpers.psm1 → src/Invoke-Pester.Helpers.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -1283,10 +1283,14 @@ function Invoke-ProcessTestDirectory {
function Install-PSResourceWithRetry {
<#
.SYNOPSIS
Installs a PowerShell module with retry mechanism
Installs a PowerShell module with a retry mechanism, then imports the installed version.

.DESCRIPTION
Attempts to install a PowerShell module multiple times in case of failure
Attempts to install a PowerShell module multiple times in case of failure. When a version
constraint is supplied, the newest version satisfying it is installed; that exact version is
then imported so the loaded module is deterministic even when other versions (for example the
runner's preinstalled copy) are present on the machine. When no version is supplied, the latest
available version is installed.
#>
[CmdletBinding()]
param (
Expand All @@ -1298,6 +1302,15 @@ function Install-PSResourceWithRetry {
)]
[string] $Name,

# Version of the module to install. Accepts an exact version or a NuGet version range,
# e.g. '[6.0.0,7.0.0)' to allow any 6.x. When empty, the latest available version is installed.
[Parameter()]
[string] $Version,

# Allow installing prerelease versions.
[Parameter()]
[switch] $Prerelease,

# Number of times to retry installation, default is 5
[Parameter()]
[int] $RetryCount = 5,
Expand All @@ -1308,10 +1321,30 @@ function Install-PSResourceWithRetry {
)

process {
Write-Output "Installing module: $Name"
$installParams = @{
Name = $Name
Repository = 'PSGallery'
TrustRepository = $true
PassThru = $true
WarningAction = 'SilentlyContinue'
}
if (-not [string]::IsNullOrWhiteSpace($Version)) {
$installParams['Version'] = $Version
}
if ($Prerelease) {
$installParams['Prerelease'] = $true
}

$label = $Name
if (-not [string]::IsNullOrWhiteSpace($Version)) {
$label = "$Name $Version"
}
Write-Output "Installing module: $label"

$installed = $null
for ($i = 0; $i -lt $RetryCount; $i++) {
try {
Install-PSResource -Name $Name -WarningAction SilentlyContinue -TrustRepository -Repository PSGallery
$installed = Install-PSResource @installParams
break
} catch {
Write-Warning "Installation of $Name failed with error: $_"
Expand All @@ -1322,6 +1355,33 @@ function Install-PSResourceWithRetry {
Start-Sleep -Seconds $RetryDelay
}
}
Import-Module -Name $Name

# Resolve the exact version to import. Prefer what was just installed; if the resource was
# already present, Install-PSResource returns nothing, so fall back to the newest installed
# version that satisfies the requested constraint.
$resolved = $installed | Where-Object { $_.Name -eq $Name } | Sort-Object Version -Descending | Select-Object -First 1
if (-not $resolved) {
$getParams = @{ Name = $Name; Verbose = $false; ErrorAction = 'SilentlyContinue' }
if (-not [string]::IsNullOrWhiteSpace($Version)) {
$getParams['Version'] = $Version
}
$resolved = Get-PSResource @getParams | Sort-Object Version -Descending | Select-Object -First 1
}

# Import into the global session state so the resolved version is the one every subsequent
# command (for example Invoke-Pester in exec.ps1) uses, instead of PowerShell auto-loading the
# highest version available on PSModulePath.
if ($resolved) {
Write-Output "Importing module: $Name $($resolved.Version)"
Import-Module -Name $Name -RequiredVersion $resolved.Version -Force -Global -ErrorAction Stop
} elseif (-not [string]::IsNullOrWhiteSpace($Version)) {
# A version constraint was requested but no satisfying installed version could be resolved. Importing the
# module unconstrained here would silently load an arbitrary copy from PSModulePath (for example the
# runner's preinstalled module), defeating the deterministic, constraint-driven selection this function
# exists to guarantee. Fail fast instead.
throw "No installed '$Name' version satisfies constraint '$Version'; refusing to import an unconstrained version."
} else {
Import-Module -Name $Name -Force -Global -ErrorAction Stop
}
}
}
15 changes: 10 additions & 5 deletions src/exec.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ $VerbosePreference = $env:PSMODULE_INVOKE_PESTER_INPUT_Verbose -eq 'true' ? 'Con
$PSStyle.OutputRendering = 'Ansi'

'::group::Exec - Setup prerequisites'
Import-Module "$PSScriptRoot/Helpers.psm1"
'Pester' | Install-PSResourceWithRetry
Import-Module "$PSScriptRoot/Invoke-Pester.Helpers.psm1"
# Install Pester honoring the optional version constraint from the action input. An empty value installs the latest version.
$pesterVersion = $env:PSMODULE_INVOKE_PESTER_INPUT_Version
$pesterPrerelease = $env:PSMODULE_INVOKE_PESTER_INPUT_Prerelease -eq 'true'
Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease
'::endgroup::'

'::group::Exec - Get test kit versions'
$pesterModule = Get-PSResource -Name Pester -Verbose:$false | Sort-Object Version -Descending | Select-Object -First 1
$pesterModule = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1

[PSCustomObject]@{
PowerShell = $PSVersionTable.PSVersion.ToString()
Expand Down Expand Up @@ -59,11 +62,13 @@ $testResults = Invoke-Pester -Configuration $configuration
$PSStyle.OutputRendering = 'Ansi'

'::group::Eval - Setup prerequisites'
'Pester', 'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry
# Reuse the Pester version constraint resolved during Exec setup. An empty value installs the latest version.
Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease
'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry
'::endgroup::'

'::group::Eval - Get test kit versions'
$pesterModule = Get-PSResource -Name Pester -Verbose:$false | Sort-Object Version -Descending | Select-Object -First 1
$pesterModule = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1

[PSCustomObject]@{
PowerShell = $PSVersionTable.PSVersion.ToString()
Expand Down
10 changes: 7 additions & 3 deletions src/init.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
param()

LogGroup 'Init - Setup prerequisites' {
Import-Module "$PSScriptRoot/Helpers.psm1"
'Pester', 'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry
Import-Module "$PSScriptRoot/Invoke-Pester.Helpers.psm1"
# Install Pester honoring the optional version constraint from the action input. An empty value installs the latest version.
$pesterVersion = $env:PSMODULE_INVOKE_PESTER_INPUT_Version
$pesterPrerelease = $env:PSMODULE_INVOKE_PESTER_INPUT_Prerelease -eq 'true'
Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease
'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry
}

LogGroup 'Init - Get test kit versions' {
$pesterModule = Get-PSResource -Name Pester -Verbose:$false | Sort-Object Version -Descending | Select-Object -First 1
$pesterModule = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1

[PSCustomObject]@{
PowerShell = $PSVersionTable.PSVersion.ToString()
Expand Down
Loading
Loading