diff --git a/.github/linters/.jscpd.json b/.github/linters/.jscpd.json index 23970e8..88330de 100644 --- a/.github/linters/.jscpd.json +++ b/.github/linters/.jscpd.json @@ -4,7 +4,8 @@ "consoleFull" ], "ignore": [ - "**/tests/**" + "**/tests/**", + "**/.github/workflows/Action-Test.yml" ], "absolute": true } diff --git a/.github/workflows/Action-Test-Src-Default-Custom.yml b/.github/workflows/Action-Test-Src-Default-Custom.yml deleted file mode 100644 index e89fa3b..0000000 --- a/.github/workflows/Action-Test-Src-Default-Custom.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Action-Test [Src-Default-Custom] - -run-name: "Action-Test [Src-Default-Custom] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - workflow_dispatch: - pull_request: - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - ActionTestCustom: - uses: ./.github/workflows/ActionTestWorkflow.yml - with: - TestType: Src-Default-Custom - Path: tests/srcTestRepo/src - Settings: Custom - SettingsFilePath: tests/srcTestRepo/tests/Custom.Settings.psd1 diff --git a/.github/workflows/Action-Test-Src-Default.yml b/.github/workflows/Action-Test-Src-Default.yml deleted file mode 100644 index 7896cc0..0000000 --- a/.github/workflows/Action-Test-Src-Default.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Action-Test [Src-Default] - -run-name: "Action-Test [Src-Default] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - workflow_dispatch: - pull_request: - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - ActionTest: - uses: ./.github/workflows/ActionTestWorkflow.yml - with: - TestType: Src-Default - Path: tests/srcTestRepo/src - Settings: SourceCode diff --git a/.github/workflows/Action-Test-Src-WithManifest.yml b/.github/workflows/Action-Test-Src-WithManifest.yml deleted file mode 100644 index f2fb1ff..0000000 --- a/.github/workflows/Action-Test-Src-WithManifest.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Action-Test [Src-WithManifest] - -run-name: "Action-Test [Src-WithManifest] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - workflow_dispatch: - pull_request: - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - ActionTest: - uses: ./.github/workflows/ActionTestWorkflow.yml - with: - TestType: Src-WithManifest - Path: tests/srcWithManifestTestRepo/src - Settings: SourceCode diff --git a/.github/workflows/Action-Test-outputs.yml b/.github/workflows/Action-Test-outputs.yml deleted file mode 100644 index 38094ad..0000000 --- a/.github/workflows/Action-Test-outputs.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Action-Test [outputs] - -run-name: "Action-Test [outputs] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" - -on: - workflow_dispatch: - pull_request: - schedule: - - cron: '0 0 * * *' - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: {} - -jobs: - ActionTest: - uses: ./.github/workflows/ActionTestWorkflow.yml - with: - TestType: outputs - Path: tests/outputTestRepo/outputs/modules/PSModuleTest - Settings: Module diff --git a/.github/workflows/Action-Test.yml b/.github/workflows/Action-Test.yml new file mode 100644 index 0000000..d39594a --- /dev/null +++ b/.github/workflows/Action-Test.yml @@ -0,0 +1,228 @@ +name: Action-Test + +run-name: "Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" + +on: + workflow_dispatch: + pull_request: + schedule: + - cron: '0 0 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + pull-requests: read + +jobs: + ActionTestSrcSourceCode: + name: Action-Test - [Src-SourceCode] + runs-on: ubuntu-latest + outputs: + Outcome: ${{ steps.action-test.outcome }} + Conclusion: ${{ steps.action-test.conclusion }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Action-Test + uses: ./ + id: action-test + with: + Path: tests/srcTestRepo/src + Settings: SourceCode + + - name: Status + shell: pwsh + run: | + Write-Host "Outcome: ${{ steps.action-test.outcome }}" + Write-Host "Conclusion: ${{ steps.action-test.conclusion }}" + + ActionTestSrcCustom: + name: Action-Test - [Src-Custom] + runs-on: ubuntu-latest + outputs: + Outcome: ${{ steps.action-test.outcome }} + Conclusion: ${{ steps.action-test.conclusion }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Action-Test + uses: ./ + id: action-test + with: + Path: tests/srcTestRepo/src + Settings: Custom + SettingsFilePath: tests/srcTestRepo/tests/Custom.Settings.psd1 + + - name: Status + shell: pwsh + run: | + Write-Host "Outcome: ${{ steps.action-test.outcome }}" + Write-Host "Conclusion: ${{ steps.action-test.conclusion }}" + + ActionTestSrcWithManifest: + name: Action-Test - [Src-WithManifest] + runs-on: ubuntu-latest + outputs: + Outcome: ${{ steps.action-test.outcome }} + Conclusion: ${{ steps.action-test.conclusion }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Action-Test + uses: ./ + continue-on-error: true + id: action-test + with: + Path: tests/srcWithManifestTestRepo/src + Settings: SourceCode + + - name: Status + shell: pwsh + run: | + Write-Host "Outcome: ${{ steps.action-test.outcome }}" + Write-Host "Conclusion: ${{ steps.action-test.conclusion }}" + + ActionTestOutputs: + name: Action-Test - [outputs] + runs-on: ubuntu-latest + outputs: + Outcome: ${{ steps.action-test.outcome }} + Conclusion: ${{ steps.action-test.conclusion }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Action-Test + uses: ./ + id: action-test + with: + Path: tests/outputTestRepo/outputs/modules/PSModuleTest + Settings: Module + + - name: Status + shell: pwsh + run: | + Write-Host "Outcome: ${{ steps.action-test.outcome }}" + Write-Host "Conclusion: ${{ steps.action-test.conclusion }}" + + CatchJob: + name: Aggregate Status + needs: + - ActionTestSrcSourceCode + - ActionTestSrcCustom + - ActionTestSrcWithManifest + - ActionTestOutputs + if: always() + runs-on: ubuntu-latest + env: + ActionTestSrcSourceCodeOutcome: ${{ needs.ActionTestSrcSourceCode.outputs.Outcome }} + ActionTestSrcSourceCodeConclusion: ${{ needs.ActionTestSrcSourceCode.outputs.Conclusion }} + ActionTestSrcCustomOutcome: ${{ needs.ActionTestSrcCustom.outputs.Outcome }} + ActionTestSrcCustomConclusion: ${{ needs.ActionTestSrcCustom.outputs.Conclusion }} + ActionTestSrcWithManifestOutcome: ${{ needs.ActionTestSrcWithManifest.outputs.Outcome }} + ActionTestSrcWithManifestConclusion: ${{ needs.ActionTestSrcWithManifest.outputs.Conclusion }} + ActionTestOutputsOutcome: ${{ needs.ActionTestOutputs.outputs.Outcome }} + ActionTestOutputsConclusion: ${{ needs.ActionTestOutputs.outputs.Conclusion }} + steps: + - name: Aggregated Status + uses: PSModule/Github-Script@v1 + with: + Script: | + Install-PSResource -Name Markdown -Repository PSGallery -TrustRepository + + # Build an array of objects for each job + $ActionTestSrcSourceCodeExpectedOutcome = 'success' + $ActionTestSrcSourceCodeOutcomeResult = $env:ActionTestSrcSourceCodeOutcome -eq $ActionTestSrcSourceCodeExpectedOutcome + $ActionTestSrcSourceCodeExpectedConclusion = 'success' + $ActionTestSrcSourceCodeConclusionResult = $env:ActionTestSrcSourceCodeConclusion -eq $ActionTestSrcSourceCodeExpectedConclusion + + $ActionTestSrcCustomExpectedOutcome = 'success' + $ActionTestSrcCustomOutcomeResult = $env:ActionTestSrcCustomOutcome -eq $ActionTestSrcCustomExpectedOutcome + $ActionTestSrcCustomExpectedConclusion = 'success' + $ActionTestSrcCustomConclusionResult = $env:ActionTestSrcCustomConclusion -eq $ActionTestSrcCustomExpectedConclusion + + $ActionTestSrcWithManifestExpectedOutcome = 'failure' + $ActionTestSrcWithManifestOutcomeResult = $env:ActionTestSrcWithManifestOutcome -eq $ActionTestSrcWithManifestExpectedOutcome + $ActionTestSrcWithManifestExpectedConclusion = 'success' + $ActionTestSrcWithManifestConclusionResult = $env:ActionTestSrcWithManifestConclusion -eq $ActionTestSrcWithManifestExpectedConclusion + + $ActionTestOutputsExpectedOutcome = 'success' + $ActionTestOutputsOutcomeResult = $env:ActionTestOutputsOutcome -eq $ActionTestOutputsExpectedOutcome + $ActionTestOutputsExpectedConclusion = 'success' + $ActionTestOutputsConclusionResult = $env:ActionTestOutputsConclusion -eq $ActionTestOutputsExpectedConclusion + + $jobs = @( + [PSCustomObject]@{ + Name = 'Action-Test - [Src-SourceCode]' + Outcome = $env:ActionTestSrcSourceCodeOutcome + ExpectedOutcome = $ActionTestSrcSourceCodeExpectedOutcome + PassedOutcome = $ActionTestSrcSourceCodeOutcomeResult + Conclusion = $env:ActionTestSrcSourceCodeConclusion + ExpectedConclusion = $ActionTestSrcSourceCodeExpectedConclusion + PassedConclusion = $ActionTestSrcSourceCodeConclusionResult + }, + [PSCustomObject]@{ + Name = 'Action-Test - [Src-Custom]' + Outcome = $env:ActionTestSrcCustomOutcome + ExpectedOutcome = $ActionTestSrcCustomExpectedOutcome + PassedOutcome = $ActionTestSrcCustomOutcomeResult + Conclusion = $env:ActionTestSrcCustomConclusion + ExpectedConclusion = $ActionTestSrcCustomExpectedConclusion + PassedConclusion = $ActionTestSrcCustomConclusionResult + }, + [PSCustomObject]@{ + Name = 'Action-Test - [Src-WithManifest]' + Outcome = $env:ActionTestSrcWithManifestOutcome + ExpectedOutcome = $ActionTestSrcWithManifestExpectedOutcome + PassedOutcome = $ActionTestSrcWithManifestOutcomeResult + Conclusion = $env:ActionTestSrcWithManifestConclusion + ExpectedConclusion = $ActionTestSrcWithManifestExpectedConclusion + PassedConclusion = $ActionTestSrcWithManifestConclusionResult + }, + [PSCustomObject]@{ + Name = 'Action-Test - [outputs]' + Outcome = $env:ActionTestOutputsOutcome + ExpectedOutcome = $ActionTestOutputsExpectedOutcome + PassedOutcome = $ActionTestOutputsOutcomeResult + Conclusion = $env:ActionTestOutputsConclusion + ExpectedConclusion = $ActionTestOutputsExpectedConclusion + PassedConclusion = $ActionTestOutputsConclusionResult + } + ) + + # Display the table in the workflow logs + $jobs | Format-List + + $passed = $true + $jobs | ForEach-Object { + if (-not $_.PassedOutcome) { + Write-Error "Job $($_.Name) failed with Outcome $($_.Outcome) and Expected Outcome $($_.ExpectedOutcome)" + $passed = $false + } + + if (-not $_.PassedConclusion) { + Write-Error "Job $($_.Name) failed with Conclusion $($_.Conclusion) and Expected Conclusion $($_.ExpectedConclusion)" + $passed = $false + } + } + + $icon = if ($passed) { '✅' } else { '❌' } + $status = Heading 1 "$icon - GitHub Actions Status" { + Table { + $jobs + } + } + + Set-GitHubStepSummary -Summary $status + + if (-not $passed) { + Write-GitHubError 'One or more jobs failed' + exit 1 + } + diff --git a/.github/workflows/ActionTestWorkflow.yml b/.github/workflows/ActionTestWorkflow.yml deleted file mode 100644 index b8f12df..0000000 --- a/.github/workflows/ActionTestWorkflow.yml +++ /dev/null @@ -1,50 +0,0 @@ -on: - workflow_call: - inputs: - TestType: - type: string - required: true - Path: - type: string - required: true - Settings: - type: string - required: false - SettingsFilePath: - type: string - required: false - -permissions: {} - -jobs: - ActionTest: - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - name: Action-Test [outputs] - [${{ matrix.os }}] - runs-on: ${{ matrix.os }} - steps: - - name: Checkout repo - uses: actions/checkout@v4 - - - name: Initialize environment - uses: PSModule/Initialize-PSModule@main - - - name: Action-Test - uses: ./ - id: action-test - with: - Path: ${{ inputs.Path }} - Settings: ${{ inputs.Settings }} - SettingsFilePath: ${{ inputs.SettingsFilePath }} - - - name: Status - shell: pwsh - env: - PASSED: ${{ steps.action-test.outputs.passed }} - run: | - Write-Host "Passed: [$env:PASSED]" - if ($env:PASSED -ne 'true') { - exit 1 - } diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml index 1f677cb..192ab27 100644 --- a/.github/workflows/Linter.yml +++ b/.github/workflows/Linter.yml @@ -30,3 +30,4 @@ jobs: VALIDATE_JSON_PRETTIER: false VALIDATE_MARKDOWN_PRETTIER: false VALIDATE_YAML_PRETTIER: false + FILTER_REGEX_EXCLUDE: '.*Set-PSModuleTest\.ps1$' diff --git a/README.md b/README.md index 8ee34ce..e4e9292 100644 --- a/README.md +++ b/README.md @@ -17,15 +17,13 @@ customize rule selection, severity filtering, and custom rule inclusion. | Input | Description | Required | Default | |---------------------|----------------------------------------------------------------|----------|-----------------------------------------------------------------------------| -| **Path** | The path to the code to test. | Yes | `${{ github.workspace }}` | +| **Path** | The path to the code to test. | No | `${{ github.workspace }}` | | **Settings** | The type of tests to run: `Module`, `SourceCode`, or `Custom`. | No | `Custom` | | **SettingsFilePath**| If `Custom` is selected, the path to the settings file. | No | `${{ github.workspace }}/.github/linters/.powershell-psscriptanalyzer.psd1` | ## Outputs -| Output | Description | Value | -|----------|--------------------------------|------------------------------------| -| `passed` | Indicates if the tests passed. | `${{ steps.test.outputs.Passed }}` | +N/A ## How It Works @@ -56,11 +54,12 @@ customize rule selection, severity filtering, and custom rule inclusion. To be clear; the action follows the settings file to determine which rules to skip. 4. **View the Results** - The action outputs the results of the tests. If the tests pass, the action - will return a `passed` output with a value of `true`. If the tests fail, the - action will return a `passed` output with a value of `false`. + The action outputs the results of the tests to goth logs and step summary. If the tests pass, the actions `outcome` will be `success`. + If the tests fail, the actions outcome will be `failure`. To make the workflow continue even if the tests fail, you can set the + `continue-on-error` option to `true`. Use this built-in feature to stop the workflow from failing so that you can aggregate the status of tests + across multiple jobs. - The action also outputs the results of the tests to the console. + An example of how this is done can be seen in the [Action-Test workflow](.github/workflows/Action-Test.yml) file. ## Example Workflow @@ -79,7 +78,7 @@ jobs: uses: actions/checkout@v2 - name: Invoke PSScriptAnalyzer - uses: PSModule/Invoke-ScriptAnalyzer@v1 + uses: PSModule/Invoke-ScriptAnalyzer@v2 with: Path: ${{ github.workspace }} Settings: SourceCode diff --git a/action.yml b/action.yml index 29b5579..2714a8b 100644 --- a/action.yml +++ b/action.yml @@ -8,7 +8,7 @@ branding: inputs: Path: description: The path to the code to test. - required: true + required: false default: ${{ github.workspace }} Settings: description: The type of tests to run. Can be either 'Module', 'SourceCode' or 'Custom'. @@ -19,11 +19,6 @@ inputs: required: false default: ${{ github.workspace }}/.github/linters/.powershell-psscriptanalyzer.psd1 -outputs: - passed: - description: If the tests passed. - value: ${{ steps.test.outputs.Passed }} - runs: using: composite steps: @@ -38,7 +33,7 @@ runs: Script: ${{ github.action_path }}/scripts/main.ps1 - name: Invoke-Pester - uses: PSModule/Invoke-Pester@v2 + uses: PSModule/Invoke-Pester@v3 id: test env: Settings: ${{ fromJson(steps.paths.outputs.result).Settings }} diff --git a/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 b/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 index d048114..7ec133d 100644 --- a/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 +++ b/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 @@ -60,24 +60,52 @@ BeforeDiscovery { } ) } - Write-Warning "Discovered [$($rules.Count)] rules" - $relativeSettingsFilePath = $SettingsFilePath.Replace($PSScriptRoot, '').Trim('\').Trim('/') } } -Describe "PSScriptAnalyzer tests using settings file [$relativeSettingsFilePath]" { +Describe 'PSScriptAnalyzer' { BeforeAll { - $testResults = Invoke-ScriptAnalyzer -Path $Path -Settings $SettingsFilePath -Recurse -Verbose:$false - Write-Warning "Found [$($testResults.Count)] issues" + $relativeSettingsFilePath = if ($SettingsFilePath.StartsWith($PSScriptRoot)) { + $SettingsFilePath.Replace($PSScriptRoot, 'Action:').Trim('\').Trim('/') + } elseif ($SettingsFilePath.StartsWith($env:GITHUB_WORKSPACE)) { + $SettingsFilePath.Replace($env:GITHUB_WORKSPACE, 'Workspace:').Trim('\').Trim('/') + } else { + $SettingsFilePath + } + $Path = Resolve-Path -Path $Path | Select-Object -ExpandProperty Path + $relativePath = if ($Path.StartsWith($PSScriptRoot)) { + $Path.Replace($PSScriptRoot, 'Action:').Trim('\').Trim('/') + } elseif ($Path.StartsWith($env:GITHUB_WORKSPACE)) { + $Path.Replace($env:GITHUB_WORKSPACE, 'Workspace:').Trim('\').Trim('/') + } else { + $Path + } + + [pscustomobject]@{ + relativeSettingsFilePath = $relativeSettingsFilePath + SettingsFilePath = $SettingsFilePath + PSScriptRoot = $PSScriptRoot + GITHUB_WORKSPACE = $env:GITHUB_WORKSPACE + } + + LogGroup "Invoke-ScriptAnalyzer -Path [$relativePath] -Settings [$relativeSettingsFilePath]" { + $testResults = Invoke-ScriptAnalyzer -Path $Path -Settings $SettingsFilePath -Recurse -Verbose + } + LogGroup "TestResults [$($testResults.Count)]" { + $testResults | ForEach-Object { + $_ | Format-List | Out-String -Stream | ForEach-Object { + Write-Verbose $_ -Verbose + } + } + } } foreach ($Severety in $Severeties) { Context "Severity: $Severety" { foreach ($rule in $rules | Where-Object -Property Severity -EQ $Severety) { - It "$($rule.CommonName) ($($rule.RuleName))" -Skip:$rule.Skip { + It "$($rule.CommonName) ($($rule.RuleName))" -Skip:$rule.Skip -ForEach @{ Rule = $rule } { $issues = [Collections.Generic.List[string]]::new() - $testResults | Where-Object -Property RuleName -EQ $rule.RuleName | ForEach-Object { - $relativePath = $_.ScriptPath.Replace($Path, '').Trim('\').Trim('/') + $testResults | Where-Object { $_.RuleName -eq $Rule.RuleName } | ForEach-Object { $issues.Add(([Environment]::NewLine + " - $relativePath`:L$($_.Line):C$($_.Column)")) } $issues -join '' | Should -BeNullOrEmpty -Because $rule.Description @@ -86,3 +114,7 @@ Describe "PSScriptAnalyzer tests using settings file [$relativeSettingsFilePath] } } } + +AfterAll { + $PSStyle.OutputRendering = 'Host' +} diff --git a/tests/Get-AggregatedStatus.ps1 b/tests/Get-AggregatedStatus.ps1 new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/tests/Get-AggregatedStatus.ps1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 b/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 index a87ac11..86a4ff7 100644 --- a/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 +++ b/tests/srcWithManifestTestRepo/src/functions/public/SomethingElse/Set-PSModuleTest.ps1 @@ -8,10 +8,6 @@ "Hello, World!" #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseShouldProcessForStateChangingFunctions', '', Scope = 'Function', - Justification = 'Reason for suppressing' - )] [CmdletBinding()] param ( # Name of the person to greet.