Skip to content

Commit 688896d

Browse files
🪲 [Fix]: Version resolution no longer fails on repositories without releases (#432)
A module repository that has not published its first release no longer breaks. Previously the `Plan` job failed on any repository with zero GitHub releases, which meant every brand-new module created from the template was blocked on its very first pull request — a chicken-and-egg problem where the framework could not run until a release existed, and a release could not be created until the framework ran. ## Fixed: Version resolution works before the first release exists A repository with no GitHub releases, and a module that has never been published to the PowerShell Gallery, now resolve cleanly to a `0.0.0` baseline. The first labelled pull request produces the expected first version — `0.0.1` for a patch, `0.1.0` for a minor, `1.0.0` for a major — instead of failing the `Plan` job with: ```text Cannot bind argument to parameter 'Releases' because it is null. ``` Because every downstream job depends on `Plan`, that failure skipped the whole run and made the pull request unmergeable. Nothing needs to change in consuming repositories; bumping to the released version is enough. --- <details> <summary>Technical details</summary> - Verified the reported diagnosis before changing anything. Both `Get-LatestGitHubVersion -Releases $null` **and** `Get-LatestGitHubVersion -Releases @()` failed. `[Parameter(Mandatory)] [array]` rejects an empty collection as well as `$null`, so normalising at the call site with `@(Get-GitHubRelease)` alone would have turned the null error into an "empty collection" error. The parameter declarations had to be relaxed too. - `Resolve-PSModuleVersion.Helpers.psm1`: `Releases` on `Get-LatestGitHubVersion`, `Get-NextPrereleaseNumber`, and `Get-NextModuleVersion` is now optional with `[AllowNull()]`, `[AllowEmptyCollection()]`, and an `@()` default, so each function is individually robust rather than depending on a careful caller. - New `ConvertFrom-GitHubReleaseJson` owns the normalisation of the `gh release list` output into a flat array, including the case where the command produced no output at all (the second reproduction in #381, where a repository with five releases still yielded `$null`). `Get-GitHubRelease` delegates to it and `src/main.ps1` normalises with `$releases = @(Get-GitHubRelease)`. - `Get-LatestPublishedVersion` accepts null versions and filters empty candidates before sorting, warning and flooring to `0.0.0` when neither source has a version. `Get-NextModuleVersion` accepts a null `LatestVersion` and floors it to `0.0.0`. - The downstream fallback logic was already correct — only parameter binding, null handling, and the array shape changed. Version resolution was not redesigned. - Tests: `.github/actions/Resolve-PSModuleVersion/tests/Resolve-PSModuleVersion.Helpers.Tests.ps1` adds 36 Pester tests following the [test specification](https://psmodule.io/docs/Modules/Test-Specification/). 21 of them fail against the previous parameter declarations. They cover a null releases list, an empty releases list, releases with none marked `isLatest`, prerelease-only repositories, the release-JSON normalisation, and the full brand-new-module chain for major, minor, and patch decisions. - CI: `.github/workflows/Test-Actions.yml` runs every `.github/actions/*/tests` folder with Pester on pull requests that touch an action, so the repository now has a unit-test surface for its own actions. - Standards and framework alignment: | Changed surface | Standards checked | Framework docs checked | Result | | --- | --- | --- | --- | | `.github/actions/Resolve-PSModuleVersion/src/**` | PSScriptAnalyzer via `.github/linters/.powershell-psscriptanalyzer.psd1` | PSModule function/parameter conventions | Aligned | | `.github/actions/Resolve-PSModuleVersion/tests/**` | PSScriptAnalyzer | [Test Specification](https://psmodule.io/docs/Modules/Test-Specification/) | Aligned | | `.github/workflows/Test-Actions.yml` | Pinned action SHAs, least-privilege permissions | Reusable workflow contract | Aligned — repository-internal workflow, not consumer-facing | </details> <details> <summary>Relevant issues (or links)</summary> - Fixes #381 - #433 — follow-up for end-to-end coverage of a repository with zero releases - Reproduction: PSModule/Lovdata#1 — [failing run 30743884958](https://github.com/PSModule/Lovdata/actions/runs/30743884958/job/91486012154) </details> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent b11b310 commit 688896d

4 files changed

Lines changed: 599 additions & 22 deletions

File tree

‎.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1‎

Lines changed: 90 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -277,16 +277,52 @@ function Resolve-ReleaseDecision {
277277
}
278278
}
279279

