diff --git a/.github/workflows/Process-PSModule.yml b/.github/workflows/Process-PSModule.yml index 932862f..44cb9c9 100644 --- a/.github/workflows/Process-PSModule.yml +++ b/.github/workflows/Process-PSModule.yml @@ -18,19 +18,39 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -permissions: - contents: write - pull-requests: write - statuses: write - pages: write - id-token: write +permissions: {} jobs: - Process-PSModule: + Process-PSModule-PR: + if: github.event_name == 'pull_request' && github.event.action != 'closed' + permissions: + contents: read + pull-requests: write + statuses: write uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@fb1bdb8fefd243292f779d2a856a38db6fe6daf4 # v6.1.13 with: ImportantFilePatterns: | ^src/ ^tests/ ^README\.md$ - secrets: inherit + secrets: + APIKey: ${{ secrets.APIKey }} + TestData: ${{ secrets.TestData }} + + Process-PSModule-Release: + if: github.event_name != 'pull_request' || github.event.action == 'closed' + permissions: + contents: write + pull-requests: write + statuses: write + pages: write + id-token: write + uses: PSModule/Process-PSModule/.github/workflows/workflow.yml@fb1bdb8fefd243292f779d2a856a38db6fe6daf4 # v6.1.13 + with: + ImportantFilePatterns: | + ^src/ + ^tests/ + ^README\.md$ + secrets: + APIKey: ${{ secrets.APIKey }} + TestData: ${{ secrets.TestData }} diff --git a/src/functions/public/ConvertFrom-Yaml.ps1 b/src/functions/public/ConvertFrom-Yaml.ps1 index 73af471..445707e 100644 --- a/src/functions/public/ConvertFrom-Yaml.ps1 +++ b/src/functions/public/ConvertFrom-Yaml.ps1 @@ -61,6 +61,9 @@ function ConvertFrom-Yaml { .OUTPUTS System.Object + + .LINK + https://github.com/PSModule/Yaml#parse-yaml #> [CmdletBinding()] [OutputType([object])] diff --git a/src/functions/public/ConvertTo-Yaml.ps1 b/src/functions/public/ConvertTo-Yaml.ps1 index 4f382cb..5ca0fc0 100644 --- a/src/functions/public/ConvertTo-Yaml.ps1 +++ b/src/functions/public/ConvertTo-Yaml.ps1 @@ -49,6 +49,9 @@ function ConvertTo-Yaml { .OUTPUTS System.String + + .LINK + https://github.com/PSModule/Yaml#serialize-powershell-values #> [CmdletBinding()] [OutputType([string])] diff --git a/src/functions/public/Export-Yaml.ps1 b/src/functions/public/Export-Yaml.ps1 index abb5f6a..f5d245e 100644 --- a/src/functions/public/Export-Yaml.ps1 +++ b/src/functions/public/Export-Yaml.ps1 @@ -88,7 +88,7 @@ function Export-Yaml { Only FileSystem provider destinations are supported. .LINK - https://psmodule.io/Yaml/Functions/ConvertTo-Yaml/ + https://github.com/PSModule/Yaml#export-yaml-files #> [OutputType([System.IO.FileInfo])] [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] @@ -1167,3 +1167,4 @@ function Export-Yaml { } } } + diff --git a/src/functions/public/Format-Yaml.ps1 b/src/functions/public/Format-Yaml.ps1 index 21e4783..7626438 100644 --- a/src/functions/public/Format-Yaml.ps1 +++ b/src/functions/public/Format-Yaml.ps1 @@ -62,6 +62,9 @@ function Format-Yaml { .OUTPUTS System.String + + .LINK + https://github.com/PSModule/Yaml#format-yaml-streams #> [CmdletBinding()] [OutputType([string])] diff --git a/src/functions/public/Import-Yaml.ps1 b/src/functions/public/Import-Yaml.ps1 index 40168d8..a283f8c 100644 --- a/src/functions/public/Import-Yaml.ps1 +++ b/src/functions/public/Import-Yaml.ps1 @@ -80,7 +80,7 @@ function Import-Yaml { Only FileSystem provider paths are supported. .LINK - https://psmodule.io/Yaml/Functions/ConvertFrom-Yaml/ + https://github.com/PSModule/Yaml#import-yaml-files #> [OutputType([object])] [CmdletBinding(DefaultParameterSetName = 'Path')] @@ -645,3 +645,4 @@ function Import-Yaml { } } } + diff --git a/src/functions/public/Merge-Yaml.ps1 b/src/functions/public/Merge-Yaml.ps1 index a2dd73d..3d4620c 100644 --- a/src/functions/public/Merge-Yaml.ps1 +++ b/src/functions/public/Merge-Yaml.ps1 @@ -96,7 +96,7 @@ function Merge-Yaml { YAML 1.1 merge keys are ordinary mapping data and are never expanded. .LINK - https://github.com/PSModule/Yaml#format-yaml-streams + https://github.com/PSModule/Yaml#merge-yaml-streams #> [OutputType([string])] [CmdletBinding()] @@ -277,3 +277,4 @@ function Merge-Yaml { } } } + diff --git a/src/functions/public/Test-Yaml.ps1 b/src/functions/public/Test-Yaml.ps1 index 64ffc8d..ba0a0f1 100644 --- a/src/functions/public/Test-Yaml.ps1 +++ b/src/functions/public/Test-Yaml.ps1 @@ -46,6 +46,9 @@ function Test-Yaml { .OUTPUTS System.Boolean + + .LINK + https://github.com/PSModule/Yaml#validate-yaml #> [CmdletBinding()] [OutputType([bool])] diff --git a/tests/Packaging.Tests.ps1 b/tests/Packaging.Tests.ps1 index ac5e530..538c9ca 100644 --- a/tests/Packaging.Tests.ps1 +++ b/tests/Packaging.Tests.ps1 @@ -37,24 +37,9 @@ Describe 'Dependency-free package source' { Test-Path (Join-Path $repositoryRoot 'src\THIRD-PARTY-NOTICES.txt') | Should -BeFalse } - It 'keeps RequiredAssemblies out of the source manifest' { - $manifest = Import-PowerShellDataFile -Path ( - Join-Path $repositoryRoot 'src\manifest.psd1' - ) - - $manifest.PowerShellVersion | Should -Be '7.6' - @($manifest.CompatiblePSEditions) | Should -Be @('Core') - @($manifest.CompatiblePSEditions) | Should -Not -Contain 'Desktop' - $manifest.ContainsKey('DotNetFrameworkVersion') | Should -BeFalse - $manifest.ContainsKey('RequiredAssemblies') | Should -BeFalse - } - - It 'declares the generated artifact runtime once in the module header' { - $requirementPath = Join-Path $repositoryRoot 'src\header.ps1' - $source = Get-Content -LiteralPath $requirementPath -Raw - - $source | Should -Match '(?m)^#Requires -Version 7\.6\r?$' - $source | Should -Match '(?m)^#Requires -PSEdition Core\r?$' + It 'keeps runtime requirements out of removed source manifest and header files' { + Test-Path (Join-Path $repositoryRoot 'src\manifest.psd1') | Should -BeFalse + Test-Path (Join-Path $repositoryRoot 'src\header.ps1') | Should -BeFalse } It 'contains no external parser references or custom assembly loader' { diff --git a/tests/tools/Invoke-YamlPerfReview.ps1 b/tests/tools/Invoke-YamlPerfReview.ps1 new file mode 100644 index 0000000..66f560a --- /dev/null +++ b/tests/tools/Invoke-YamlPerfReview.ps1 @@ -0,0 +1,200 @@ +[CmdletBinding()] +param ( + [Parameter()] + [ValidateSet('Baseline', 'Regression')] + [string] $Mode = 'Baseline', + + [Parameter()] + [ValidateRange(1, 200)] + [int] $Runs = 12, + + [Parameter()] + [ValidateRange(0, 50)] + [int] $Preheat = 2, + + [Parameter()] + [string] $OutputPath = (Join-Path $PSScriptRoot "..\fixtures\perf\$Mode.json") +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +if ($PSVersionTable.PSVersion -lt [version] '7.6' -or $PSVersionTable.PSEdition -cne 'Core') { + throw 'Invoke-YamlPerfReview.ps1 requires PowerShell 7.6+ (Core).' +} + +Import-Module -Name Profiler -ErrorAction Stop +. (Join-Path $PSScriptRoot '..\TestBootstrap.ps1') + +function Get-Percentile { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [double[]] $Values, + + [Parameter(Mandatory)] + [ValidateRange(0.0, 1.0)] + [double] $Percentile + ) + + if ($Values.Count -eq 0) { + return 0.0 + } + + $sorted = @($Values | Sort-Object) + $index = [Math]::Ceiling($Percentile * $sorted.Count) - 1 + $index = [Math]::Max(0, [Math]::Min($index, $sorted.Count - 1)) + return [double] $sorted[$index] +} + +function Measure-Scenario { + [CmdletBinding()] + param ( + [Parameter(Mandatory)] + [string] $Name, + + [Parameter(Mandatory)] + [scriptblock] $Script, + + [Parameter(Mandatory)] + [int] $RunCount, + + [Parameter(Mandatory)] + [int] $WarmupCount + ) + + for ($i = 0; $i -lt $WarmupCount; $i++) { + & $Script > $null + } + + $elapsedMs = [System.Collections.Generic.List[double]]::new() + for ($i = 0; $i -lt $RunCount; $i++) { + $stopwatch = [System.Diagnostics.Stopwatch]::StartNew() + & $Script > $null + $stopwatch.Stop() + $elapsedMs.Add($stopwatch.Elapsed.TotalMilliseconds) + } + + $trace = Trace-Script -ScriptBlock $Script -Preheat 1 -DisableWarning + $topFunctions = @( + $trace.Top50FunctionSelfDuration | + Select-Object -First 5 Function, Module, Line, HitCount, @{ + Name = 'SelfDurationMs' + Expression = { [Math]::Round($_.SelfDuration.TotalMilliseconds, 3) } + }, @{ + Name = 'DurationMs' + Expression = { [Math]::Round($_.Duration.TotalMilliseconds, 3) } + } + ) + + $values = [double[]] $elapsedMs.ToArray() + [pscustomobject]@{ + Name = $Name + Runs = $RunCount + Preheat = $WarmupCount + AverageMs = [Math]::Round(($values | Measure-Object -Average).Average, 3) + MedianMs = [Math]::Round((Get-Percentile -Values $values -Percentile 0.50), 3) + P95Ms = [Math]::Round((Get-Percentile -Values $values -Percentile 0.95), 3) + MinMs = [Math]::Round(($values | Measure-Object -Minimum).Minimum, 3) + MaxMs = [Math]::Round(($values | Measure-Object -Maximum).Maximum, 3) + SamplesMs = $values + TraceTopSelf = $topFunctions + } +} + +Write-Host "Preparing performance fixtures for mode: $Mode" + +$smallYaml = @' +name: small +enabled: true +ports: [80, 443] +'@ + +$mediumYamlBuilder = [System.Text.StringBuilder]::new() +[void] $mediumYamlBuilder.AppendLine('items:') +for ($index = 0; $index -lt 1200; $index++) { + [void] $mediumYamlBuilder.AppendLine(" - id: $index") + [void] $mediumYamlBuilder.AppendLine(" name: item-$index") + [void] $mediumYamlBuilder.AppendLine(" enabled: true") + [void] $mediumYamlBuilder.AppendLine(" value: $($index * 3)") +} +$mediumYaml = $mediumYamlBuilder.ToString().TrimEnd("`r", "`n") + +$overlayYamlBuilder = [System.Text.StringBuilder]::new() +[void] $overlayYamlBuilder.AppendLine('items:') +for ($index = 0; $index -lt 1200; $index++) { + [void] $overlayYamlBuilder.AppendLine(" - id: $index") + [void] $overlayYamlBuilder.AppendLine(" name: item-$index-override") + [void] $overlayYamlBuilder.AppendLine(" enabled: false") +} +$overlayYaml = $overlayYamlBuilder.ToString().TrimEnd("`r", "`n") + +$mediumObject = ConvertFrom-Yaml -Yaml $mediumYaml -NoEnumerate +$tempPath = Join-Path ([System.IO.Path]::GetTempPath()) 'yaml-perf-review.yaml' + +$scenarios = @( + @{ + Name = 'ConvertFrom-Yaml/small' + Script = { ConvertFrom-Yaml -Yaml $smallYaml | Out-Null } + }, + @{ + Name = 'ConvertFrom-Yaml/medium' + Script = { ConvertFrom-Yaml -Yaml $mediumYaml -NoEnumerate | Out-Null } + }, + @{ + Name = 'ConvertTo-Yaml/medium' + Script = { ConvertTo-Yaml -InputObject $mediumObject | Out-Null } + }, + @{ + Name = 'Format-Yaml/medium' + Script = { Format-Yaml -InputObject $mediumYaml -Indent 2 | Out-Null } + }, + @{ + Name = 'Merge-Yaml/medium' + Script = { Merge-Yaml -InputObject @($mediumYaml, $overlayYaml) -SequenceAction Replace | Out-Null } + }, + @{ + Name = 'Remove-YamlEntry/medium' + Script = { Remove-YamlEntry -InputObject $mediumYaml -Path '/items/12/name' -IgnoreMissing | Out-Null } + }, + @{ + Name = 'Test-Yaml/medium' + Script = { Test-Yaml -Yaml $mediumYaml | Out-Null } + }, + @{ + Name = 'Export-Import-Yaml/medium' + Script = { + Export-Yaml -InputObject $mediumObject -Path $tempPath -Force | Out-Null + Import-Yaml -LiteralPath $tempPath -NoEnumerate | Out-Null + } + } +) + +$results = [System.Collections.Generic.List[object]]::new() +foreach ($scenario in $scenarios) { + Write-Host "Measuring $($scenario.Name)" + $results.Add((Measure-Scenario -Name $scenario.Name -Script $scenario.Script -RunCount $Runs -WarmupCount $Preheat)) +} + +if (Test-Path -LiteralPath $tempPath) { + Remove-Item -LiteralPath $tempPath -Force +} + +$outputDirectory = Split-Path -Parent $OutputPath +if (-not [string]::IsNullOrWhiteSpace($outputDirectory) -and -not (Test-Path -LiteralPath $outputDirectory)) { + $null = New-Item -Path $outputDirectory -ItemType Directory -Force +} + +$report = [pscustomobject]@{ + Mode = $Mode + TimestampUtc = [DateTime]::UtcNow.ToString('o') + PowerShell = $PSVersionTable.PSVersion.ToString() + Edition = $PSVersionTable.PSEdition + Runs = $Runs + Preheat = $Preheat + RegressionFail = 'Greater than 5 percent slowdown on critical-path scenarios.' + Scenarios = $results +} + +$report | ConvertTo-Json -Depth 8 | Set-Content -Path $OutputPath -Encoding utf8NoBOM +Write-Host "Performance report written to: $OutputPath"