diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml
index 72da746b..fb63f0c5 100644
--- a/.github/workflows/Action-Test.yml
+++ b/.github/workflows/Action-Test.yml
@@ -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:
@@ -378,6 +475,9 @@ jobs:
- ActionTest3Advanced
- ActionTestPester5RangeConstraint
- ActionTestPester6ExactConstraint
+ - ActionTestPesterGuidPin
+ - ActionTestPesterGuidMatch
+ - ActionTestPesterGuidMismatch
if: always()
runs-on: ubuntu-latest
steps:
@@ -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
diff --git a/README.md b/README.md
index 5df868bb..1c2123da 100644
--- a/README.md
+++ b/README.md
@@ -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.
- `Full` - show on success and failure
- `Failed` - show only on failure
- `None` - disable notices
| `Failed` |
diff --git a/action.yml b/action.yml
index f0e7f499..99301eb6 100644
--- a/action.yml
+++ b/action.yml
@@ -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
ReportAsJson:
description: |
Output generated reports in JSON format in addition to the configured format through Pester.
@@ -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 }}
@@ -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 }}
diff --git a/src/Invoke-Pester.Helpers.psm1 b/src/Invoke-Pester.Helpers.psm1
index e6646a66..300f0c3e 100644
--- a/src/Invoke-Pester.Helpers.psm1
+++ b/src/Invoke-Pester.Helpers.psm1
@@ -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,
@@ -1365,7 +1370,7 @@ 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
@@ -1373,7 +1378,7 @@ function Install-PSResourceWithRetry {
# 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
} 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
@@ -1381,7 +1386,23 @@ function Install-PSResourceWithRetry {
# 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."
+ }
+ # 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"
}
}
}
diff --git a/src/exec.ps1 b/src/exec.ps1
index 66b19e1c..9810aa46 100644
--- a/src/exec.ps1
+++ b/src/exec.ps1
@@ -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'
@@ -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::'
diff --git a/src/init.ps1 b/src/init.ps1
index d1a7f25c..23186d25 100644
--- a/src/init.ps1
+++ b/src/init.ps1
@@ -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
}
diff --git a/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1
new file mode 100644
index 00000000..8c7f90c5
--- /dev/null
+++ b/tests/4-PesterVersionConstraints/PesterGuidMatch/PesterVersion.Tests.ps1
@@ -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')
+ }
+}
diff --git a/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1
new file mode 100644
index 00000000..db087943
--- /dev/null
+++ b/tests/4-PesterVersionConstraints/PesterGuidPin/PesterVersion.Tests.ps1
@@ -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')
+ }
+}
diff --git a/tests/Test-ActionResults.ps1 b/tests/Test-ActionResults.ps1
index 5167d3ce..0fd34adb 100644
--- a/tests/Test-ActionResults.ps1
+++ b/tests/Test-ActionResults.ps1
@@ -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()]
@@ -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