Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions .github/workflows/Action-Test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,103 @@ jobs:
CONCLUSION: ${{ steps.action-test.conclusion }}
run: tests/Show-Status.ps1

ActionTestPesterGuidPin:
name: Action-Test - [Pester GUID Pin]
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 it's a local action
- name: Checkout repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Action-Test [Pester GUID Pin]
uses: ./
id: action-test
with:
WorkingDirectory: tests/4-PesterVersionConstraints/PesterGuidPin
Version: '6.0.0'
Guid: 'a699dea5-2c73-4616-a270-1f7abb777e71'
TestResult_TestSuiteName: PesterGuidPin

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

ActionTestPesterGuidMatch:
name: Action-Test - [Pester GUID Match]
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 it's a local action
- name: Checkout repo
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

# A matching GUID passed via the action input must let the run proceed and the tests pass.
- name: Action-Test [Pester GUID Match]
uses: ./
id: action-test
with:
WorkingDirectory: tests/4-PesterVersionConstraints/PesterGuidMatch
Version: '6.0.0'
Guid: 'a699dea5-2c73-4616-a270-1f7abb777e71'
TestResult_TestSuiteName: PesterGuidMatch

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

ActionTestPesterGuidMismatch:
name: Action-Test - [Pester GUID Mismatch]
runs-on: ubuntu-latest

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

# A wrong GUID must make the action fail fast during init, before any tests run.
- name: Action-Test [Pester GUID Mismatch]
uses: ./
id: action-test
continue-on-error: true
with:
WorkingDirectory: tests/2-Standard
Version: '6.0.0'
Guid: '00000000-0000-0000-0000-000000000000'

- name: Assert the action failed on the GUID mismatch
shell: pwsh
env:
OUTCOME: ${{ steps.action-test.outcome }}
run: |
if ($env:OUTCOME -ne 'failure') {
Write-Error "Expected the action to fail on a GUID mismatch, but the outcome was '$env:OUTCOME'."
exit 1
}
Write-Output 'Action failed as expected on the GUID mismatch.'