280+
function ConvertFrom-GitHubReleaseJson {
281+
<#
282+
.SYNOPSIS
283+
Converts the JSON output of 'gh release list' into a flat array of release objects.
284+
285+
.DESCRIPTION
286+
Normalizes the release listing so a repository with no releases, or a command that
287+
produced no output at all, yields an empty array instead of $null.
288+
289+
.OUTPUTS
290+
Array of release objects. Empty when there are no releases.
291+
292+
.EXAMPLE
293+
$releases = ConvertFrom-GitHubReleaseJson -Json '[{"tagName":"v1.0.0"}]'
294+
#>
295+
[CmdletBinding()]
296+
[OutputType([object[]], [array])]
297+
param(
298+
# The raw JSON returned by 'gh release list'. Empty or null when the command produced no output.
299+
[Parameter()]
300+
[AllowNull()]
301+
[AllowEmptyString()]
302+
[string] $Json
303+
)
304+
305+
if ([string]::IsNullOrWhiteSpace($Json)) {
306+
return @()
307+
}
308+
309+
@($Json | ConvertFrom-Json)
310+
}
311+
280312
function Get-GitHubRelease {
281313
<#
282314
.SYNOPSIS
283315
Retrieves all releases from the current GitHub repository.
284316
317+
.DESCRIPTION
318+
Lists the releases of the current repository. A repository that has no releases yet
319+
produces no output, so callers normalize the result with @() before using it.
320+
285321
.OUTPUTS
286-
Array of release objects.
322+
Array of release objects. Nothing when the repository has no releases.
287323
288324
.EXAMPLE
289-
$releases = Get-GitHubRelease
325+
$releases = @(Get-GitHubRelease)
290326
#>
291327
[CmdletBinding()]
292328
[OutputType([array])]
@@ -298,9 +334,10 @@ function Get-GitHubRelease {
298334
Write-Error 'Failed to list releases for the repo.'
299335
exit $LASTEXITCODE
300336
}
301-
$releases = $releasesJson | ConvertFrom-Json
337+
$releases = ConvertFrom-GitHubReleaseJson -Json $releasesJson
302338

303339
Write-Host '-------------------------------------------------'
340+
Write-Host "Found [$($releases.Count)] releases."
304341
Write-Host ($releases | Select-Object -Property name, isPrerelease, isLatest, publishedAt |
305342
Format-Table | Out-String)
306343
Write-Host '-------------------------------------------------'
@@ -314,6 +351,11 @@ function Get-LatestGitHubVersion {
314351
.SYNOPSIS
315352
Extracts the latest stable version from a GitHub releases list.
316353
354+
.DESCRIPTION
355+
Returns the version of the release marked as latest. A repository that has no releases
356+
yet - or that has releases but none marked as latest - resolves to '0.0.0' so a brand-new
357+
module can still be versioned before its first release exists.
358+
317359
.OUTPUTS
318360
PSSemVer representing the latest GitHub release version.
319361
@@ -325,9 +367,11 @@ function Get-LatestGitHubVersion {
325367
[CmdletBinding()]
326368
[OutputType([object])]
327369
param(
328-
# The GitHub releases array to search.
329-
[Parameter(Mandatory)]
330-
[array] $Releases
370+
# The GitHub releases array to search. Empty or null when the repository has no releases.
371+
[Parameter()]
372+
[AllowNull()]
373+
[AllowEmptyCollection()]
374+
[array] $Releases = @()
331375
)
332376

333377
LogGroup 'Get latest version - GitHub' {
@@ -409,6 +453,11 @@ function Get-LatestPublishedVersion {
409453
.SYNOPSIS
410454
Returns the highest version between GitHub and the PowerShell Gallery.
411455
456+
.DESCRIPTION
457+
Compares the two known published versions and returns the highest one. A missing
458+
(null) version is treated as '0.0.0', so a module that has never been released to
459+
GitHub or published to the PowerShell Gallery resolves to a '0.0.0' baseline.
460+
412461
.OUTPUTS
413462
PSSemVer representing the highest known published version.
414463
@@ -420,19 +469,27 @@ function Get-LatestPublishedVersion {
420469
[CmdletBinding()]
421470
[OutputType([object])]
422471
param(
423-
# The latest version found in GitHub releases.
424-
[Parameter(Mandatory)]
472+
# The latest version found in GitHub releases. Null when the repository has no releases.
473+
[Parameter()]
474+
[AllowNull()]
425475
[object] $GitHubVersion,
426476

427-
# The latest version found in the PowerShell Gallery.
428-
[Parameter(Mandatory)]
477+
# The latest version found in the PowerShell Gallery. Null when the module is unpublished.
478+
[Parameter()]
479+
[AllowNull()]
429480
[object] $PSGalleryVersion
430481
)
431482

432483
LogGroup 'Latest version' {
433-
$latestVersion = New-PSSemVer -Version (
434-
$PSGalleryVersion, $GitHubVersion | Sort-Object -Descending | Select-Object -First 1
435-
)
484+
$candidates = @($PSGalleryVersion, $GitHubVersion) |
485+
Where-Object { $null -ne $_ -and -not [string]::IsNullOrWhiteSpace([string]$_) }
486+
487+
$latestVersion = if ($candidates.Count -gt 0) {
488+
New-PSSemVer -Version ($candidates | Sort-Object -Descending | Select-Object -First 1)
489+
} else {
490+
Write-Warning "No published version found in GitHub or the PowerShell Gallery. Using '0.0.0'."
491+
New-PSSemVer -Version '0.0.0'
492+
}
436493
Write-Host "Latest version: [$($latestVersion.ToString())]"
437494
$latestVersion
438495
}
@@ -472,9 +529,11 @@ function Get-NextPrereleaseNumber {
472529
[ValidateNotNullOrEmpty()]
473530
[string] $PrereleaseName,
474531

475-
# The GitHub releases list.
476-
[Parameter(Mandatory)]
477-
[array] $Releases
532+
# The GitHub releases list. Empty or null when the repository has no releases.
533+
[Parameter()]
534+
[AllowNull()]
535+
[AllowEmptyCollection()]
536+
[array] $Releases = @()
478537
)
479538

480539
$params = @{
@@ -532,8 +591,9 @@ function Get-NextModuleVersion {
532591
[CmdletBinding()]
533592
[OutputType([object])]
534593
param(
535-
# The current latest published version.
536-
[Parameter(Mandatory)]
594+
# The current latest published version. Null resolves to a '0.0.0' baseline.
595+
[Parameter()]
596+
[AllowNull()]
537597
[object] $LatestVersion,
538598

539599
# The release decision object.
@@ -550,12 +610,21 @@ function Get-NextModuleVersion {
550610
[string] $ModuleName,
551611

552612
# The GitHub releases list, used for incremental prerelease calculation.
553-
[Parameter(Mandatory)]
554-
[array] $Releases
613+
# Empty or null when the repository has no releases.
614+
[Parameter()]
615+
[AllowNull()]
616+
[AllowEmptyCollection()]
617+
[array] $Releases = @()
555618
)
556619

557620
LogGroup 'Calculate new version' {
558-
$newVersion = New-PSSemVer -Version $LatestVersion
621+
$baseVersion = if ($null -eq $LatestVersion -or [string]::IsNullOrWhiteSpace([string]$LatestVersion)) {
622+
Write-Warning "No latest version was resolved. Using '0.0.0' as the baseline."
623+
'0.0.0'
624+
} else {
625+
$LatestVersion
626+
}
627+
$newVersion = New-PSSemVer -Version $baseVersion
559628
$newVersion.Prefix = $Configuration.VersionPrefix
560629

561630
if ($Decision.MajorRelease) {

‎.github/actions/Resolve-PSModuleVersion/src/main.ps1‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ $decision = if ($null -eq $pullRequest) {
2828
Resolve-ReleaseDecision -Configuration $config -PullRequest $pullRequest
2929
}
3030

31-
$releases = Get-GitHubRelease
31+
$releases = @(Get-GitHubRelease)
3232
$ghVersion = Get-LatestGitHubVersion -Releases $releases
3333
$psGalleryVersion = Get-LatestPSGalleryVersion -ModuleName $actionInput.Name
3434
$latestVersion = Get-LatestPublishedVersion -GitHubVersion $ghVersion -PSGalleryVersion $psGalleryVersion

0 commit comments

Comments
 (0)