Skip to content

Commit 5f4652d

Browse files
Merge remote-tracking branch 'origin/main' into github-app-planning
2 parents 61be878 + 15c2814 commit 5f4652d

139 files changed

Lines changed: 6865 additions & 258 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/actions/Build-PSModule/src/helpers/Build/Build-PSModuleManifest.ps1

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@
260260
Write-Host "Processing required module [$requiredModuleName]"
261261
$requiredVersion = $group.Group.RequiredVersion | ForEach-Object { [Version]$_ } | Sort-Object -Unique
262262
$minimumVersion = $group.Group.Version | ForEach-Object { [Version]$_ } | Sort-Object -Unique | Select-Object -Last 1
263-
$maximumVersion = $group.Group.MaximumVersion | ForEach-Object { [Version]$_ } | Sort-Object -Unique | Select-Object -First 1
263+
$maximumVersion = $group.Group.MaximumVersion | Sort-Object -Unique | Select-Object -First 1
264+
$maximumVersionBound = if ($maximumVersion -and ($maximumVersion -notmatch '[*]')) { [Version]$maximumVersion } else { $null }
264265
Write-Host "RequiredVersion: [$($requiredVersion -join ', ')]"
265266
Write-Host "ModuleVersion: [$minimumVersion]"
266267
Write-Host "MaximumVersion: [$maximumVersion]"
@@ -274,17 +275,17 @@
274275
}
275276

276277
if (-not $maximumVersion) {
277-
$maximumVersion = [Version]'9999.9999.9999'
278+
$maximumVersionBound = [Version]'9999.9999.9999'
278279
}
279280

280281
if ($requiredVersion -and ($minimumVersion -gt $requiredVersion)) {
281282
throw 'ModuleVersion is higher than RequiredVersion.'
282283
}
283284

284-
if ($minimumVersion -gt $maximumVersion) {
285+
if ($maximumVersionBound -and ($minimumVersion -gt $maximumVersionBound)) {
285286
throw 'ModuleVersion is higher than MaximumVersion.'
286287
}
287-
if ($requiredVersion -and ($requiredVersion -gt $maximumVersion)) {
288+
if ($requiredVersion -and $maximumVersionBound -and ($requiredVersion -gt $maximumVersionBound)) {
288289
throw 'RequiredVersion is higher than MaximumVersion.'
289290
}
290291

@@ -294,15 +295,15 @@
294295
ModuleName = $requiredModuleName
295296
RequiredVersion = $requiredVersion
296297
}
297-
} elseif (($minimumVersion -ne [Version]'0.0.0') -or ($maximumVersion -ne [Version]'9999.9999.9999')) {
298+
} elseif (($minimumVersion -ne [Version]'0.0.0') -or ($maximumVersionBound -ne [Version]'9999.9999.9999')) {
298299
Write-Host '[RequiredModules] - ModuleVersion/MaximumVersion'
299300
$uniqueModule = @{
300301
ModuleName = $requiredModuleName
301302
}
302303
if ($minimumVersion -ne [Version]'0.0.0') {
303304
$uniqueModule['ModuleVersion'] = $minimumVersion
304305
}
305-
if ($maximumVersion -ne [Version]'9999.9999.9999') {
306+
if ($maximumVersionBound -ne [Version]'9999.9999.9999') {
306307
$uniqueModule['MaximumVersion'] = $maximumVersion
307308
}
308309
} else {

.github/actions/Cleanup-PSModulePrereleases/action.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ inputs:
77
description: Control whether to automatically delete prerelease tags.
88
required: false
99
default: 'true'
10+
ReleaseTag:
11+
description: GitHub release tag to retain during cleanup.
12+
required: false
13+
default: ''
1014
WhatIf:
1115
description: If specified, the action will only log the changes it would make.
1216
required: false
@@ -29,5 +33,5 @@ runs:
2933
env:
3034
GH_TOKEN: ${{ env.GH_TOKEN }}
3135
PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_WhatIf: ${{ inputs.WhatIf }}
32-
PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag }}
36+
PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ inputs.ReleaseTag }}
3337
run: ${{ github.action_path }}/src/cleanup.ps1

