diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d4dd83b..c7022e9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,7 +1,7 @@ name: Test on: push: - branches: [ $default-branch ] + branches: [ main ] pull_request: workflow_dispatch: jobs: @@ -9,7 +9,7 @@ jobs: name: Test runs-on: ${{ matrix.os }} strategy: - fail-fast: false + fail-fast: true matrix: os: [ubuntu-latest, windows-latest, macOS-latest] steps: @@ -23,3 +23,19 @@ jobs: $DebugPreference = 'Continue' } ./build.ps1 -Task Test -Bootstrap + test_powershell: + name: Test + runs-on: windows-latest + strategy: + fail-fast: true + steps: + - uses: actions/checkout@v4 + - name: Test + shell: powershell + env: + DEBUG: ${{ runner.debug == '1' }} + run: | + if($env:DEBUG -eq 'true' -or $env:DEBUG -eq '1') { + $DebugPreference = 'Continue' + } + ./build.ps1 -Task Test -Bootstrap diff --git a/CHANGELOG.md b/CHANGELOG.md index 331ab36..f8ad86c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,48 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased +## [1.0.0-alpha1] 2026-03-22 + +### Breaking Changes + +- **Minimum PowerShell version raised from 3.0 to 5.1.** PowerShellBuild 1.0.0+ requires + PowerShell 5.1 or later. +- **psake 5.0.0 required.** The consumer-facing `psakeFile.ps1` now uses psake 5.0.0 + declarative task syntax with `Version 5` pinning. +- **`Invoke-psake` now returns `PsakeBuildResult`.** Build scripts that check + `$psake.build_success` should migrate to inspecting the returned object's `.Success` property. + +### Added + +- **Task caching** — Cacheable tasks (`StageFiles`, `Analyze`, `Pester`, `GenerateMarkdown`, + `GenerateMAML`, `GenerateUpdatableHelp`) now declare `Inputs`/`Outputs` for psake 5.0.0's + content-addressed caching. Unchanged tasks are automatically skipped on incremental builds. + Disable with `$PSBPreference.Build.EnableTaskCaching = $false`. +- **LLM-optimized test output** — New `$PSBPreference.Test.OutputMode` setting with values + `'Detailed'` (default, verbose human output), `'Minimal'` (failures only, compact), and + `'LLM'` (structured JSON with failure details, optimized for machine consumption). +- **External PesterConfiguration support** — New `$PSBPreference.Test.PesterConfigurationPath` + setting to load a `.psd1` file as the base PesterConfiguration. Explicit `$PSBPreference.Test` + values overlay on top. +- **`-Configuration` parameter on `Test-PSBuildPester`** — Pass a `[PesterConfiguration]` object + directly for full control over Pester execution. +- **`-OutputMode` parameter on `Test-PSBuildPester`** — Control output format per-invocation. +- **`-PesterConfigurationPath` parameter on `Test-PSBuildPester`** — Load external config files. +- **`Format-PSBuildResult`** — New public function to format psake 5.0.0's `PsakeBuildResult` + for Human, JSON, or GitHubActions output. +- **Declarative task syntax** — All tasks in `psakeFile.ps1` rewritten to use psake 5.0.0's + hashtable-based declarative syntax. +- **Invoke-Build parity** — `IB.tasks.ps1` updated with matching `Inputs`/`Outputs` caching + and new Pester parameter passthrough. + +### Changed + +- `$PSBPreference` now includes `Build.EnableTaskCaching`, `Test.OutputMode`, and + `Test.PesterConfigurationPath` keys. +- `Test-PSBuildPester` now always returns the Pester test result object for programmatic access. +- Synchronized inline `LocalizedData` in `PowerShellBuild.psm1` with all strings from + `en-US/Messages.psd1` (was missing signing-related strings from 0.8.0). + ## [0.8.0] 2026-02-20 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index a11fa07..ab89b29 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,10 +4,10 @@ **PowerShellBuild** is a PowerShell module that provides a standardized set of build, test, and publish tasks for PowerShell module projects. It supports two popular PowerShell task-runner frameworks: -- **psake** (4.9.0+) — task-based build system -- **Invoke-Build** (5.8.1+) — alternative task runner +- **psake** (5.0.0+) — task-based build system with declarative syntax and content-addressed caching +- **Invoke-Build** (5.8.1+) — alternative task runner with native Inputs/Outputs caching -The module version is **0.7.3** and targets PowerShell 3.0+. It is cross-platform and tested on Windows, Linux, and macOS. +The module version is **1.0.0-alpha1** and targets PowerShell 5.1+. It is cross-platform and tested on Windows (including Windows PowerShell 5.1), Linux, and macOS. --- @@ -26,8 +26,8 @@ PowerShellBuild/ ├── Build/ │ └── Convert-PSAke.ps1 # Utility: converts psake tasks to Invoke-Build ├── PowerShellBuild/ # THE MODULE SOURCE (System Under Test) -│ ├── Public/ # Exported (public) functions — 9 functions -│ ├── Private/ # Internal functions — 1 function +│ ├── Public/ # Exported (public) functions — 13 functions +│ ├── Private/ # Internal functions — 2 functions │ ├── en-US/ │ │ └── Messages.psd1 # Localized string resources │ ├── PowerShellBuild.psd1 # Module manifest (version, deps, exports) @@ -67,8 +67,8 @@ The hashtable is organized into sections: | Section | Purpose | |---------|---------| | `General` | ProjectRoot, SrcRootDir, ModuleName, ModuleVersion, ModuleManifestPath | -| `Build` | OutDir, ModuleOutDir, CompileModule, CompileDirectories, CopyDirectories, Exclude | -| `Test` | Enabled, RootDir, OutputFile, OutputFormat, ScriptAnalysis, CodeCoverage, ImportModule, SkipRemainingOnFailure, OutputVerbosity | +| `Build` | OutDir, ModuleOutDir, CompileModule, CompileDirectories, CopyDirectories, Exclude, EnableTaskCaching | +| `Test` | Enabled, RootDir, OutputFile, OutputFormat, ScriptAnalysis, CodeCoverage, ImportModule, SkipRemainingOnFailure, OutputVerbosity, OutputMode, PesterConfigurationPath | | `Help` | UpdatableHelpOutDir, DefaultLocale, ConvertReadMeToAboutHelp | | `Docs` | RootDir, Overwrite, AlphabeticParamsOrder, ExcludeDontShow, UseFullTypeName | | `Publish` | PSRepository, PSRepositoryApiKey, PSRepositoryCredential | @@ -124,8 +124,11 @@ All functions reside in `PowerShellBuild/Public/`. | `Test-PSBuildPester` | Runs Pester tests with configurable output and coverage | | `Test-PSBuildScriptAnalysis` | Runs PSScriptAnalyzer with configurable severity threshold | | `Publish-PSBuildModule` | Publishes the built module to a PowerShell repository | +| `Format-PSBuildResult` | Formats a PsakeBuildResult for Human, JSON, or GitHubActions output | -Private helper: `Remove-ExcludedItem` — filters file system items by regex patterns during builds. +Private helpers: +- `Remove-ExcludedItem` — filters file system items by regex patterns during builds +- `ConvertTo-PSBuildLLMOutput` — converts Pester results to structured JSON for LLM consumption ### Invoke-Build Alias @@ -207,7 +210,7 @@ Defined in `requirements.psd1`, installed via **PSDepend**: |--------|---------| | BuildHelpers | 2.0.16 | | Pester | ≥ 5.6.1 | -| psake | 4.9.0 | +| psake | 5.0.0 | | PSScriptAnalyzer | 1.24.0 | | InvokeBuild | 5.8.1 | | platyPS | 0.14.2 | @@ -386,7 +389,7 @@ After a successful build, output is in `Output/PowerShellBuild//`: ``` Output/ └── PowerShellBuild/ - └── 0.7.3/ + └── 1.0.0/ ├── Public/ # (when CompileModule = $false) ├── Private/ ├── en-US/ diff --git a/PowerShellBuild/IB.tasks.ps1 b/PowerShellBuild/IB.tasks.ps1 index fe54942..3bb8947 100644 --- a/PowerShellBuild/IB.tasks.ps1 +++ b/PowerShellBuild/IB.tasks.ps1 @@ -13,7 +13,14 @@ Task Clean Init, { } # Synopsis: Builds module based on source directory -Task StageFiles Clean, { +Task StageFiles -Inputs { + Get-ChildItem -Path $PSBPreference.General.SrcRootDir -Recurse -File | + Where-Object { $_.Extension -in '.ps1', '.psm1', '.psd1', '.ps1xml', '.txt' } +} -Outputs { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File + } +} Clean, { $buildParams = @{ Path = $PSBPreference.General.SrcRootDir ModuleName = $PSBPreference.General.ModuleName @@ -59,13 +66,19 @@ $analyzePreReqs = { } # Synopsis: Execute PSScriptAnalyzer tests -Task Analyze -If (. $analyzePreReqs) Build, { +Task Analyze -If (. $analyzePreReqs) -Inputs { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Include '*.ps1', '*.psm1', '*.psd1' +} -Outputs { + Join-Path $PSBPreference.Build.OutDir '.analyze-ok' +} Build, { $analyzeParams = @{ Path = $PSBPreference.Build.ModuleOutDir SeverityThreshold = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel SettingsPath = $PSBPreference.Test.ScriptAnalysis.SettingsPath } Test-PSBuildScriptAnalysis @analyzeParams + # Write marker file for cache validation + Set-Content -Path (Join-Path $PSBPreference.Build.OutDir '.analyze-ok') -Value (Get-Date -Format 'o') } $pesterPreReqs = { @@ -86,7 +99,13 @@ $pesterPreReqs = { } # Synopsis: Execute Pester tests -Task Pester -If (. $pesterPreReqs) Build, { +Task Pester -If (. $pesterPreReqs) -Inputs { + $testFiles = Get-ChildItem -Path $PSBPreference.Test.RootDir -Recurse -File -Filter '*.ps1' + $moduleFiles = Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -ErrorAction SilentlyContinue + @($testFiles) + @($moduleFiles) +} -Outputs { + $PSBPreference.Test.OutputFile +} Build, { $pesterParams = @{ Path = $PSBPreference.Test.RootDir ModuleName = $PSBPreference.General.ModuleName @@ -101,6 +120,10 @@ Task Pester -If (. $pesterPreReqs) Build, { ImportModule = $PSBPreference.Test.ImportModule SkipRemainingOnFailure = $PSBPreference.Test.SkipRemainingOnFailure OutputVerbosity = $PSBPreference.Test.OutputVerbosity + OutputMode = $PSBPreference.Test.OutputMode + } + if ($PSBPreference.Test.PesterConfigurationPath) { + $pesterParams.PesterConfigurationPath = $PSBPreference.Test.PesterConfigurationPath } Test-PSBuildPester @pesterParams } @@ -117,7 +140,15 @@ $genMarkdownPreReqs = { } # Synopsis: Generates PlatyPS markdown files from module help -Task GenerateMarkdown -if (. $genMarkdownPreReqs) StageFiles, { +Task GenerateMarkdown -if (. $genMarkdownPreReqs) -Inputs { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Include '*.ps1', '*.psm1' + } +} -Outputs { + if (Test-Path $PSBPreference.Docs.RootDir) { + Get-ChildItem -Path $PSBPreference.Docs.RootDir -Recurse -File -Filter '*.md' + } +} StageFiles, { $buildMDParams = @{ ModulePath = $PSBPreference.Build.ModuleOutDir ModuleName = $PSBPreference.General.ModuleName @@ -141,7 +172,15 @@ $genHelpFilesPreReqs = { } # Synopsis: Generates MAML-based help from PlatyPS markdown files -Task GenerateMAML -if (. $genHelpFilesPreReqs) GenerateMarkdown, { +Task GenerateMAML -if (. $genHelpFilesPreReqs) -Inputs { + if (Test-Path $PSBPreference.Docs.RootDir) { + Get-ChildItem -Path $PSBPreference.Docs.RootDir -Recurse -File -Filter '*.md' + } +} -Outputs { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Filter '*-help.xml' + } +} GenerateMarkdown, { Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir } @@ -155,7 +194,15 @@ $genUpdatableHelpPreReqs = { } # Synopsis: Create updatable help .cab file based on PlatyPS markdown help -Task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) BuildHelp, { +Task GenerateUpdatableHelp -if (. $genUpdatableHelpPreReqs) -Inputs { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Filter '*-help.xml' + } +} -Outputs { + if (Test-Path $PSBPreference.Help.UpdatableHelpOutDir) { + Get-ChildItem -Path $PSBPreference.Help.UpdatableHelpOutDir -Recurse -File -Filter '*.cab' + } +} BuildHelp, { Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir } diff --git a/PowerShellBuild/PowerShellBuild.psd1 b/PowerShellBuild/PowerShellBuild.psd1 index b19fdbf..dea88cb 100644 --- a/PowerShellBuild/PowerShellBuild.psd1 +++ b/PowerShellBuild/PowerShellBuild.psd1 @@ -1,17 +1,17 @@ @{ RootModule = 'PowerShellBuild.psm1' - ModuleVersion = '0.8.0' + ModuleVersion = '1.0.0' GUID = '15431eb8-be2d-4154-b8ad-4cb68a488e3d' Author = 'Brandon Olin' CompanyName = 'Community' Copyright = '(c) Brandon Olin. All rights reserved.' Description = 'A common psake and Invoke-Build task module for PowerShell projects' - PowerShellVersion = '3.0' + PowerShellVersion = '5.1' RequiredModules = @( @{ModuleName = 'BuildHelpers'; ModuleVersion = '2.0.16' } @{ModuleName = 'Pester'; ModuleVersion = '5.6.1' } @{ModuleName = 'platyPS'; ModuleVersion = '0.14.1' } - @{ModuleName = 'psake'; ModuleVersion = '4.9.0' } + @{ModuleName = 'psake'; ModuleVersion = '5.0.0' } ) FunctionsToExport = @( 'Build-PSBuildMAMLHelp' @@ -26,12 +26,14 @@ 'Publish-PSBuildModule' 'Test-PSBuildPester' 'Test-PSBuildScriptAnalysis' + 'Format-PSBuildResult' ) CmdletsToExport = @() VariablesToExport = @() AliasesToExport = @('*tasks') PrivateData = @{ PSData = @{ + Prerelease = 'alpha1' Tags = @('psake', 'build', 'InvokeBuild') LicenseUri = 'https://raw.githubusercontent.com/psake/PowerShellBuild/master/LICENSE' ProjectUri = 'https://github.com/psake/PowerShellBuild' diff --git a/PowerShellBuild/PowerShellBuild.psm1 b/PowerShellBuild/PowerShellBuild.psm1 index 0d4e7fa..56b08e9 100644 --- a/PowerShellBuild/PowerShellBuild.psm1 +++ b/PowerShellBuild/PowerShellBuild.psm1 @@ -36,6 +36,23 @@ PSScriptAnalyzerResults=PSScriptAnalyzer results: ScriptAnalyzerErrors=One or more ScriptAnalyzer errors were found! ScriptAnalyzerWarnings=One or more ScriptAnalyzer warnings were found! ScriptAnalyzerIssues=One or more ScriptAnalyzer issues were found! +NoCertificateFound=No valid code signing certificate was found. Verify the configured CertificateSource and that a certificate with a private key is available. +CertificateResolvedFromStore=Resolved code signing certificate from store [{0}]: Subject=[{1}] +CertificateResolvedFromThumbprint=Resolved code signing certificate by thumbprint [{0}]: Subject=[{1}] +CertificateResolvedFromEnvVar=Resolved code signing certificate from environment variable [{0}] +CertificateResolvedFromPfxFile=Resolved code signing certificate from PFX file [{0}] +SigningModuleFiles=Signing [{0}] file(s) matching [{1}] in [{2}]... +CreatingFileCatalog=Creating file catalog [{0}] (version {1})... +FileCatalogCreated=File catalog created: [{0}] +CertificateSourceAutoResolved=CertificateSource is 'Auto'. Resolved to '{0}'. +CertificateMissingPrivateKey=The resolved certificate does not have an accessible private key. Code signing requires a certificate with a private key. Subject=[{0}] +CertificateExpired=The resolved certificate has expired (NotAfter: {0}). Code signing requires a valid, unexpired certificate. Subject=[{1}] +CertificateMissingCodeSigningEku=The resolved certificate does not have the Code Signing Enhanced Key Usage (EKU: 1.3.6.1.5.5.7.3.3). Subject=[{0}] +CertificateSourceStoreNotSupported=CertificateSource 'Store' is only supported on Windows platforms. +LLMOutputHeader=Test results (structured output): +MinimalFailureLine=[FAIL] {0} ({1}:{2}) - {3} +PesterConfigLoaded=Loaded PesterConfiguration from [{0}] +InvalidPesterConfigPath=PesterConfiguration file [{0}] not found '@ } $importLocalizedDataSplat = @{ diff --git a/PowerShellBuild/Private/ConvertTo-PSBuildLLMOutput.ps1 b/PowerShellBuild/Private/ConvertTo-PSBuildLLMOutput.ps1 new file mode 100644 index 0000000..3213fea --- /dev/null +++ b/PowerShellBuild/Private/ConvertTo-PSBuildLLMOutput.ps1 @@ -0,0 +1,85 @@ +function ConvertTo-PSBuildLLMOutput { + <# + .SYNOPSIS + Converts Pester test results to structured JSON optimized for LLM consumption. + .DESCRIPTION + Takes a Pester TestResult (PassThru) object and produces a concise JSON structure + containing a summary and an array of failure details. Designed for machine consumption + where only actionable information (failures) matters. + .PARAMETER TestResult + The Pester test result object returned by Invoke-Pester with -PassThru. + #> + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)] + [object]$TestResult + ) + + $failures = [System.Collections.Generic.List[object]]::new() + + foreach ($container in $TestResult.Containers) { + Get-FailedTestsFromBlock -Blocks $container.Blocks -ContainerName $container.Name -Failures $failures + } + + $output = [ordered]@{ + summary = [ordered]@{ + total = $TestResult.TotalCount + passed = $TestResult.PassedCount + failed = $TestResult.FailedCount + skipped = $TestResult.SkippedCount + duration = [Math]::Round($TestResult.Duration.TotalSeconds, 2) + } + failures = $failures.ToArray() + } + + $output | ConvertTo-Json -Depth 10 +} + +function Get-FailedTestsFromBlock { + <# + .SYNOPSIS + Recursively collects failed tests from Pester block hierarchy. + #> + [CmdletBinding()] + param( + [object[]]$Blocks, + [string]$ContainerName, + [System.Collections.Generic.List[object]]$Failures + ) + + foreach ($block in $Blocks) { + foreach ($test in $block.Tests) { + if ($test.Result -eq 'Failed') { + $errorMessage = if ($test.ErrorRecord -and $test.ErrorRecord.Count -gt 0) { + $test.ErrorRecord[0].DisplayErrorMessage + } elseif ($test.ErrorRecord) { + "$($test.ErrorRecord)" + } else { + 'Unknown error' + } + + $file = $null + $line = $null + if ($test.ScriptBlock -and $test.ScriptBlock.File) { + $file = $test.ScriptBlock.File + $line = $test.ScriptBlock.StartPosition.StartLine + } + + $Failures.Add([ordered]@{ + test = $test.ExpandedPath + container = $ContainerName + file = $file + line = $line + error = $errorMessage + duration = [Math]::Round($test.Duration.TotalMilliseconds, 1) + }) + } + } + + # Recurse into nested blocks + if ($block.Blocks -and $block.Blocks.Count -gt 0) { + Get-FailedTestsFromBlock -Blocks $block.Blocks -ContainerName $ContainerName -Failures $Failures + } + } +} diff --git a/PowerShellBuild/Public/Format-PSBuildResult.ps1 b/PowerShellBuild/Public/Format-PSBuildResult.ps1 new file mode 100644 index 0000000..da44b14 --- /dev/null +++ b/PowerShellBuild/Public/Format-PSBuildResult.ps1 @@ -0,0 +1,105 @@ +function Format-PSBuildResult { + <# + .SYNOPSIS + Formats a PsakeBuildResult for human, CI, or LLM consumption. + .DESCRIPTION + Takes a PsakeBuildResult object from psake 5.0.0's Invoke-psake and formats it + according to the specified output format. Useful for CI pipelines, LLM-driven + builds, and human-readable summaries. + .PARAMETER Result + The PsakeBuildResult object returned by Invoke-psake. + .PARAMETER Format + Output format. 'Human' (default) produces a readable table. 'JSON' produces + structured JSON with task durations and cache hits. 'GitHubActions' emits + workflow command annotations. + .EXAMPLE + PS> $result = Invoke-psake -buildFile ./psakeFile.ps1 + PS> Format-PSBuildResult -Result $result + + Format the build result as a human-readable table. + .EXAMPLE + PS> $result = Invoke-psake -buildFile ./psakeFile.ps1 + PS> Format-PSBuildResult -Result $result -Format JSON + + Format the build result as structured JSON. + .EXAMPLE + PS> $result = Invoke-psake -buildFile ./psakeFile.ps1 + PS> Format-PSBuildResult -Result $result -Format GitHubActions + + Format the build result with GitHub Actions workflow annotations. + #> + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [object]$Result, + + [ValidateSet('Human', 'JSON', 'GitHubActions')] + [string]$Format = 'Human' + ) + + process { + switch ($Format) { + 'Human' { + $status = if ($Result.Success) { 'SUCCEEDED' } else { 'FAILED' } + Write-Host "`nBuild $status" -ForegroundColor $(if ($Result.Success) { 'Green' } else { 'Red' }) + Write-Host "Duration: $([Math]::Round($Result.Duration.TotalSeconds, 2))s`n" + + if ($Result.TaskResults) { + $tableData = $Result.TaskResults | ForEach-Object { + [PSCustomObject]@{ + Task = $_.Name + Status = $_.Status + Duration = '{0:N2}s' -f $_.Duration.TotalSeconds + Cached = if ($_.Cached) { 'Yes' } else { 'No' } + } + } + $tableData | Format-Table -AutoSize + } + + if (-not $Result.Success -and $Result.ErrorMessage) { + Write-Host "Error: $($Result.ErrorMessage)" -ForegroundColor Red + } + } + 'JSON' { + $jsonData = [ordered]@{ + success = $Result.Success + duration = [Math]::Round($Result.Duration.TotalSeconds, 2) + } + + if ($Result.TaskResults) { + $jsonData.tasks = @($Result.TaskResults | ForEach-Object { + [ordered]@{ + name = $_.Name + status = $_.Status + duration = [Math]::Round($_.Duration.TotalSeconds, 2) + cached = [bool]$_.Cached + } + }) + } + + if (-not $Result.Success -and $Result.ErrorMessage) { + $jsonData.error = $Result.ErrorMessage + } + + $jsonData | ConvertTo-Json -Depth 5 + } + 'GitHubActions' { + if ($Result.TaskResults) { + foreach ($taskResult in $Result.TaskResults) { + if ($taskResult.Status -eq 'Failed') { + Write-Host "::error title=Task '$($taskResult.Name)' failed::$($taskResult.ErrorMessage)" + } elseif ($taskResult.Cached) { + Write-Host "::notice title=Task '$($taskResult.Name)' cached::Skipped (cached)" + } + } + } + + if ($Result.Success) { + Write-Host "::notice title=Build succeeded::Completed in $([Math]::Round($Result.Duration.TotalSeconds, 2))s" + } else { + Write-Host "::error title=Build failed::$($Result.ErrorMessage)" + } + } + } + } +} diff --git a/PowerShellBuild/Public/Test-PSBuildPester.ps1 b/PowerShellBuild/Public/Test-PSBuildPester.ps1 index 3dbd4a9..8197a50 100644 --- a/PowerShellBuild/Public/Test-PSBuildPester.ps1 +++ b/PowerShellBuild/Public/Test-PSBuildPester.ps1 @@ -3,7 +3,9 @@ function Test-PSBuildPester { .SYNOPSIS Execute Pester tests for module. .DESCRIPTION - Execute Pester tests for module. + Execute Pester tests for module. Supports individual parameter configuration, + external PesterConfiguration files, and direct PesterConfiguration object passthrough. + Includes an LLM output mode that produces structured JSON with only failure details. .PARAMETER Path Directory Pester tests to execute. .PARAMETER ModuleName @@ -30,12 +32,30 @@ function Test-PSBuildPester { Skip remaining tests after failure for selected scope. Options are None, Run, Container and Block. Default: None. .PARAMETER OutputVerbosity The verbosity of output, options are None, Normal, Detailed and Diagnostic. Default is Detailed. + .PARAMETER OutputMode + Controls how test results are presented. 'Detailed' (default) shows full Pester output. + 'Minimal' shows only failures in a compact format. 'LLM' suppresses console output and + emits structured JSON optimized for machine consumption. + .PARAMETER PesterConfigurationPath + Path to an external PesterConfiguration .psd1 file. When set, loaded as the base + PesterConfiguration with explicit parameter values overlaid on top. + .PARAMETER Configuration + A pre-built PesterConfiguration object. When provided, individual Pester parameters + are ignored and this configuration is used directly. .EXAMPLE PS> Test-PSBuildPester -Path ./tests -ModuleName MyModule -OutputPath ./out/testResults.xml Run Pester tests in ./tests and save results to ./out/testResults.xml + .EXAMPLE + PS> Test-PSBuildPester -Path ./tests -ModuleName MyModule -OutputMode LLM + + Run Pester tests with structured JSON output optimized for LLM consumption + .EXAMPLE + PS> Test-PSBuildPester -Path ./tests -Configuration $myConfig + + Run Pester tests using a pre-built PesterConfiguration object #> - [CmdletBinding()] + [CmdletBinding(DefaultParameterSetName = 'Individual')] param( [parameter(Mandatory)] [string]$Path, @@ -44,27 +64,45 @@ function Test-PSBuildPester { [string]$ModuleManifest, + [Parameter(ParameterSetName = 'Individual')] [string]$OutputPath, + [Parameter(ParameterSetName = 'Individual')] [string]$OutputFormat = 'NUnit2.5', + [Parameter(ParameterSetName = 'Individual')] [switch]$CodeCoverage, + [Parameter(ParameterSetName = 'Individual')] [double]$CodeCoverageThreshold, + [Parameter(ParameterSetName = 'Individual')] [string[]]$CodeCoverageFiles = @(), + [Parameter(ParameterSetName = 'Individual')] [string]$CodeCoverageOutputFile = 'coverage.xml', + [Parameter(ParameterSetName = 'Individual')] [string]$CodeCoverageOutputFileFormat = 'JaCoCo', [switch]$ImportModule, + [Parameter(ParameterSetName = 'Individual')] [ValidateSet('None', 'Run', 'Container', 'Block')] [string]$SkipRemainingOnFailure = 'None', + [Parameter(ParameterSetName = 'Individual')] [ValidateSet('None', 'Normal', 'Detailed', 'Diagnostic')] - [string]$OutputVerbosity = 'Detailed' + [string]$OutputVerbosity = 'Detailed', + + [ValidateSet('Detailed', 'Minimal', 'LLM')] + [string]$OutputMode = 'Detailed', + + [Parameter(ParameterSetName = 'Individual')] + [string]$PesterConfigurationPath, + + [Parameter(ParameterSetName = 'Configuration')] + [object]$Configuration ) if (-not (Get-Module -Name Pester)) { @@ -85,25 +123,78 @@ function Test-PSBuildPester { Push-Location -LiteralPath $Path Import-Module Pester -MinimumVersion 5.0.0 - $configuration = [PesterConfiguration]::Default - $configuration.Output.Verbosity = $OutputVerbosity - $configuration.Run.PassThru = $true - $configuration.Run.SkipRemainingOnFailure = $SkipRemainingOnFailure - $configuration.TestResult.Enabled = -not [string]::IsNullOrEmpty($OutputPath) - $configuration.TestResult.OutputPath = $OutputPath - $configuration.TestResult.OutputFormat = $OutputFormat - if ($CodeCoverage.IsPresent) { - $configuration.CodeCoverage.Enabled = $true - if ($CodeCoverageFiles.Count -gt 0) { - $configuration.CodeCoverage.Path = $CodeCoverageFiles + # Build PesterConfiguration based on parameter set + if ($PSCmdlet.ParameterSetName -eq 'Configuration' -and $Configuration) { + $configuration = $Configuration + } elseif ($PesterConfigurationPath) { + # Load external config file as base, overlay explicit params + if (-not (Test-Path $PesterConfigurationPath)) { + Write-Error ($LocalizedData.InvalidPesterConfigPath -f $PesterConfigurationPath) + return + } + Write-Verbose ($LocalizedData.PesterConfigLoaded -f $PesterConfigurationPath) + $configData = Import-PowerShellDataFile -Path $PesterConfigurationPath + $configuration = [PesterConfiguration]$configData + # Overlay explicit parameter values on top of file-based config + $configuration.Run.PassThru = $true + if ($OutputMode -eq 'LLM') { + $configuration.Output.Verbosity = 'None' + } elseif ($OutputMode -eq 'Minimal') { + $configuration.Output.Verbosity = 'Normal' + } + } else { + # Build from individual parameters (backward-compatible path) + $configuration = [PesterConfiguration]::Default + + # Apply OutputMode overrides to verbosity + switch ($OutputMode) { + 'LLM' { + $configuration.Output.Verbosity = 'None' + } + 'Minimal' { + $configuration.Output.Verbosity = 'Normal' + } + default { + $configuration.Output.Verbosity = $OutputVerbosity + } + } + + $configuration.Run.PassThru = $true + $configuration.Run.SkipRemainingOnFailure = $SkipRemainingOnFailure + $configuration.TestResult.Enabled = -not [string]::IsNullOrEmpty($OutputPath) + $configuration.TestResult.OutputPath = $OutputPath + $configuration.TestResult.OutputFormat = $OutputFormat + + if ($CodeCoverage.IsPresent) { + $configuration.CodeCoverage.Enabled = $true + if ($CodeCoverageFiles.Count -gt 0) { + $configuration.CodeCoverage.Path = $CodeCoverageFiles + } + $configuration.CodeCoverage.OutputPath = $CodeCoverageOutputFile + $configuration.CodeCoverage.OutputFormat = $CodeCoverageOutputFileFormat } - $configuration.CodeCoverage.OutputPath = $CodeCoverageOutputFile - $configuration.CodeCoverage.OutputFormat = $CodeCoverageOutputFileFormat } $testResult = Invoke-Pester -Configuration $configuration -Verbose:$VerbosePreference + # Post-process results based on OutputMode + switch ($OutputMode) { + 'LLM' { + $jsonOutput = ConvertTo-PSBuildLLMOutput -TestResult $testResult + Write-Output $jsonOutput + } + 'Minimal' { + if ($testResult.FailedCount -gt 0) { + foreach ($container in $testResult.Containers) { + foreach ($block in $container.Blocks) { + _WriteMinimalFailures -Block $block -ContainerName $container.Name + } + } + } + } + } + if ($testResult.FailedCount -gt 0) { throw $LocalizedData.PesterTestsFailed } @@ -137,8 +228,40 @@ function Test-PSBuildPester { Write-Error ($LocalizedData.CodeCoverageCodeCoverageFileNotFound -f $CodeCoverageOutputFile) } } + + # Always return the test result object for programmatic access + $testResult } finally { Pop-Location Remove-Module $ModuleName -ErrorAction SilentlyContinue } } + +function _WriteMinimalFailures { + <# + .SYNOPSIS + Recursively writes minimal failure lines from Pester blocks. + #> + [CmdletBinding()] + param( + [object]$Block, + [string]$ContainerName + ) + + foreach ($test in $Block.Tests) { + if ($test.Result -eq 'Failed') { + $errorMsg = if ($test.ErrorRecord -and $test.ErrorRecord.Count -gt 0) { + $test.ErrorRecord[0].DisplayErrorMessage + } else { + "$($test.ErrorRecord)" + } + $file = if ($test.ScriptBlock.File) { $test.ScriptBlock.File } else { $ContainerName } + $line = if ($test.ScriptBlock.StartPosition) { $test.ScriptBlock.StartPosition.StartLine } else { 0 } + Write-Host ($LocalizedData.MinimalFailureLine -f $test.ExpandedPath, $file, $line, $errorMsg) -ForegroundColor Red + } + } + + foreach ($childBlock in $Block.Blocks) { + _WriteMinimalFailures -Block $childBlock -ContainerName $ContainerName + } +} diff --git a/PowerShellBuild/build.properties.ps1 b/PowerShellBuild/build.properties.ps1 index 2f1c0b0..6572e90 100644 --- a/PowerShellBuild/build.properties.ps1 +++ b/PowerShellBuild/build.properties.ps1 @@ -44,6 +44,10 @@ $moduleVersion = (Import-PowerShellDataFile -Path $env:BHPSModuleManifest).Modul # List of files (regular expressions) to exclude from output directory Exclude = @() + + # When $true (default), cacheable tasks use psake 5.0.0 content-addressed caching. + # Set $false to force all tasks to re-execute (equivalent to -NoCache). + EnableTaskCaching = $true } Test = @{ # Enable/disable Pester tests @@ -103,6 +107,16 @@ $moduleVersion = (Import-PowerShellDataFile -Path $env:BHPSModuleManifest).Modul # Set verbosity of output. Options are None, Normal, Detailed and Diagnostic. Default: Detailed. OutputVerbosity = 'Detailed' + + # Output mode for test results. + # 'Detailed' = verbose human-readable (default) + # 'Minimal' = failures only, compact summary + # 'LLM' = structured JSON with failure details, optimized for machine consumption + OutputMode = 'Detailed' + + # Path to an external PesterConfiguration .psd1 file. + # When set, loaded as base config; explicit $PSBPreference.Test values overlay on top. + PesterConfigurationPath = $null } Help = @{ # Path to updatable help CAB diff --git a/PowerShellBuild/en-US/Messages.psd1 b/PowerShellBuild/en-US/Messages.psd1 index 58aff5e..213b99b 100644 --- a/PowerShellBuild/en-US/Messages.psd1 +++ b/PowerShellBuild/en-US/Messages.psd1 @@ -36,4 +36,8 @@ CertificateMissingPrivateKey=The resolved certificate does not have an accessibl CertificateExpired=The resolved certificate has expired (NotAfter: {0}). Code signing requires a valid, unexpired certificate. Subject=[{1}] CertificateMissingCodeSigningEku=The resolved certificate does not have the Code Signing Enhanced Key Usage (EKU: 1.3.6.1.5.5.7.3.3). Subject=[{0}] CertificateSourceStoreNotSupported=CertificateSource 'Store' is only supported on Windows platforms. +LLMOutputHeader=Test results (structured output): +MinimalFailureLine=[FAIL] {0} ({1}:{2}) - {3} +PesterConfigLoaded=Loaded PesterConfiguration from [{0}] +InvalidPesterConfigPath=PesterConfiguration file [{0}] not found '@ diff --git a/PowerShellBuild/psakeFile.ps1 b/PowerShellBuild/psakeFile.ps1 index cff79f9..74ac12f 100644 --- a/PowerShellBuild/psakeFile.ps1 +++ b/PowerShellBuild/psakeFile.ps1 @@ -1,4 +1,6 @@ # spell-checker:ignore Reqs +Version 5 + # Load in build settings Remove-Variable -Name PSBPreference -Scope Script -Force -ErrorAction Ignore Set-Variable -Name PSBPreference -Option ReadOnly -Scope Script -Value (. ([IO.Path]::Combine($PSScriptRoot, 'build.properties.ps1'))) @@ -70,45 +72,68 @@ if ($null -eq $PSBSignDependency) { # Can't have two 'default' tasks # Task default -depends Test -Task Init { - Initialize-PSBuild -UseBuildHelpers -BuildEnvironment $PSBPreference -Verbose:($VerbosePreference -eq 'Continue') -} -Description 'Initialize build environment variables' - -Task Clean -Depends $PSBCleanDependency { - Clear-PSBuildOutputFolder -Path $PSBPreference.Build.ModuleOutDir -Verbose:($VerbosePreference -eq 'Continue') -} -Description 'Clears module output directory' +Task Init @{ + Action = { + Initialize-PSBuild -UseBuildHelpers -BuildEnvironment $PSBPreference -Verbose:($VerbosePreference -eq 'Continue') + } + Description = 'Initialize build environment variables' +} -Task StageFiles -Depends $PSBStageFilesDependency { - $buildParams = @{ - Path = $PSBPreference.General.SrcRootDir - ModuleName = $PSBPreference.General.ModuleName - DestinationPath = $PSBPreference.Build.ModuleOutDir - Exclude = $PSBPreference.Build.Exclude - Compile = $PSBPreference.Build.CompileModule - CompileDirectories = $PSBPreference.Build.CompileDirectories - CopyDirectories = $PSBPreference.Build.CopyDirectories - Culture = $PSBPreference.Help.DefaultLocale +Task Clean @{ + DependsOn = $PSBCleanDependency + Action = { + Clear-PSBuildOutputFolder -Path $PSBPreference.Build.ModuleOutDir -Verbose:($VerbosePreference -eq 'Continue') } + Description = 'Clears module output directory' +} - if ($PSBPreference.Help.ConvertReadMeToAboutHelp) { - $readMePath = Get-ChildItem -Path $PSBPreference.General.ProjectRoot -Include 'readme.md', 'readme.markdown', 'readme.txt' -Depth 1 | - Select-Object -First 1 - if ($readMePath) { - $buildParams.ReadMePath = $readMePath +Task StageFiles @{ + DependsOn = $PSBStageFilesDependency + Inputs = { + Get-ChildItem -Path $PSBPreference.General.SrcRootDir -Recurse -File | + Where-Object { $_.Extension -in '.ps1', '.psm1', '.psd1', '.ps1xml', '.txt' } + } + Outputs = { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File } } + Action = { + $buildParams = @{ + Path = $PSBPreference.General.SrcRootDir + ModuleName = $PSBPreference.General.ModuleName + DestinationPath = $PSBPreference.Build.ModuleOutDir + Exclude = $PSBPreference.Build.Exclude + Compile = $PSBPreference.Build.CompileModule + CompileDirectories = $PSBPreference.Build.CompileDirectories + CopyDirectories = $PSBPreference.Build.CopyDirectories + Culture = $PSBPreference.Help.DefaultLocale + } - # only add these configuration values to the build parameters if they have been been set - 'CompileHeader', 'CompileFooter', 'CompileScriptHeader', 'CompileScriptFooter' | ForEach-Object { - if ($PSBPreference.Build.Keys -contains $_) { - $buildParams.$_ = $PSBPreference.Build.$_ + if ($PSBPreference.Help.ConvertReadMeToAboutHelp) { + $readMePath = Get-ChildItem -Path $PSBPreference.General.ProjectRoot -Include 'readme.md', 'readme.markdown', 'readme.txt' -Depth 1 | + Select-Object -First 1 + if ($readMePath) { + $buildParams.ReadMePath = $readMePath + } } - } - Build-PSBuildModule @buildParams -Verbose:($VerbosePreference -eq 'Continue') -} -Description 'Builds module based on source directory' + # only add these configuration values to the build parameters if they have been been set + 'CompileHeader', 'CompileFooter', 'CompileScriptHeader', 'CompileScriptFooter' | ForEach-Object { + if ($PSBPreference.Build.Keys -contains $_) { + $buildParams.$_ = $PSBPreference.Build.$_ + } + } -Task Build -Depends $PSBBuildDependency -Description 'Builds module and generate help documentation' + Build-PSBuildModule @buildParams -Verbose:($VerbosePreference -eq 'Continue') + } + Description = 'Builds module based on source directory' +} + +Task Build @{ + DependsOn = $PSBBuildDependency + Description = 'Builds module and generate help documentation' +} $analyzePreReqs = { $result = $true @@ -122,14 +147,27 @@ $analyzePreReqs = { } $result } -Task Analyze -Depends $PSBAnalyzeDependency -PreCondition $analyzePreReqs { - $analyzeParams = @{ - Path = $PSBPreference.Build.ModuleOutDir - SeverityThreshold = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel - SettingsPath = $PSBPreference.Test.ScriptAnalysis.SettingsPath +Task Analyze @{ + DependsOn = $PSBAnalyzeDependency + PreCondition = $analyzePreReqs + Inputs = { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Include '*.ps1', '*.psm1', '*.psd1' + } + Outputs = { + Join-Path $PSBPreference.Build.OutDir '.analyze-ok' + } + Action = { + $analyzeParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + SeverityThreshold = $PSBPreference.Test.ScriptAnalysis.FailBuildOnSeverityLevel + SettingsPath = $PSBPreference.Test.ScriptAnalysis.SettingsPath + } + Test-PSBuildScriptAnalysis @analyzeParams -Verbose:($VerbosePreference -eq 'Continue') + # Write marker file for cache validation + Set-Content -Path (Join-Path $PSBPreference.Build.OutDir '.analyze-ok') -Value (Get-Date -Format 'o') } - Test-PSBuildScriptAnalysis @analyzeParams -Verbose:($VerbosePreference -eq 'Continue') -} -Description 'Execute PSScriptAnalyzer tests' + Description = 'Execute PSScriptAnalyzer tests' +} $pesterPreReqs = { $result = $true @@ -147,30 +185,52 @@ $pesterPreReqs = { } return $result } -Task Pester -Depends $PSBPesterDependency -PreCondition $pesterPreReqs { - $pesterParams = @{ - Path = $PSBPreference.Test.RootDir - ModuleName = $PSBPreference.General.ModuleName - ModuleManifest = Join-Path $PSBPreference.Build.ModuleOutDir "$($PSBPreference.General.ModuleName).psd1" - OutputPath = $PSBPreference.Test.OutputFile - OutputFormat = $PSBPreference.Test.OutputFormat - CodeCoverage = $PSBPreference.Test.CodeCoverage.Enabled - CodeCoverageThreshold = $PSBPreference.Test.CodeCoverage.Threshold - CodeCoverageFiles = $PSBPreference.Test.CodeCoverage.Files - CodeCoverageOutputFile = $PSBPreference.Test.CodeCoverage.OutputFile - CodeCoverageOutputFileFormat = $PSBPreference.Test.CodeCoverage.OutputFileFormat - ImportModule = $PSBPreference.Test.ImportModule - SkipRemainingOnFailure = $PSBPreference.Test.SkipRemainingOnFailure - OutputVerbosity = $PSBPreference.Test.OutputVerbosity - Verbose = $VerbosePreference -eq 'Continue' - } - Test-PSBuildPester @pesterParams -} -Description 'Execute Pester tests' - -Task Test -Depends $PSBTestDependency { -} -Description 'Execute Pester and ScriptAnalyzer tests' - -Task BuildHelp -Depends $PSBBuildHelpDependency {} -Description 'Builds help documentation' +Task Pester @{ + DependsOn = $PSBPesterDependency + PreCondition = $pesterPreReqs + Inputs = { + $testFiles = Get-ChildItem -Path $PSBPreference.Test.RootDir -Recurse -File -Filter '*.ps1' + $moduleFiles = Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -ErrorAction SilentlyContinue + @($testFiles) + @($moduleFiles) + } + Outputs = { + $PSBPreference.Test.OutputFile + } + Action = { + $pesterParams = @{ + Path = $PSBPreference.Test.RootDir + ModuleName = $PSBPreference.General.ModuleName + ModuleManifest = Join-Path $PSBPreference.Build.ModuleOutDir "$($PSBPreference.General.ModuleName).psd1" + OutputPath = $PSBPreference.Test.OutputFile + OutputFormat = $PSBPreference.Test.OutputFormat + CodeCoverage = $PSBPreference.Test.CodeCoverage.Enabled + CodeCoverageThreshold = $PSBPreference.Test.CodeCoverage.Threshold + CodeCoverageFiles = $PSBPreference.Test.CodeCoverage.Files + CodeCoverageOutputFile = $PSBPreference.Test.CodeCoverage.OutputFile + CodeCoverageOutputFileFormat = $PSBPreference.Test.CodeCoverage.OutputFileFormat + ImportModule = $PSBPreference.Test.ImportModule + SkipRemainingOnFailure = $PSBPreference.Test.SkipRemainingOnFailure + OutputVerbosity = $PSBPreference.Test.OutputVerbosity + OutputMode = $PSBPreference.Test.OutputMode + Verbose = $VerbosePreference -eq 'Continue' + } + if ($PSBPreference.Test.PesterConfigurationPath) { + $pesterParams.PesterConfigurationPath = $PSBPreference.Test.PesterConfigurationPath + } + Test-PSBuildPester @pesterParams + } + Description = 'Execute Pester tests' +} + +Task Test @{ + DependsOn = $PSBTestDependency + Description = 'Execute Pester and ScriptAnalyzer tests' +} + +Task BuildHelp @{ + DependsOn = $PSBBuildHelpDependency + Description = 'Builds help documentation' +} $genMarkdownPreReqs = { $result = $true @@ -180,20 +240,35 @@ $genMarkdownPreReqs = { } $result } -Task GenerateMarkdown -Depends $PSBGenerateMarkdownDependency -PreCondition $genMarkdownPreReqs { - $buildMDParams = @{ - ModulePath = $PSBPreference.Build.ModuleOutDir - ModuleName = $PSBPreference.General.ModuleName - DocsPath = $PSBPreference.Docs.RootDir - Locale = $PSBPreference.Help.DefaultLocale - Overwrite = $PSBPreference.Docs.Overwrite - AlphabeticParamsOrder = $PSBPreference.Docs.AlphabeticParamsOrder - ExcludeDontShow = $PSBPreference.Docs.ExcludeDontShow - UseFullTypeName = $PSBPreference.Docs.UseFullTypeName - Verbose = $VerbosePreference -eq 'Continue' - } - Build-PSBuildMarkdown @buildMDParams -} -Description 'Generates PlatyPS markdown files from module help' +Task GenerateMarkdown @{ + DependsOn = $PSBGenerateMarkdownDependency + PreCondition = $genMarkdownPreReqs + Inputs = { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Include '*.ps1', '*.psm1' + } + } + Outputs = { + if (Test-Path $PSBPreference.Docs.RootDir) { + Get-ChildItem -Path $PSBPreference.Docs.RootDir -Recurse -File -Filter '*.md' + } + } + Action = { + $buildMDParams = @{ + ModulePath = $PSBPreference.Build.ModuleOutDir + ModuleName = $PSBPreference.General.ModuleName + DocsPath = $PSBPreference.Docs.RootDir + Locale = $PSBPreference.Help.DefaultLocale + Overwrite = $PSBPreference.Docs.Overwrite + AlphabeticParamsOrder = $PSBPreference.Docs.AlphabeticParamsOrder + ExcludeDontShow = $PSBPreference.Docs.ExcludeDontShow + UseFullTypeName = $PSBPreference.Docs.UseFullTypeName + Verbose = $VerbosePreference -eq 'Continue' + } + Build-PSBuildMarkdown @buildMDParams + } + Description = 'Generates PlatyPS markdown files from module help' +} $genHelpFilesPreReqs = { $result = $true @@ -203,9 +278,24 @@ $genHelpFilesPreReqs = { } $result } -Task GenerateMAML -Depends $PSBGenerateMAMLDependency -PreCondition $genHelpFilesPreReqs { - Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir -Verbose:($VerbosePreference -eq 'Continue') -} -Description 'Generates MAML-based help from PlatyPS markdown files' +Task GenerateMAML @{ + DependsOn = $PSBGenerateMAMLDependency + PreCondition = $genHelpFilesPreReqs + Inputs = { + if (Test-Path $PSBPreference.Docs.RootDir) { + Get-ChildItem -Path $PSBPreference.Docs.RootDir -Recurse -File -Filter '*.md' + } + } + Outputs = { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Filter '*-help.xml' + } + } + Action = { + Build-PSBuildMAMLHelp -Path $PSBPreference.Docs.RootDir -DestinationPath $PSBPreference.Build.ModuleOutDir -Verbose:($VerbosePreference -eq 'Continue') + } + Description = 'Generates MAML-based help from PlatyPS markdown files' +} $genUpdatableHelpPreReqs = { $result = $true @@ -215,29 +305,50 @@ $genUpdatableHelpPreReqs = { } $result } -Task GenerateUpdatableHelp -Depends $PSBGenerateUpdatableHelpDependency -PreCondition $genUpdatableHelpPreReqs { - Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir -Verbose:($VerbosePreference -eq 'Continue') -} -Description 'Create updatable help .cab file based on PlatyPS markdown help' - -Task Publish -Depends $PSBPublishDependency { - Assert -ConditionToCheck ($PSBPreference.Publish.PSRepositoryApiKey -or $PSBPreference.Publish.PSRepositoryCredential) -FailureMessage "API key or credential not defined to authenticate with [$($PSBPreference.Publish.PSRepository)] with." - - $publishParams = @{ - Path = $PSBPreference.Build.ModuleOutDir - Version = $PSBPreference.General.ModuleVersion - Repository = $PSBPreference.Publish.PSRepository - Verbose = $VerbosePreference +Task GenerateUpdatableHelp @{ + DependsOn = $PSBGenerateUpdatableHelpDependency + PreCondition = $genUpdatableHelpPreReqs + Inputs = { + if (Test-Path $PSBPreference.Build.ModuleOutDir) { + Get-ChildItem -Path $PSBPreference.Build.ModuleOutDir -Recurse -File -Filter '*-help.xml' + } + } + Outputs = { + if (Test-Path $PSBPreference.Help.UpdatableHelpOutDir) { + Get-ChildItem -Path $PSBPreference.Help.UpdatableHelpOutDir -Recurse -File -Filter '*.cab' + } } - if ($PSBPreference.Publish.PSRepositoryApiKey) { - $publishParams.ApiKey = $PSBPreference.Publish.PSRepositoryApiKey + Action = { + Build-PSBuildUpdatableHelp -DocsPath $PSBPreference.Docs.RootDir -OutputPath $PSBPreference.Help.UpdatableHelpOutDir -Verbose:($VerbosePreference -eq 'Continue') } + Description = 'Create updatable help .cab file based on PlatyPS markdown help' +} + +Task Publish @{ + DependsOn = $PSBPublishDependency + Action = { + Assert -ConditionToCheck ($PSBPreference.Publish.PSRepositoryApiKey -or $PSBPreference.Publish.PSRepositoryCredential) -FailureMessage "API key or credential not defined to authenticate with [$($PSBPreference.Publish.PSRepository)] with." + + $publishParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Version = $PSBPreference.General.ModuleVersion + Repository = $PSBPreference.Publish.PSRepository + Verbose = $VerbosePreference + } + if ($PSBPreference.Publish.PSRepositoryApiKey) { + $publishParams.ApiKey = $PSBPreference.Publish.PSRepositoryApiKey + } - if ($PSBPreference.Publish.PSRepositoryCredential) { - $publishParams.Credential = $PSBPreference.Publish.PSRepositoryCredential + if ($PSBPreference.Publish.PSRepositoryCredential) { + $publishParams.Credential = $PSBPreference.Publish.PSRepositoryCredential + } + + Publish-PSBuildModule @publishParams } + Description = 'Publish module to the defined PowerShell repository' +} - Publish-PSBuildModule @publishParams -} -Description 'Publish module to the defined PowerShell repository' +#region Signing Tasks $signModulePreReqs = { $result = $true @@ -251,43 +362,48 @@ $signModulePreReqs = { } $result } -Task SignModule -Depends $PSBSignModuleDependency -PreCondition $signModulePreReqs { - $certParams = @{ - CertificateSource = $PSBPreference.Sign.CertificateSource - CertStoreLocation = $PSBPreference.Sign.CertStoreLocation - CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar - CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar - SkipValidation = $PSBPreference.Sign.SkipCertificateValidation - Verbose = $VerbosePreference -eq 'Continue' - } - if ($PSBPreference.Sign.Thumbprint) { - $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint - } - if ($PSBPreference.Sign.PfxFilePath) { - $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath - } - if ($PSBPreference.Sign.PfxFilePassword) { - $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword - } +Task SignModule @{ + DependsOn = $PSBSignModuleDependency + PreCondition = $signModulePreReqs + Action = { + $certParams = @{ + CertificateSource = $PSBPreference.Sign.CertificateSource + CertStoreLocation = $PSBPreference.Sign.CertStoreLocation + CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar + CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar + SkipValidation = $PSBPreference.Sign.SkipCertificateValidation + Verbose = $VerbosePreference -eq 'Continue' + } + if ($PSBPreference.Sign.Thumbprint) { + $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint + } + if ($PSBPreference.Sign.PfxFilePath) { + $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath + } + if ($PSBPreference.Sign.PfxFilePassword) { + $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword + } - $certificate = if ($PSBPreference.Sign.Certificate) { - $PSBPreference.Sign.Certificate - } else { - Get-PSBuildCertificate @certParams - } + $certificate = if ($PSBPreference.Sign.Certificate) { + $PSBPreference.Sign.Certificate + } else { + Get-PSBuildCertificate @certParams + } - Assert ($null -ne $certificate) $LocalizedData.NoCertificateFound + Assert ($null -ne $certificate) $LocalizedData.NoCertificateFound - $signingParams = @{ - Path = $PSBPreference.Build.ModuleOutDir - Certificate = $certificate - TimestampServer = $PSBPreference.Sign.TimestampServer - HashAlgorithm = $PSBPreference.Sign.HashAlgorithm - Include = $PSBPreference.Sign.FilesToSign - Verbose = $VerbosePreference -eq 'Continue' + $signingParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Certificate = $certificate + TimestampServer = $PSBPreference.Sign.TimestampServer + HashAlgorithm = $PSBPreference.Sign.HashAlgorithm + Include = $PSBPreference.Sign.FilesToSign + Verbose = $VerbosePreference -eq 'Continue' + } + Invoke-PSBuildModuleSigning @signingParams } - Invoke-PSBuildModuleSigning @signingParams -} -Description 'Signs module files (*.psd1, *.psm1, *.ps1) with an Authenticode signature' + Description = 'Signs module files (*.psd1, *.psm1, *.ps1) with an Authenticode signature' +} $buildCatalogPreReqs = { $result = $true @@ -301,22 +417,27 @@ $buildCatalogPreReqs = { } $result } -Task BuildCatalog -Depends $PSBBuildCatalogDependency -PreCondition $buildCatalogPreReqs { - $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { - $PSBPreference.Sign.Catalog.FileName - } else { - "$($PSBPreference.General.ModuleName).cat" - } - $catalogFilePath = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath $catalogFileName +Task BuildCatalog @{ + DependsOn = $PSBBuildCatalogDependency + PreCondition = $buildCatalogPreReqs + Action = { + $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { + $PSBPreference.Sign.Catalog.FileName + } else { + "$($PSBPreference.General.ModuleName).cat" + } + $catalogFilePath = Join-Path -Path $PSBPreference.Build.ModuleOutDir -ChildPath $catalogFileName - $catalogParams = @{ - ModulePath = $PSBPreference.Build.ModuleOutDir - CatalogFilePath = $catalogFilePath - CatalogVersion = $PSBPreference.Sign.Catalog.Version - Verbose = $VerbosePreference -eq 'Continue' + $catalogParams = @{ + ModulePath = $PSBPreference.Build.ModuleOutDir + CatalogFilePath = $catalogFilePath + CatalogVersion = $PSBPreference.Sign.Catalog.Version + Verbose = $VerbosePreference -eq 'Continue' + } + New-PSBuildFileCatalog @catalogParams } - New-PSBuildFileCatalog @catalogParams -} -Description 'Creates a Windows catalog (.cat) file for the built module' + Description = 'Creates a Windows catalog (.cat) file for the built module' +} $signCatalogPreReqs = { $result = $true @@ -330,53 +451,66 @@ $signCatalogPreReqs = { } $result } -Task SignCatalog -Depends $PSBSignCatalogDependency -PreCondition $signCatalogPreReqs { - $certParams = @{ - CertificateSource = $PSBPreference.Sign.CertificateSource - CertStoreLocation = $PSBPreference.Sign.CertStoreLocation - CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar - CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar - SkipValidation = $PSBPreference.Sign.SkipCertificateValidation - Verbose = $VerbosePreference -eq 'Continue' - } - if ($PSBPreference.Sign.Thumbprint) { - $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint - } - if ($PSBPreference.Sign.PfxFilePath) { - $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath - } - if ($PSBPreference.Sign.PfxFilePassword) { - $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword - } +Task SignCatalog @{ + DependsOn = $PSBSignCatalogDependency + PreCondition = $signCatalogPreReqs + Action = { + $certParams = @{ + CertificateSource = $PSBPreference.Sign.CertificateSource + CertStoreLocation = $PSBPreference.Sign.CertStoreLocation + CertificateEnvVar = $PSBPreference.Sign.CertificateEnvVar + CertificatePasswordEnvVar = $PSBPreference.Sign.CertificatePasswordEnvVar + SkipValidation = $PSBPreference.Sign.SkipCertificateValidation + Verbose = $VerbosePreference -eq 'Continue' + } + if ($PSBPreference.Sign.Thumbprint) { + $certParams.Thumbprint = $PSBPreference.Sign.Thumbprint + } + if ($PSBPreference.Sign.PfxFilePath) { + $certParams.PfxFilePath = $PSBPreference.Sign.PfxFilePath + } + if ($PSBPreference.Sign.PfxFilePassword) { + $certParams.PfxFilePassword = $PSBPreference.Sign.PfxFilePassword + } - $certificate = if ($PSBPreference.Sign.Certificate) { - $PSBPreference.Sign.Certificate - } else { - Get-PSBuildCertificate @certParams - } + $certificate = if ($PSBPreference.Sign.Certificate) { + $PSBPreference.Sign.Certificate + } else { + Get-PSBuildCertificate @certParams + } - Assert ($null -ne $certificate) $LocalizedData.NoCertificateFound + Assert ($null -ne $certificate) $LocalizedData.NoCertificateFound - $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { - $PSBPreference.Sign.Catalog.FileName - } else { - "$($PSBPreference.General.ModuleName).cat" - } + $catalogFileName = if ($PSBPreference.Sign.Catalog.FileName) { + $PSBPreference.Sign.Catalog.FileName + } else { + "$($PSBPreference.General.ModuleName).cat" + } - $signingParams = @{ - Path = $PSBPreference.Build.ModuleOutDir - Certificate = $certificate - TimestampServer = $PSBPreference.Sign.TimestampServer - HashAlgorithm = $PSBPreference.Sign.HashAlgorithm - Include = @($catalogFileName) - Verbose = $VerbosePreference -eq 'Continue' + $signingParams = @{ + Path = $PSBPreference.Build.ModuleOutDir + Certificate = $certificate + TimestampServer = $PSBPreference.Sign.TimestampServer + HashAlgorithm = $PSBPreference.Sign.HashAlgorithm + Include = @($catalogFileName) + Verbose = $VerbosePreference -eq 'Continue' + } + Invoke-PSBuildModuleSigning @signingParams } - Invoke-PSBuildModuleSigning @signingParams -} -Description 'Signs the module catalog (.cat) file with an Authenticode signature' + Description = 'Signs the module catalog (.cat) file with an Authenticode signature' +} -Task Sign -Depends $PSBSignDependency {} -Description 'Signs module files and catalog (meta task)' +Task Sign @{ + DependsOn = $PSBSignDependency + Description = 'Signs module files and catalog (meta task)' +} -Task ? -Description 'Lists the available tasks' { - 'Available tasks:' - $psake.context.Peek().Tasks.Keys | Sort-Object +#endregion Signing Tasks + +Task ? @{ + Action = { + 'Available tasks:' + $psake.context.Peek().Tasks.Keys | Sort-Object + } + Description = 'Lists the available tasks' } diff --git a/build.ps1 b/build.ps1 index 8529de6..e5a3dba 100644 --- a/build.ps1 +++ b/build.ps1 @@ -55,6 +55,6 @@ if ($PSCmdlet.ParameterSetName -eq 'Help') { if ($PSGalleryApiKey) { $parameters['galleryApiKey'] = $PSGalleryApiKey } - Invoke-psake -buildFile $psakeFile -taskList $Task -nologo -parameters $parameters - exit ( [int]( -not $psake.build_success ) ) + $result = Invoke-psake -buildFile $psakeFile -taskList $Task -nologo -parameters $parameters + exit ( [int]( -not $result.Success ) ) } diff --git a/psakeFile.ps1 b/psakeFile.ps1 index 22a24c4..760fa5e 100644 --- a/psakeFile.ps1 +++ b/psakeFile.ps1 @@ -1,3 +1,5 @@ +Version 5 + properties { $settings = . ([IO.Path]::Combine($PSScriptRoot, 'build.settings.ps1')) if ($galleryApiKey) { diff --git a/requirements.psd1 b/requirements.psd1 index 5e22bd3..d120358 100755 --- a/requirements.psd1 +++ b/requirements.psd1 @@ -9,7 +9,12 @@ SkipPublisherCheck = $true } } - psake = '4.9.0' + psake = @{ + MinimumVerson = '5.0.0' + Parameters = @{ + AllowPrerelease = $true + } + } PSScriptAnalyzer = '1.24.0' InvokeBuild = '5.8.1' platyPS = '0.14.2' diff --git a/tests/FormatBuildResult.tests.ps1 b/tests/FormatBuildResult.tests.ps1 new file mode 100644 index 0000000..96b5451 --- /dev/null +++ b/tests/FormatBuildResult.tests.ps1 @@ -0,0 +1,110 @@ +BeforeAll { + Set-BuildEnvironment -Force + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $ENV:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + Import-Module (Join-Path $outputModVerDir "$($env:BHProjectName).psd1") -Force +} + +Describe 'Format-PSBuildResult' { + BeforeAll { + $script:mockTaskResults = @( + [PSCustomObject]@{ + Name = 'Init' + Status = 'Executed' + Duration = [timespan]::FromSeconds(0.5) + Cached = $false + }, + [PSCustomObject]@{ + Name = 'Build' + Status = 'Executed' + Duration = [timespan]::FromSeconds(2.3) + Cached = $false + }, + [PSCustomObject]@{ + Name = 'StageFiles' + Status = 'Skipped' + Duration = [timespan]::FromSeconds(0.01) + Cached = $true + } + ) + } + + Context 'Successful build' { + BeforeAll { + $script:successResult = [PSCustomObject]@{ + Success = $true + Duration = [timespan]::FromSeconds(3.5) + TaskResults = $script:mockTaskResults + ErrorMessage = $null + } + } + + It 'JSON format produces valid JSON' { + $output = Format-PSBuildResult -Result $script:successResult -Format JSON + { $output | ConvertFrom-Json } | Should -Not -Throw + } + + It 'JSON format includes success flag' { + $output = Format-PSBuildResult -Result $script:successResult -Format JSON + $parsed = $output | ConvertFrom-Json + $parsed.success | Should -BeTrue + } + + It 'JSON format includes task details' { + $output = Format-PSBuildResult -Result $script:successResult -Format JSON + $parsed = $output | ConvertFrom-Json + $parsed.tasks | Should -HaveCount 3 + $parsed.tasks[2].cached | Should -BeTrue + } + + It 'JSON format includes duration' { + $output = Format-PSBuildResult -Result $script:successResult -Format JSON + $parsed = $output | ConvertFrom-Json + $parsed.duration | Should -BeGreaterThan 0 + } + } + + Context 'Failed build' { + BeforeAll { + $failedTask = [PSCustomObject]@{ + Name = 'Test' + Status = 'Failed' + Duration = [timespan]::FromSeconds(1.0) + Cached = $false + ErrorMessage = 'Pester tests failed' + } + + $script:failedResult = [PSCustomObject]@{ + Success = $false + Duration = [timespan]::FromSeconds(5.0) + TaskResults = @($failedTask) + ErrorMessage = 'Build failed: Pester tests failed' + } + } + + It 'JSON format includes error for failed builds' { + $output = Format-PSBuildResult -Result $script:failedResult -Format JSON + $parsed = $output | ConvertFrom-Json + $parsed.success | Should -BeFalse + $parsed.error | Should -Not -BeNullOrEmpty + } + } + + Context 'Parameter validation' { + It 'Has a Format parameter with correct validate set' { + $cmd = Get-Command Format-PSBuildResult + $param = $cmd.Parameters['Format'] + $validateSet = $param.Attributes.Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }) + $validateSet.ValidValues | Should -Contain 'Human' + $validateSet.ValidValues | Should -Contain 'JSON' + $validateSet.ValidValues | Should -Contain 'GitHubActions' + } + + It 'Accepts pipeline input for Result' { + $cmd = Get-Command Format-PSBuildResult + $cmd.Parameters['Result'].Attributes.Where({ $_ -is [System.Management.Automation.ParameterAttribute] }).ValueFromPipeline | Should -BeTrue + } + } +} diff --git a/tests/LLMOutput.tests.ps1 b/tests/LLMOutput.tests.ps1 new file mode 100644 index 0000000..658ba5e --- /dev/null +++ b/tests/LLMOutput.tests.ps1 @@ -0,0 +1,117 @@ +BeforeAll { + Set-BuildEnvironment -Force + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $ENV:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + Import-Module (Join-Path $outputModVerDir "$($env:BHProjectName).psd1") -Force +} + +Describe 'ConvertTo-PSBuildLLMOutput' { + BeforeAll { + # Create a mock Pester test result object that mimics Invoke-Pester -PassThru output + $mockFailedTest = [PSCustomObject]@{ + Result = 'Failed' + ExpandedPath = 'Describe > Context > It should work' + ScriptBlock = [PSCustomObject]@{ + File = 'C:\tests\example.tests.ps1' + StartPosition = [PSCustomObject]@{ + StartLine = 42 + } + } + ErrorRecord = @( + [PSCustomObject]@{ + DisplayErrorMessage = 'Expected 5, but got 3.' + } + ) + Duration = [timespan]::FromMilliseconds(123.4) + } + + $mockPassedTest = [PSCustomObject]@{ + Result = 'Passed' + ExpandedPath = 'Describe > Context > It should also work' + ScriptBlock = [PSCustomObject]@{ + File = 'C:\tests\example.tests.ps1' + StartPosition = [PSCustomObject]@{ + StartLine = 50 + } + } + ErrorRecord = $null + Duration = [timespan]::FromMilliseconds(10.5) + } + + $mockBlock = [PSCustomObject]@{ + Tests = @($mockPassedTest, $mockFailedTest) + Blocks = @() + } + + $mockContainer = [PSCustomObject]@{ + Name = 'example.tests.ps1' + Blocks = @($mockBlock) + } + + $script:mockTestResult = [PSCustomObject]@{ + TotalCount = 2 + PassedCount = 1 + FailedCount = 1 + SkippedCount = 0 + Duration = [timespan]::FromSeconds(1.5) + Containers = @($mockContainer) + } + } + + It 'Produces valid JSON' { + $output = ConvertTo-PSBuildLLMOutput -TestResult $script:mockTestResult + { $output | ConvertFrom-Json } | Should -Not -Throw + } + + It 'Contains summary with expected keys' { + $output = ConvertTo-PSBuildLLMOutput -TestResult $script:mockTestResult + $parsed = $output | ConvertFrom-Json + $parsed.summary.total | Should -Be 2 + $parsed.summary.passed | Should -Be 1 + $parsed.summary.failed | Should -Be 1 + $parsed.summary.skipped | Should -Be 0 + $parsed.summary.duration | Should -BeGreaterThan 0 + } + + It 'Contains failure details with required fields' { + $output = ConvertTo-PSBuildLLMOutput -TestResult $script:mockTestResult + $parsed = $output | ConvertFrom-Json + $parsed.failures | Should -HaveCount 1 + $failure = $parsed.failures[0] + $failure.test | Should -Be 'Describe > Context > It should work' + $failure.container | Should -Be 'example.tests.ps1' + $failure.error | Should -Be 'Expected 5, but got 3.' + $failure.file | Should -Be 'C:\tests\example.tests.ps1' + $failure.line | Should -Be 42 + } + + Context 'All tests pass' { + BeforeAll { + $passOnlyBlock = [PSCustomObject]@{ + Tests = @($mockPassedTest) + Blocks = @() + } + $passOnlyContainer = [PSCustomObject]@{ + Name = 'passing.tests.ps1' + Blocks = @($passOnlyBlock) + } + $script:passingResult = [PSCustomObject]@{ + TotalCount = 1 + PassedCount = 1 + FailedCount = 0 + SkippedCount = 0 + Duration = [timespan]::FromSeconds(0.5) + Containers = @($passOnlyContainer) + } + } + + It 'Returns empty failures array when all tests pass' { + $output = ConvertTo-PSBuildLLMOutput -TestResult $script:passingResult + $parsed = $output | ConvertFrom-Json + $parsed.failures | Should -HaveCount 0 + $parsed.summary.failed | Should -Be 0 + } + } +} diff --git a/tests/Manifest.tests.ps1 b/tests/Manifest.tests.ps1 index b659654..e70bbf6 100644 --- a/tests/Manifest.tests.ps1 +++ b/tests/Manifest.tests.ps1 @@ -10,8 +10,8 @@ BeforeAll { $changelogPath = Join-Path -Path $env:BHProjectPath -Child 'CHANGELOG.md' $changelogVersion = Get-Content $changelogPath | ForEach-Object { - if ($_ -match "^##\s\[(?(\d+\.){1,3}\d+)\]") { - $changelogVersion = $matches.Version + if ($_ -match "^##\s\[(?(\d+\.){1,3}\d+(-[\w.]+)?)\]") { + $changelogVersion = $matches.Version -replace '-.*$', '' break } } diff --git a/tests/PesterConfig.tests.ps1 b/tests/PesterConfig.tests.ps1 new file mode 100644 index 0000000..cffa2f1 --- /dev/null +++ b/tests/PesterConfig.tests.ps1 @@ -0,0 +1,74 @@ +BeforeAll { + Set-BuildEnvironment -Force + $manifest = Import-PowerShellDataFile -Path $env:BHPSModuleManifest + $outputDir = Join-Path -Path $ENV:BHProjectPath -ChildPath 'Output' + $outputModDir = Join-Path -Path $outputDir -ChildPath $env:BHProjectName + $outputModVerDir = Join-Path -Path $outputModDir -ChildPath $manifest.ModuleVersion + Import-Module (Join-Path $outputModVerDir "$($env:BHProjectName).psd1") -Force +} + +Describe 'Test-PSBuildPester PesterConfiguration support' { + + Context 'OutputMode parameter' { + It 'Accepts Detailed as default' { + $cmd = Get-Command Test-PSBuildPester + $param = $cmd.Parameters['OutputMode'] + $param | Should -Not -BeNullOrEmpty + $param.Attributes.Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues | Should -Contain 'Detailed' + } + + It 'Accepts Minimal value' { + $cmd = Get-Command Test-PSBuildPester + $param = $cmd.Parameters['OutputMode'] + $param.Attributes.Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues | Should -Contain 'Minimal' + } + + It 'Accepts LLM value' { + $cmd = Get-Command Test-PSBuildPester + $param = $cmd.Parameters['OutputMode'] + $param.Attributes.Where({ $_ -is [System.Management.Automation.ValidateSetAttribute] }).ValidValues | Should -Contain 'LLM' + } + } + + Context 'PesterConfigurationPath parameter' { + It 'Has a PesterConfigurationPath parameter' { + $cmd = Get-Command Test-PSBuildPester + $cmd.Parameters.Keys | Should -Contain 'PesterConfigurationPath' + } + + It 'PesterConfigurationPath is a string' { + $cmd = Get-Command Test-PSBuildPester + $cmd.Parameters['PesterConfigurationPath'].ParameterType | Should -Be ([string]) + } + } + + Context 'Configuration parameter' { + It 'Has a Configuration parameter' { + $cmd = Get-Command Test-PSBuildPester + $cmd.Parameters.Keys | Should -Contain 'Configuration' + } + + It 'Configuration belongs to the Configuration parameter set' { + $cmd = Get-Command Test-PSBuildPester + $paramSets = $cmd.Parameters['Configuration'].ParameterSets.Keys + $paramSets | Should -Contain 'Configuration' + } + } + + Context 'Parameter sets' { + It 'Has an Individual parameter set' { + $cmd = Get-Command Test-PSBuildPester + $cmd.ParameterSets.Name | Should -Contain 'Individual' + } + + It 'Has a Configuration parameter set' { + $cmd = Get-Command Test-PSBuildPester + $cmd.ParameterSets.Name | Should -Contain 'Configuration' + } + + It 'Default parameter set is Individual' { + $cmd = Get-Command Test-PSBuildPester + $cmd.DefaultParameterSet | Should -Be 'Individual' + } + } +} diff --git a/tests/TestModule/psakeFile.ps1 b/tests/TestModule/psakeFile.ps1 index 68e4dbb..ce3473a 100644 --- a/tests/TestModule/psakeFile.ps1 +++ b/tests/TestModule/psakeFile.ps1 @@ -32,4 +32,4 @@ properties { task default -depends Build -task Build -FromModule PowerShellBuild -minimumVersion 0.5.0 +task Build -FromModule PowerShellBuild -minimumVersion 1.0.0