CatchJob:
name: Catch Job - Aggregate Status
needs:
Expand All @@ -378,6 +475,9 @@ jobs:
- ActionTest3Advanced
- ActionTestPester5RangeConstraint
- ActionTestPester6ExactConstraint
- ActionTestPesterGuidPin
- ActionTestPesterGuidMatch
- ActionTestPesterGuidMismatch
if: always()
runs-on: ubuntu-latest
steps:
Expand Down Expand Up @@ -433,5 +533,13 @@ jobs:
ACTIONTESTPESTER6EXACTCONSTRAINT_CONCLUSION: ${{ needs.ActionTestPester6ExactConstraint.outputs.Conclusion }}
ACTIONTESTPESTER6EXACTCONSTRAINT_EXECUTED: ${{ needs.ActionTestPester6ExactConstraint.outputs.Executed }}
ACTIONTESTPESTER6EXACTCONSTRAINT_RESULT: ${{ needs.ActionTestPester6ExactConstraint.outputs.Result }}
ACTIONTESTPESTERGUIDPIN_OUTCOME: ${{ needs.ActionTestPesterGuidPin.outputs.Outcome }}
ACTIONTESTPESTERGUIDPIN_CONCLUSION: ${{ needs.ActionTestPesterGuidPin.outputs.Conclusion }}
ACTIONTESTPESTERGUIDPIN_EXECUTED: ${{ needs.ActionTestPesterGuidPin.outputs.Executed }}
ACTIONTESTPESTERGUIDPIN_RESULT: ${{ needs.ActionTestPesterGuidPin.outputs.Result }}
ACTIONTESTPESTERGUIDMATCH_OUTCOME: ${{ needs.ActionTestPesterGuidMatch.outputs.Outcome }}
ACTIONTESTPESTERGUIDMATCH_CONCLUSION: ${{ needs.ActionTestPesterGuidMatch.outputs.Conclusion }}
ACTIONTESTPESTERGUIDMATCH_EXECUTED: ${{ needs.ActionTestPesterGuidMatch.outputs.Executed }}
ACTIONTESTPESTERGUIDMATCH_RESULT: ${{ needs.ActionTestPesterGuidMatch.outputs.Result }}
with:
Script: tests/Test-ActionResults.ps1
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ jobs:
| `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` |
| `Guid` | Optional module identity (GUID) the installed Pester must match; fails fast if a different module named Pester is loaded. Mirrors the GUID key of a `#Requires -Modules` pin. | *(none)* |
| `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
8 changes: 8 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ inputs:
Allow installing prerelease versions of Pester.
required: false
default: 'false'
Guid:
description: |
Optional module identity (GUID) that the installed Pester must match, mirroring the GUID key of a
'#Requires -Modules @{ ... }' declaration. When set, the run fails fast if the loaded Pester's GUID does
not match, guarding against a different module named Pester on the runner's module path.
required: false
Comment thread
MariusStorhaug marked this conversation as resolved.
ReportAsJson:
description: |
Output generated reports in JSON format in addition to the configured format through Pester.
Expand Down Expand Up @@ -302,6 +308,7 @@ runs:
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_Guid: ${{ inputs.Guid }}
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 @@ -365,6 +372,7 @@ runs:
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_Guid: ${{ inputs.Guid }}
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
27 changes: 24 additions & 3 deletions src/Invoke-Pester.Helpers.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -1311,6 +1311,11 @@ function Install-PSResourceWithRetry {
[Parameter()]
[switch] $Prerelease,

# Optional module identity (GUID) the loaded module must match. When set, the imported module's GUID is
# validated and a mismatch throws, guarding against a different module with the same name.
[Parameter()]
[string] $Guid,

# Number of times to retry installation, default is 5
[Parameter()]
[int] $RetryCount = 5,
Expand Down Expand Up @@ -1365,23 +1370,39 @@ function Install-PSResourceWithRetry {
if (-not [string]::IsNullOrWhiteSpace($Version)) {
$getParams['Version'] = $Version
}
$resolved = Get-PSResource @getParams | Sort-Object Version -Descending | Select-Object -First 1
$resolved = Get-InstalledPSResource @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
$imported = Import-Module -Name $Name -RequiredVersion $resolved.Version -Force -Global -PassThru -ErrorAction Stop
Comment thread
MariusStorhaug marked this conversation as resolved.
} 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
$imported = Import-Module -Name $Name -Force -Global -PassThru -ErrorAction Stop
}

# Validate module identity when a GUID pin is supplied, so a different module with the same name cannot
# satisfy the request. This fails at install time (shifted left) in addition to any '#Requires' GUID pin.
if (-not [string]::IsNullOrWhiteSpace($Guid)) {
$expectedGuid = [guid]::Empty
if (-not [guid]::TryParse($Guid, [ref]$expectedGuid)) {
throw "The provided Guid '$Guid' is not a valid GUID."
}
Comment thread
MariusStorhaug marked this conversation as resolved.
# Validate the module this call just imported (via -PassThru), not the highest version that happens
# to be loaded, so a side-by-side loaded version cannot mask a mismatch.
$importedModule = $imported | Where-Object { $_.Name -eq $Name } | Select-Object -First 1
if (-not $importedModule -or $importedModule.Guid -ne $expectedGuid) {
throw "Loaded '$Name' does not match the required GUID '$expectedGuid' (loaded GUID: $($importedModule.Guid))."
}
Write-Output "Verified module identity for ${Name}: GUID $expectedGuid"
}
}
}
5 changes: 3 additions & 2 deletions src/exec.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ 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
$pesterGuid = $env:PSMODULE_INVOKE_PESTER_INPUT_Guid
Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease -Guid $pesterGuid
'::endgroup::'

'::group::Exec - Get test kit versions'
Expand Down Expand Up @@ -63,7 +64,7 @@ $PSStyle.OutputRendering = 'Ansi'

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

Expand Down
3 changes: 2 additions & 1 deletion src/init.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ LogGroup 'Init - Setup prerequisites' {
# 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
$pesterGuid = $env:PSMODULE_INVOKE_PESTER_INPUT_Guid
Install-PSResourceWithRetry -Name 'Pester' -Version $pesterVersion -Prerelease:$pesterPrerelease -Guid $pesterGuid
'Hashtable', 'TimeSpan', 'Markdown' | Install-PSResourceWithRetry
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '6.0.0' }

Describe 'Pester GUID input (match)' {
It 'runs when the Guid input matches the installed Pester identity' {
# The GUID is pinned through the action's Guid input (validated at install), not via #Requires here,
# so reaching and passing this test proves the matching Guid input let the run proceed.
# Select the pinned version explicitly so a side-by-side loaded version cannot make the assertion flaky.
$module = Get-Module -Name Pester | Where-Object { $_.Version -eq [version]'6.0.0' } | Select-Object -First 1

$module | Should -Not -BeNullOrEmpty
$module.Guid | Should -Be ([guid]'a699dea5-2c73-4616-a270-1f7abb777e71')
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '6.0.0'; GUID = 'a699dea5-2c73-4616-a270-1f7abb777e71' }

Describe 'Pester GUID pin' {
It 'loads the Pester module identified by its GUID' {
# The #Requires above pins Pester by module identity (GUID) in addition to version. PowerShell refuses to
# run this file unless the loaded Pester matches both, so reaching this assertion already proves the pin held.
# Select the pinned version explicitly so a side-by-side loaded version cannot make the assertion flaky.
$module = Get-Module -Name Pester | Where-Object { $_.Version -eq [version]'6.0.0' } | Select-Object -First 1

$module | Should -Not -BeNullOrEmpty
$module.Guid | Should -Be ([guid]'a699dea5-2c73-4616-a270-1f7abb777e71')
}
}
19 changes: 17 additions & 2 deletions tests/Test-ActionResults.ps1
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<#
.DESCRIPTION
Aggregates and validates test results from all Action-Test workflow jobs.
Compares actual outcomes against expected values and generates a summary report.
Aggregates and validates results from the result-producing Action-Test workflow jobs.
Compares actual outcomes against expected values and generates a summary report. The negative
'Action-Test - [Pester GUID Mismatch]' job self-validates in its own step and is not tabled here.
#>

[CmdletBinding()]
Expand Down Expand Up @@ -108,6 +109,20 @@ $jobs = @(
Executed = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_EXECUTED; Expected = 'True' }
Result = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_RESULT; Expected = 'Passed' }
}
@{
Name = 'Action-Test - [Pester GUID Pin]'
Outcome = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_OUTCOME; Expected = 'success' }
Conclusion = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_CONCLUSION; Expected = 'success' }
Executed = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_EXECUTED; Expected = 'True' }
Result = @{ Actual = $env:ACTIONTESTPESTERGUIDPIN_RESULT; Expected = 'Passed' }
}
@{
Name = 'Action-Test - [Pester GUID Match]'
Outcome = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_OUTCOME; Expected = 'success' }
Conclusion = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_CONCLUSION; Expected = 'success' }
Executed = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_EXECUTED; Expected = 'True' }
Result = @{ Actual = $env:ACTIONTESTPESTERGUIDMATCH_RESULT; Expected = 'Passed' }
}
)

# Add Pass property to each check and convert to PSCustomObject for table output
Expand Down
Loading