.github/actions/Get-PesterCodeCoverage/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ This GitHub Action is a part of the [PSModule framework](https://github.com/PSMo
99
## Features
1010

1111
- Combines multiple code coverage reports from parallel test runs
12-
- Generates markdown/HTML tables showing missed & executed commands
12+
- Generates Markdown/HTML tables showing missed & executed commands
1313
- Displays analyzed files and coverage statistics
14+
- Generates a generic missed-path report (Markdown + JSON)
1415
- Configurable step summary sections
1516
- Threshold enforcement for minimum code coverage
1617

@@ -40,6 +41,10 @@ This GitHub Action is a part of the [PSModule framework](https://github.com/PSMo
4041
4142
## Outputs
4243
44+
| Name | Description |
45+
| ---- | ----------- |
46+
| `MissedPathReportPath` | Folder path containing `CodeCoverage-MissedPaths.md` and `CodeCoverage-MissedPaths.json` |
47+
4348
### GitHub Step Summary
4449

4550
The action generates a detailed summary visible in the GitHub Actions UI:
@@ -53,6 +58,7 @@ The action generates a detailed summary visible in the GitHub Actions UI:
5358
- **Missed Commands**: HTML table with code snippets
5459
- **Executed Commands**: HTML table with code snippets
5560
- **Analyzed Files**: List of covered files
61+
- **Missed Paths**: Aggregated missed-command counts grouped by file path
5662

5763
Example summary:
5864

.github/actions/Get-PesterCodeCoverage/action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ inputs:
3535
description: The target for code coverage.
3636
required: false
3737

38+
outputs:
39+
MissedPathReportPath:
40+
description: Path to the generated missed-path report folder.
41+
value: ${{ steps.Get-PesterCodeCoverage.outputs.MissedPathReportPath }}
42+
3843
runs:
3944
using: composite
4045
steps:

.github/actions/Get-PesterCodeCoverage/src/main.ps1

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,73 @@ $success = $coveragePercent -ge $coveragePercentTarget
158158
$statusIcon = $success ? '' : ''
159159
$stats | Format-Table -AutoSize | Out-String
160160

161+
$missedPathGroups = $codeCoverage.CommandsMissed |
162+
Where-Object { -not [string]::IsNullOrWhiteSpace($_.File) } |
163+
Group-Object -Property File |
164+
Sort-Object -Property @(
165+
@{ Expression = 'Count'; Descending = $true },
166+
@{ Expression = 'Name'; Descending = $false }
167+
)
168+
169+
$missedPaths = foreach ($group in $missedPathGroups) {
170+
$lines = $group.Group |
171+
Where-Object { $_.Line -is [ValueType] } |
172+
ForEach-Object { [int]$_.Line } |
173+
Sort-Object -Unique
174+
$missedLines = if ($lines.Count -eq 0) { '' } else { $lines -join ', ' }
175+
[pscustomobject]@{
176+
Path = $group.Name
177+
MissedCommands = [int]$group.Count
178+
MissedLines = $missedLines
179+
}
180+
}
181+
182+
$missedPathReportPath = 'CodeCoverage-MissedPaths'
183+
$null = New-Item -Path $missedPathReportPath -ItemType Directory -Force
184+
185+
$missedPathReport = [ordered]@{
186+
GeneratedAtUtc = [DateTime]::UtcNow.ToString('o')
187+
CoveragePercent = [double]$coveragePercent
188+
CoverageTarget = [double]$coveragePercentTarget
189+
MissedPaths = @($missedPaths)
190+
}
191+
192+
$missedPathReportJsonPath = Join-Path -Path $missedPathReportPath -ChildPath 'CodeCoverage-MissedPaths.json'
193+
$missedPathReportMarkdownPath = Join-Path -Path $missedPathReportPath -ChildPath 'CodeCoverage-MissedPaths.md'
194+
$missedPathReport | ConvertTo-Json -Depth 10 | Set-Content -Path $missedPathReportJsonPath -Encoding utf8NoBOM
195+
196+
$missedPathMarkdown = [System.Collections.Generic.List[string]]::new()
197+
$null = $missedPathMarkdown.Add('# Code Coverage Missed Paths')
198+
$null = $missedPathMarkdown.Add('')
199+
$null = $missedPathMarkdown.Add("| Coverage | Target | Missed Paths | Missed Commands |")
200+
$null = $missedPathMarkdown.Add("| --- | --- | --- | --- |")
201+
$coverageSummaryRow = '| {0}% | {1}% | {2} | {3} |' -f (
202+
[Math]::Round($coveragePercent, 2)
203+
), (
204+
[Math]::Round($coveragePercentTarget, 2)
205+
), $missedPaths.Count, $codeCoverage.CommandsMissedCount
206+
$null = $missedPathMarkdown.Add($coverageSummaryRow)
207+
$null = $missedPathMarkdown.Add('')
208+
$null = $missedPathMarkdown.Add('## Paths')
209+
$null = $missedPathMarkdown.Add('| Path | Missed Commands | Missed Lines |')
210+
$null = $missedPathMarkdown.Add('| --- | ---: | --- |')
211+
if ($missedPaths.Count -eq 0) {
212+
$null = $missedPathMarkdown.Add('| _None_ | 0 | |')
213+
} else {
214+
foreach ($pathEntry in $missedPaths) {
215+
$safePath = ([string]$pathEntry.Path).Replace('|', '\|')
216+
$safeLines = ([string]$pathEntry.MissedLines).Replace('|', '\|')
217+
$null = $missedPathMarkdown.Add("| $safePath | $($pathEntry.MissedCommands) | $safeLines |")
218+
}
219+
}
220+
221+
$missedPathMarkdown | Set-Content -Path $missedPathReportMarkdownPath -Encoding utf8NoBOM
222+
Write-Output 'Missed path report generated:'
223+
$missedPaths | Select-Object -First 20 | Format-Table -Property Path, MissedCommands, MissedLines -AutoSize | Out-String
224+
if ($env:GITHUB_OUTPUT) {
225+
"MissedPathReportPath=$missedPathReportPath" >> $env:GITHUB_OUTPUT
226+
}
227+
161228
# Build HTML table for 'missed' commands
162229
$tableheader = @'
163230
<table>
@@ -300,6 +367,18 @@ LogGroup 'Step Summary - Set step summary' {
300367
}
301368
}
302369
}
370+
371+
Details "Missed paths [$($missedPaths.Count)]" {
372+
if ($missedPaths.Count -eq 0) {
373+
Paragraph {
374+
Write-Output 'No missed paths were detected.'
375+
}
376+
} else {
377+
Table {
378+
$missedPaths | Select-Object -First 100
379+
}
380+
}
381+
}
303382
}
304383
}
305384

