diff --git a/.github/workflows/Action-Test-Src-Default-Custom.yml b/.github/workflows/Action-Test-Src-Default-Custom.yml new file mode 100644 index 0000000..e89fa3b --- /dev/null +++ b/.github/workflows/Action-Test-Src-Default-Custom.yml @@ -0,0 +1,24 @@ +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-outputs.yml b/.github/workflows/Action-Test-outputs.yml index c318e4c..38094ad 100644 --- a/.github/workflows/Action-Test-outputs.yml +++ b/.github/workflows/Action-Test-outputs.yml @@ -21,4 +21,3 @@ jobs: TestType: outputs Path: tests/outputTestRepo/outputs/modules/PSModuleTest Settings: Module - SettingsFilePath: diff --git a/README.md b/README.md index 2bcc8a5..8ee34ce 100644 --- a/README.md +++ b/README.md @@ -1,72 +1,66 @@ -# Invoke-ScriptAnalyzer (by PSModule) +# Invoke-ScriptAnalyzer -This repository contains a GitHub Action that runs PSScriptAnalyzer on your code. +This repository contains a GitHub Action that runs [`PSScriptAnalyzer`](https://github.com/PowerShell/PSScriptAnalyzer) on your code. The action analyzes PowerShell scripts using a hashtable-based settings file to customize rule selection, severity filtering, and custom rule inclusion. -> **Note:** This repository includes automated tests that run via Pester to ensure -> your settings file is working as expected. +## Dependencies -## Action Details - -- **Name:** Invoke-ScriptAnalyzer (by PSModule) -- **Description:** Runs PSScriptAnalyzer on the code. -- **Author:** PSModule -- **Branding:** - Icon: `check-square` - Color: `gray-dark` +- This action. +- [`PSScriptAnalyzer` module](https://github.com/PowerShell/PSScriptAnalyzer). +- [`Invoke-Pester` action](https://github.com/PSModule/Invoke-Pester) +- [`Pester` module](https://github.com/Pester/Pester) +- [`GitHub-Script` action](https://github.com/PSModule/GitHub-Script) +- [`GitHub` module](https://github.com/PSModule/GitHub) ## Inputs -| Input | Description | Required | Default | -|---------------------|-------------------------------------------------------------------|----------|-----------------------------------------------------------------------------| -| **Path** | The path to the code to test. | Yes | `${{ 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` | +| Input | Description | Required | Default | +|---------------------|----------------------------------------------------------------|----------|-----------------------------------------------------------------------------| +| **Path** | The path to the code to test. | Yes | `${{ 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 }}` | +| Output | Description | Value | +|----------|--------------------------------|------------------------------------| +| `passed` | Indicates if the tests passed. | `${{ steps.test.outputs.Passed }}` | -## Files Overview +## How It Works -- **action.yml** - Describes the action inputs, outputs, and run steps. The action uses a - composite run steps approach with two main steps: - 1. **Get test paths:** Uses a script to resolve paths and settings. - 2. **Invoke-Pester:** Runs Pester tests against PSScriptAnalyzer. +1. **Set a Path** + Choose a path for your code to test into the `Path` input. This can be a + directory or a file. -- **scripts/main.ps1** - Determines the correct settings file path based on the test type. It - supports testing a module, source code, or using a custom settings file. +2. **Choose settings** + Choose the type of tests to run by setting the `Settings` input. The options + are `Module`, `SourceCode`, or `Custom`. The default is `Custom`. -- **scripts/tests/PSScriptAnalyzer/** - Contains Pester tests that run PSScriptAnalyzer using the provided settings - file. The tests check for issues reported by PSScriptAnalyzer based on rule - configuration. + The predefined settings: + - [`Module`](./scripts/tests/PSScriptAnalyzer/Module.Settings.psd1): Analyzes a module following PSModule standards. + - [`SourceCode`](./scripts/tests/PSScriptAnalyzer/SourceCode.Settings.psd1): Analyzes the source code following PSModule standards. -## How It Works + You can also create a custom settings file to customize the analysis. The + settings file is a hashtable that defines the rules to include, exclude, or + customize. The settings file is in the format of a `.psd1` file. -1. **Path Resolution:** - The action reads inputs and determines the code path, test path, and the - settings file path. For custom settings, it uses the file at: - ```powershell - .github/linters/.powershell-psscriptanalyzer.psd1 - ``` - Otherwise, it uses a default settings file from the test folder. + For more info on how to create a settings file, see the [Settings Documentation](./Settings.md) file. -2. **Pester Testing:** - The tests import the settings file and use `Invoke-ScriptAnalyzer` to scan +3. **Run the Action** + The tests import the settings file and use `Invoke-ScriptAnalyzer` to analyze the code. Each rule is evaluated, and if a rule violation is found, the test will fail for that rule. Rules that are marked to be skipped (via exclusions in the settings file) are automatically skipped in the test. -3. **Automation:** - Designed for CI/CD, this action integrates with GitHub Actions, Azure Pipelines, - and other systems. The settings file customizes analysis, letting you control - rule inclusion, severity filtering, and custom rule paths. + 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 also outputs the results of the tests to the console. ## Example Workflow @@ -81,23 +75,21 @@ jobs: lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - name: Checkout code + uses: actions/checkout@v2 + - name: Invoke PSScriptAnalyzer uses: PSModule/Invoke-ScriptAnalyzer@v1 with: Path: ${{ github.workspace }} - Settings: Custom - SettingsFilePath: ${{ github.workspace }}/.github/linters/.powershell-psscriptanalyzer.psd1 + Settings: SourceCode ``` -## Appendix: Settings File Documentation - -For detailed documentation on the format of the settings file, see the -[Settings File Documentation](./SettingsFileDocumentation.md) file. - ## References and Links - [PSScriptAnalyzer Documentation](https://learn.microsoft.com/powershell/module/psscriptanalyzer/) -- [GitHub Super-Linter](https://github.com/github/super-linter) +- [PSScriptAnalyzer Module Overview](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/overview?view=ps-modules) +- [PSScriptAnalyzer Rules](https://learn.microsoft.com/en-us/powershell/utility-modules/psscriptanalyzer/rules/readme?view=ps-modules) - [PSScriptAnalyzer GitHub Repository](https://github.com/PowerShell/PSScriptAnalyzer) - [Custom Rules in PSScriptAnalyzer](https://docs.microsoft.com/powershell/scripting/developer/hosting/psscriptanalyzer-extensibility) +- [GitHub Super-Linter](https://github.com/github/super-linter) diff --git a/Settings.md b/Settings.md new file mode 100644 index 0000000..37eac38 --- /dev/null +++ b/Settings.md @@ -0,0 +1,83 @@ +# PSScriptAnalyzer Settings File Format Documentation + +This document describes the format and usage of the hashtable-based settings file +for PSScriptAnalyzer. The file is used by the GitHub action to customize analysis. + +## Basic Setup + +The file is a PowerShell data file (.psd1) that returns a hashtable. For example: +```powershell +@{ + Severity = @('Error','Warning') + ExcludeRules = @('PSAvoidUsingWriteHost') +} +``` +This example sets the severity filter and excludes a specific rule. + +## Key Configuration Options + +- **IncludeRules** + A list of rules to run. Wildcards (e.g. `PSAvoid*`) are supported. + +- **ExcludeRules** + A list of rules to skip. Excludes take precedence over include lists. + +- **Severity** + Filters output by severity. Allowed values include `Error`, `Warning`, and `Information`. + +- **IncludeDefaultRules** + A Boolean switch to include default rules when using custom rules. + +- **CustomRulePath** + One or more paths to custom rule modules or scripts. These extend PSScriptAnalyzer. + +- **RecurseCustomRulePath** + Boolean to search subdirectories of the custom rule path(s) for more rule files. + +- **Rules** + A nested hashtable for rule-specific settings. Use it to pass parameters to rules. + +```powershell +Rules = @{ + PSAvoidUsingCmdletAliases = @{ + Enabled = $true + Whitelist = @('ls','gc') + } +} +``` + +## Configuring Custom Rules + +Custom rules are implemented in modules (.psm1) or scripts. They must export +functions that return DiagnosticRecord objects. Specify their location using +`CustomRulePath`. Use `IncludeDefaultRules = $true` if you want to run both +default and custom rules. + +For example: +```powershell +@{ + CustomRulePath = @('.\Modules\MyCustomRules.psm1') + IncludeDefaultRules = $true + IncludeRules = @('PSUseApprovedVerbs', 'Measure-*') +} +``` + +## Rule Execution and Skip Logic + +The action evaluates each rule using several configurable settings to determine whether it should be executed or skipped. +The evaluation is performed in the following order: + +1. **Exclude Rules** + - If the rule's name is present in the **`ExcludeRules`** list, the rule is skipped immediately, regardless of other settings. + +2. **Include Rules** + - If an **`IncludeRules`** list is provided, the rule must be part of this list. If the rule's name is *not* in the list, it is skipped. + +3. **Severity Filtering** + - If a **`Severity`** list is specified, the rule's severity must be included in that list. If the rule's severity is not part of the allowed + values, the rule is skipped. + +4. **Rule-Specific Configuration** + - If a specific configuration exists for the rule under the **Rules** key, and its `Enable` property is set to false, the rule is skipped. + +To see what rules are skipped and why, check the logs for the action. There is a log group inside the test that contains the rules that were skipped. diff --git a/SettingsFileDocumentation.md b/SettingsFileDocumentation.md deleted file mode 100644 index ad5b739..0000000 --- a/SettingsFileDocumentation.md +++ /dev/null @@ -1,116 +0,0 @@ -# PSScriptAnalyzer Settings File Format Documentation - -This document describes the format and usage of the hashtable-based settings file -for PSScriptAnalyzer. The file is used by the GitHub action to customize analysis. - -## File Location and Basic Setup - -Place the file at: -```powershell -.github/linters/.powershell-psscriptanalyzer.psd1 -``` -The file is a PowerShell data file (.psd1) that returns a hashtable. For example: -```powershell -@{ - Severity = @('Error','Warning') - ExcludeRules = @('PSAvoidUsingWriteHost') -} -``` -This example sets the severity filter and excludes a specific rule. - -## Key Configuration Options - -- **IncludeRules** - A list of rules to run. Wildcards (e.g. `PSAvoid*`) are supported. - -- **ExcludeRules** - A list of rules to skip. Excludes take precedence over include lists. - -- **Severity** - Filters output by severity. Allowed values include `Error`, `Warning`, and - `Information`. - -- **IncludeDefaultRules** - A Boolean switch to include default rules when using custom rules. - -- **CustomRulePath** - One or more paths to custom rule modules or scripts. These extend PSScriptAnalyzer. - -- **RecurseCustomRulePath** - Boolean to search subdirectories of the custom rule path(s) for more rule files. - -- **Rules** - A nested hashtable for rule-specific settings. Use it to pass parameters to rules. - For example: - ```powershell - Rules = @{ - PSAvoidUsingCmdletAliases = @{ Whitelist = @('ls','gc') } - } - ``` - -## Configuring Custom Rules - -Custom rules are implemented in modules (.psm1) or scripts. They must export -functions that return DiagnosticRecord objects. Specify their location using -**CustomRulePath**. Use **IncludeDefaultRules = $true** if you want to run both -default and custom rules. - -For example: -```powershell -@{ - CustomRulePath = @('.\Modules\MyCustomRules.psm1') - IncludeDefaultRules = $true - IncludeRules = @('PSUseApprovedVerbs', 'Measure-*') -} -``` - -## Advanced Use Cases - -- **Selective Rule Execution** - Use either **IncludeRules** or **ExcludeRules** to control which rules run. - They help reduce noise in the analysis output. - -- **Rule-Specific Parameters** - Configure individual rules via the **Rules** key. Pass any required - parameters to fine-tune rule behavior. - -- **Multiple Settings Files** - In a multi-project repo, use separate settings files for each project and - run PSScriptAnalyzer with the appropriate file. - -- **Dynamic Settings** - Although not recommended, you can include minimal logic in the .psd1 file. - For example, using environment variables to adjust settings dynamically. - -## Automation and CI/CD Integration - -This settings file is designed to be used with automated pipelines. - -- **GitHub Actions** - The Super-Linter action automatically picks up the file from the above path. - Alternatively, use a dedicated PSScriptAnalyzer action with the settings input. - -- **Azure Pipelines** - Run a PowerShell task that installs PSScriptAnalyzer and points to the settings file. - Exit codes can be used to fail the build on errors. - -- **Other CI Tools** - Any CI system can invoke `Invoke-ScriptAnalyzer` with the `-Settings` parameter - to use this configuration. - -## Best Practices - -- **Version Control**: Store the settings file in your repository to keep configuration - consistent across environments. -- **Minimal Exclusions**: Exclude only rules that are not applicable to your project. -- **Documentation**: Use comments in the settings file to explain why certain rules are - included or excluded. -- **Regular Updates**: Update your settings when you upgrade PSScriptAnalyzer or change your - project requirements. - -## Links and References - -- [PSScriptAnalyzer Documentation](https://learn.microsoft.com/powershell/module/psscriptanalyzer/) -- [GitHub Super-Linter](https://github.com/github/super-linter) -- [PSScriptAnalyzer GitHub Repository](https://github.com/PowerShell/PSScriptAnalyzer) -- [Custom Rules in PSScriptAnalyzer](https://docs.microsoft.com/powershell/scripting/developer/hosting/psscriptanalyzer-extensibility) diff --git a/action.yml b/action.yml index 2ef0cde..29b5579 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ runs: Script: ${{ github.action_path }}/scripts/main.ps1 - name: Invoke-Pester - uses: PSModule/Invoke-Pester@fix + uses: PSModule/Invoke-Pester@v2 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 6ff16bb..d048114 100644 --- a/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 +++ b/scripts/tests/PSScriptAnalyzer/PSScriptAnalyzer.Tests.ps1 @@ -6,6 +6,10 @@ 'PSUseDeclaredVarsMoreThanAssignments', '', Justification = 'Pester blocks line of sight during analysis.' )] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSAvoidUsingWriteHost', '', + Justification = 'Write-Host is used for log output.' +)] [CmdLetBinding()] Param( [Parameter(Mandatory)] @@ -16,33 +20,49 @@ Param( ) BeforeDiscovery { - $settings = Import-PowerShellDataFile -Path $SettingsFilePath - $rules = [Collections.Generic.List[System.Collections.Specialized.OrderedDictionary]]::new() - $ruleObjects = Get-ScriptAnalyzerRule -Verbose:$false | Sort-Object -Property Severity, CommonName - $Severeties = $ruleObjects | Select-Object -ExpandProperty Severity -Unique - foreach ($ruleObject in $ruleObjects) { - $rules.Add( - [ordered]@{ - RuleName = $ruleObject.RuleName - CommonName = $ruleObject.CommonName - Severity = $ruleObject.Severity - Description = $ruleObject.Description - Skip = $ruleObject.RuleName -in $settings.ExcludeRules - <# - RuleName : PSDSCUseVerboseMessageInDSCResource - CommonName : Use verbose message in DSC resource - Description : It is a best practice to emit informative, verbose messages in DSC resource functions. - This helps in debugging issues when a DSC configuration is executed. - SourceType : Builtin - SourceName : PSDSC - Severity : Information - ImplementingType : Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseVerboseMessageInDSCResource - #> + LogGroup "PSScriptAnalyzer tests using settings file [$SettingsFilePath]" { + $settings = Import-PowerShellDataFile -Path $SettingsFilePath + $rules = [Collections.Generic.List[System.Collections.Specialized.OrderedDictionary]]::new() + $ruleObjects = Get-ScriptAnalyzerRule -Verbose:$false | Sort-Object -Property Severity, CommonName + $Severeties = $ruleObjects | Select-Object -ExpandProperty Severity -Unique + + $PSStyle.OutputRendering = 'Ansi' + $darkGrey = $PSStyle.Foreground.FromRgb(85, 85, 85) + $green = $PSStyle.Foreground.Green + $reset = $PSStyle.Reset + + foreach ($ruleObject in $ruleObjects) { + if ($settings.ContainsKey('ExcludeRules') -and $ruleObject.RuleName -in $settings.ExcludeRules) { + Write-Host "$darkGrey - $($ruleObject.RuleName) - Skipping rule - Exclude list$reset" + $skip = $true + } elseif ($settings.ContainsKey('IncludeRules') -and $ruleObject.RuleName -notin $settings.IncludeRules) { + Write-Host "$darkGrey - $($ruleObject.RuleName) - Skipping rule - Include list$reset" + $skip = $true + } elseif ($settings.ContainsKey('Severity') -and $ruleObject.Severity -notin $settings.Severity) { + Write-Host "$darkGrey - $($ruleObject.RuleName) - Skipping rule - Severity list$reset" + $skip = $true + } elseif ($settings.ContainsKey('Rules') -and $settings.Rules.ContainsKey($ruleObject.RuleName) -and + -not $settings.Rules[$ruleObject.RuleName].Enable) { + Write-Host "$darkGrey - $($ruleObject.RuleName) - Skipping rule - Disabled$reset" + $skip = $true + } else { + Write-Host "$green + $($ruleObject.RuleName) - Including rule$reset" + $skip = $false } - ) + + $rules.Add( + [ordered]@{ + RuleName = $ruleObject.RuleName + CommonName = $ruleObject.CommonName + Severity = $ruleObject.Severity + Description = $ruleObject.Description + Skip = $skip + } + ) + } + Write-Warning "Discovered [$($rules.Count)] rules" + $relativeSettingsFilePath = $SettingsFilePath.Replace($PSScriptRoot, '').Trim('\').Trim('/') } - Write-Warning "Discovered [$($rules.Count)] rules" - $relativeSettingsFilePath = $SettingsFilePath.Replace($PSScriptRoot, '').Trim('\').Trim('/') } Describe "PSScriptAnalyzer tests using settings file [$relativeSettingsFilePath]" { diff --git a/tests/srcTestRepo/tests/Custom.Settings.psd1 b/tests/srcTestRepo/tests/Custom.Settings.psd1 new file mode 100644 index 0000000..5edae56 --- /dev/null +++ b/tests/srcTestRepo/tests/Custom.Settings.psd1 @@ -0,0 +1,66 @@ +@{ + Rules = @{ + PSAlignAssignmentStatement = @{ + Enable = $true + CheckHashtable = $true + } + PSAvoidLongLines = @{ + Enable = $false + MaximumLineLength = 150 + } + PSAvoidSemicolonsAsLineTerminators = @{ + Enable = $true + } + PSPlaceCloseBrace = @{ + Enable = $true + NewLineAfter = $false + IgnoreOneLineBlock = $true + NoEmptyLineBefore = $false + } + PSPlaceOpenBrace = @{ + Enable = $true + OnSameLine = $true + NewLineAfter = $true + IgnoreOneLineBlock = $true + } + PSProvideCommentHelp = @{ + Enable = $true + ExportedOnly = $false + BlockComment = $true + VSCodeSnippetCorrection = $false + Placement = 'begin' + } + PSUseConsistentIndentation = @{ + Enable = $false + IndentationSize = 4 + PipelineIndentation = 'IncreaseIndentationForFirstPipeline' + Kind = 'space' + } + PSUseConsistentWhitespace = @{ + Enable = $true + CheckInnerBrace = $true + CheckOpenBrace = $true + CheckOpenParen = $true + CheckOperator = $true + CheckPipe = $true + CheckPipeForRedundantWhitespace = $true + CheckSeparator = $true + CheckParameter = $true + IgnoreAssignmentOperatorInsideHashTable = $true + } + } + ExcludeRules = @( + 'PSUseConsistentWhitespace' + 'PSAvoidUsingWriteHost' + ) + IncludeRules = @( + 'PSAvoidSemicolonsAsLineTerminators' + 'PSPlaceCloseBrace' + 'PSProvideCommentHelp' + 'PSUseConsistentIndentation' + ) + Severity = @( + 'Error' + 'Warning' + ) +} diff --git a/tools/records.md b/tools/records.md new file mode 100644 index 0000000..de72969 --- /dev/null +++ b/tools/records.md @@ -0,0 +1,37 @@ +# Schemas for records used in PSScriptAnalyzer + +## Schema: Get-ScriptAnalyzerRule + +```plaintext + RuleName : PSDSCUseVerboseMessageInDSCResource + CommonName : Use verbose message in DSC resource + Description : It is a best practice to emit informative, verbose messages in DSC resource functions. + This helps in debugging issues when a DSC configuration is executed. + SourceType : Builtin + SourceName : PSDSC + Severity : Information + ImplementingType : Microsoft.Windows.PowerShell.ScriptAnalyzer.BuiltinRules.UseVerboseMessageInDSCResource +``` + +## Schema: Invoke-ScriptAnalyzer ERROR record + +```plaintext +Line : 1 +Column : 1 +Message : The member 'ModuleVersion' is not present in the module manifest. This member must exist and be assigned a + version number of the form 'n.n.n.n'. Add the missing member to the file + ' C:\Repos\GitHub\PSModule\Action\Test-PSModule\tests\srcWithManifestTestRepo\src\manifest.psd1'. +Extent : @{ + Author = 'Author' + } +RuleName : PSMissingModuleManifestField +Severity : Warning +ScriptName : manifest.psd1 +ScriptPath : C:\Repos\GitHub\PSModule\Action\Test-PSModule\tests\srcWithManifestTestRepo\src\manifest.psd1 +RuleSuppressionID : +SuggestedCorrections : { + # Version number of this module. + ModuleVersion = '1.0.0.0' + } +IsSuppressed : False +```