diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml index fb8609eb..72da746b 100644 --- a/.github/workflows/Action-Test.yml +++ b/.github/workflows/Action-Test.yml @@ -27,7 +27,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -58,7 +58,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -89,7 +89,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -121,7 +121,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -153,7 +153,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -183,7 +183,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -218,7 +218,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -250,7 +250,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -281,7 +281,7 @@ jobs: Result: ${{ steps.action-test.outputs.Result }} steps: - # Need to check out as part of the test, as its a local action + # 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: @@ -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 it's 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 it's 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: @@ -314,6 +376,8 @@ jobs: - ActionTest2StandardPrescriptFile - ActionTest2StandardNoSummary - ActionTest3Advanced + - ActionTestPester5RangeConstraint + - ActionTestPester6ExactConstraint if: always() runs-on: ubuntu-latest steps: @@ -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 }} @@ -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 diff --git a/README.md b/README.md index aa681cb9..5df868bb 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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.
| `Failed` | @@ -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 diff --git a/action.yml b/action.yml index 95c1da12..f0e7f499 100644 --- a/action.yml +++ b/action.yml @@ -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. @@ -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: @@ -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 }} @@ -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: | @@ -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 }} diff --git a/src/Helpers.psm1 b/src/Invoke-Pester.Helpers.psm1 similarity index 93% rename from src/Helpers.psm1 rename to src/Invoke-Pester.Helpers.psm1 index 5cda07f2..e6646a66 100644 --- a/src/Helpers.psm1 +++ b/src/Invoke-Pester.Helpers.psm1 @@ -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 ( @@ -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, @@ -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: $_" @@ -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 + } } } diff --git a/src/exec.ps1 b/src/exec.ps1 index ab0de546..66b19e1c 100644 --- a/src/exec.ps1 +++ b/src/exec.ps1 @@ -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() @@ -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() diff --git a/src/init.ps1 b/src/init.ps1 index 990f9ac8..d1a7f25c 100644 --- a/src/init.ps1 +++ b/src/init.ps1 @@ -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() diff --git a/tests/4-PesterVersionConstraints/Pester5Range/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/Pester5Range/PesterVersion.Tests.ps1 new file mode 100644 index 00000000..be06522d --- /dev/null +++ b/tests/4-PesterVersionConstraints/Pester5Range/PesterVersion.Tests.ps1 @@ -0,0 +1,12 @@ +#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0'; MaximumVersion = '5.*' } + +Describe 'Pester version constraint' { + It 'uses a Pester 5.x version resolved from the configured range' { + $module = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1 + + $module | Should -Not -Be $null + $module.Version.Major | Should -Be 5 + $module.Version | Should -BeGreaterOrEqual ([version]'5.0.0') + $module.Version | Should -BeLessThan ([version]'6.0.0') + } +} \ No newline at end of file diff --git a/tests/4-PesterVersionConstraints/Pester6Exact/PesterVersion.Tests.ps1 b/tests/4-PesterVersionConstraints/Pester6Exact/PesterVersion.Tests.ps1 new file mode 100644 index 00000000..359143a9 --- /dev/null +++ b/tests/4-PesterVersionConstraints/Pester6Exact/PesterVersion.Tests.ps1 @@ -0,0 +1,10 @@ +#Requires -Modules @{ ModuleName = 'Pester'; RequiredVersion = '6.0.0' } + +Describe 'Pester version constraint' { + It 'uses the exact configured Pester version' { + $module = Get-Module -Name Pester | Sort-Object Version -Descending | Select-Object -First 1 + + $module | Should -Not -Be $null + $module.Version | Should -Be ([version]'6.0.0') + } +} \ No newline at end of file diff --git a/tests/Test-ActionResults.ps1 b/tests/Test-ActionResults.ps1 index 30617525..5167d3ce 100644 --- a/tests/Test-ActionResults.ps1 +++ b/tests/Test-ActionResults.ps1 @@ -94,6 +94,20 @@ $jobs = @( Executed = @{ Actual = $env:ACTIONTEST3ADVANCED_EXECUTED; Expected = 'True' } Result = @{ Actual = $env:ACTIONTEST3ADVANCED_RESULT; Expected = 'Passed' } } + @{ + Name = 'Action-Test - [Pester 5.x Range Constraint]' + Outcome = @{ Actual = $env:ACTIONTESTPESTER5RANGECONSTRAINT_OUTCOME; Expected = 'success' } + Conclusion = @{ Actual = $env:ACTIONTESTPESTER5RANGECONSTRAINT_CONCLUSION; Expected = 'success' } + Executed = @{ Actual = $env:ACTIONTESTPESTER5RANGECONSTRAINT_EXECUTED; Expected = 'True' } + Result = @{ Actual = $env:ACTIONTESTPESTER5RANGECONSTRAINT_RESULT; Expected = 'Passed' } + } + @{ + Name = 'Action-Test - [Pester 6.0.0 Exact Constraint]' + Outcome = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_OUTCOME; Expected = 'success' } + Conclusion = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_CONCLUSION; Expected = 'success' } + Executed = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_EXECUTED; Expected = 'True' } + Result = @{ Actual = $env:ACTIONTESTPESTER6EXACTCONSTRAINT_RESULT; Expected = 'Passed' } + } ) # Add Pass property to each check and convert to PSCustomObject for table output