.github/actions/Install-PSModule/src/PSModule/PSModule.psm1

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,18 +87,40 @@ function Convert-VersionSpec {
8787
[string] $RequiredVersion
8888
)
8989

90+
$maximumVersionIsWildcard = $MaximumVersion -and ($MaximumVersion -match '[*]')
91+
if ($maximumVersionIsWildcard) {
92+
$versionParts = @($MaximumVersion -split '\.')
93+
$wildcardIndex = [Array]::IndexOf($versionParts, '*')
94+
if ($wildcardIndex -lt 0) {
95+
throw "MaximumVersion '$MaximumVersion' contains an unsupported wildcard pattern."
96+
}
97+
98+
if ($wildcardIndex -eq 0) {
99+
throw "MaximumVersion '$MaximumVersion' contains an unsupported wildcard pattern."
100+
}
101+
102+
$upperBoundParts = @(0, 0, 0)
103+
for ($i = 0; $i -lt $wildcardIndex; $i++) {
104+
$upperBoundParts[$i] = [int]$versionParts[$i]
105+
}
106+
$upperBoundParts[$wildcardIndex - 1]++
107+
$MaximumVersion = $upperBoundParts -join '.'
108+
}
109+
90110
if ($RequiredVersion) {
91111
# Use exact match in bracket notation.
92112
return "[$RequiredVersion]"
93113
} elseif ($MinimumVersion -and $MaximumVersion) {
94-
# Both bounds provided; both are inclusive.
95-
return "[$MinimumVersion,$MaximumVersion]"
114+
# Wildcard maximum versions are translated to an exclusive upper bound.
115+
$upperBound = $maximumVersionIsWildcard ? ')' : ']'
116+
return "[$MinimumVersion,$MaximumVersion$upperBound"
96117
} elseif ($MinimumVersion) {
97118
# Only a minimum is provided. Use a minimum-inclusive range.
98119
return "[$MinimumVersion, ]"
99120
} elseif ($MaximumVersion) {
100-
# Only a maximum is provided; lower bound open.
101-
return "(, $MaximumVersion]"
121+
# Only a maximum is provided; lower bound open. Wildcards are exclusive upper bounds.
122+
$upperBound = $maximumVersionIsWildcard ? ')' : ']'
123+
return "(, $MaximumVersion$upperBound"
102124
} else {
103125
return $null
104126
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Publish-PSModule
2+
3+
Publishes a pre-versioned PowerShell module artifact to the PowerShell Gallery. GitHub Release creation is intentionally handled by the separate [Release-PSModule](../Release-PSModule/README.md) action.
4+
5+
## Inputs
6+
7+
| Name | Description | Required | Default |
8+
| --- | --- | --- | --- |
9+
| `Name` | Name of the module to publish. | No | Repository name |
10+
| `ModulePath` | Path containing the built `<Name>/` module directory. | No | `outputs/module` |
11+
| `ArtifactName` | Name of the module artifact to download. | No | `module` |
12+
| `APIKey` | PowerShell Gallery API key. | Yes | N/A |
13+
| `WhatIf` | Logs publishing operations without publishing the module. | No | `false` |
14+
| `WorkingDirectory` | Directory where the publishing script runs. | No | `.` |
15+
16+
## Outputs
17+
18+
This action does not provide outputs.
19+
20+
## Usage
21+
22+
```yaml
23+
- name: Publish module
24+
uses: ./.github/actions/Publish-PSModule
25+
with:
26+
Name: ExampleModule
27+
ModulePath: outputs/module
28+
ArtifactName: module
29+
APIKey: ${{ secrets.APIKEY }}
30+
```
31+
32+
Use [Release-PSModule](../Release-PSModule/README.md) in a separate workflow step to create the GitHub release from the same artifact.

.github/actions/Publish-PSModule/action.yml

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: Publish-PSModule
2-
description: Publish a pre-versioned PowerShell module artifact to the PowerShell Gallery and GitHub Releases.
2+
description: Publish a pre-versioned PowerShell module artifact to the PowerShell Gallery.
33
author: PSModule
44

55
inputs:
@@ -14,25 +14,13 @@ inputs:
1414
description: PowerShell Gallery API Key.
1515
required: true
1616
WhatIf:
17-
description: If specified, the action will only log the changes it would make, but will not actually create or delete any releases or tags.
17+
description: If specified, the action will only log the changes it would make, but will not publish the module.
1818
required: false
1919
default: 'false'
2020
WorkingDirectory:
2121
description: The working directory where the script will run from.
2222
required: false
2323
default: '.'
24-
UsePRTitleAsReleaseName:
25-
description: When enabled, uses the pull request title as the name for the GitHub release. If not set, the version string is used.
26-
required: false
27-
default: 'false'
28-
UsePRBodyAsReleaseNotes:
29-
description: When enabled, uses the pull request body as the release notes for the GitHub release. If not set, the release notes are auto-generated.
30-
required: false
31-
default: 'true'
32-
UsePRTitleAsNotesHeading:
33-
description: When enabled along with UsePRBodyAsReleaseNotes, the release notes will begin with the pull request title as a H1 heading followed by the pull request body. The title will reference the pull request number.
34-
required: false
35-
default: 'true'
3624
ArtifactName:
3725
description: Name of the uploaded artifact to download. Must match the name used in the upstream upload-artifact step.
3826
required: false
@@ -63,7 +51,4 @@ runs:
6351
PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath: ${{ inputs.ModulePath }}
6452
PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey: ${{ inputs.APIKey }}
6553
PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }}
66-
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes: ${{ inputs.UsePRBodyAsReleaseNotes }}
67-
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName: ${{ inputs.UsePRTitleAsReleaseName }}
68-
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsNotesHeading: ${{ inputs.UsePRTitleAsNotesHeading }}
6954
run: ${{ github.action_path }}/src/publish.ps1
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
function Get-ModuleVersionString {
2+
<#
3+
.SYNOPSIS
4+
Builds the SemVer version string that identifies the module itself.
5+
6+
.DESCRIPTION
7+
Composes the module version and, when there is one, the prerelease label. This is the string the
8+
PowerShell Gallery and the module manifest understand: `Major.Minor.Patch` optionally followed by
9+
`-<prerelease>`. It never carries the repository's version prefix, because neither the manifest's
10+
`ModuleVersion` nor a Gallery package version accepts one.
11+
12+
.OUTPUTS
13+
String with the module version.
14+
15+
.EXAMPLE
16+
Get-ModuleVersionString -ModuleVersion '1.1.10'
17+
18+
Returns '1.1.10'.
19+
20+
.EXAMPLE
21+
Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
22+
23+
Returns '1.1.10-mybranch001'.
24+
#>
25+
[CmdletBinding()]
26+
[OutputType([string])]
27+
param(
28+
# The module version from the compiled manifest, in Major.Minor.Patch format.
29+
[Parameter(Mandatory)]
30+
[ValidateNotNullOrEmpty()]
31+
[string] $ModuleVersion,
32+
33+
# The prerelease label from the compiled manifest. Empty for a stable release.
34+
[Parameter()]
35+
[AllowEmptyString()]
36+
[AllowNull()]
37+
[string] $Prerelease
38+
)
39+
40+
if ([string]::IsNullOrWhiteSpace($Prerelease)) {
41+
return $ModuleVersion
42+
}
43+
44+
"$ModuleVersion-$($Prerelease.Trim())"
45+
}

0 commit comments

Comments
 (0)