Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/actions/Publish-PSModule/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ inputs:
description: Name of the uploaded artifact to download. Must match the name used in the upstream upload-artifact step.
required: false
default: module
VersionPrefix:
description: |
Prefix put in front of the version in the git tag of the GitHub release, for example 'v'.
Comes from Settings.Publish.Module.VersionPrefix, which the Plan job resolves. The compiled manifest carries the module version as
Major.Minor.Patch and cannot carry the prefix, so it is supplied here. An empty value tags the release without a prefix.
required: false
default: ''

runs:
using: composite
Expand Down Expand Up @@ -65,4 +72,5 @@ runs:
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes: ${{ inputs.UsePRBodyAsReleaseNotes }}
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName: ${{ inputs.UsePRTitleAsReleaseName }}
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsNotesHeading: ${{ inputs.UsePRTitleAsNotesHeading }}
PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix: ${{ inputs.VersionPrefix }}
run: ${{ github.action_path }}/src/publish.ps1
104 changes: 104 additions & 0 deletions .github/actions/Publish-PSModule/src/Publish-PSModule.Helpers.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
ο»Ώfunction Get-ModuleVersionString {
<#
.SYNOPSIS
Builds the SemVer version string that identifies the module itself.

.DESCRIPTION
Composes the module version and, when there is one, the prerelease label. This is the string the
PowerShell Gallery and the module manifest understand: `Major.Minor.Patch` optionally followed by
`-<prerelease>`. It never carries the repository's version prefix, because neither the manifest's
`ModuleVersion` nor a Gallery package version accepts one.

.OUTPUTS
String with the module version.

.EXAMPLE
Get-ModuleVersionString -ModuleVersion '1.1.10'

Returns '1.1.10'.

.EXAMPLE
Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease 'mybranch001'

Returns '1.1.10-mybranch001'.
#>
[CmdletBinding()]
[OutputType([string])]
param(
# The module version from the compiled manifest, in Major.Minor.Patch format.
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ModuleVersion,

# The prerelease label from the compiled manifest. Empty for a stable release.
[Parameter()]
[AllowEmptyString()]
[AllowNull()]
[string] $Prerelease
)

if ([string]::IsNullOrWhiteSpace($Prerelease)) {
return $ModuleVersion
}

"$ModuleVersion-$($Prerelease.Trim())"
}

function Get-ReleaseTag {
<#
.SYNOPSIS
Builds the git tag used for the GitHub release.

.DESCRIPTION
Prefixes the module's SemVer version string with the configured version prefix. The version comes
from the compiled manifest, which is the artifact that is published, so the tag always names the
exact bytes that were tested and pushed to the PowerShell Gallery. The manifest's ModuleVersion is
Major.Minor.Patch by definition and cannot carry the prefix, so the prefix is supplied from the
resolved settings (Publish.Module.VersionPrefix) instead.

The prefix belongs to the GitHub release tag and to nothing else. PowerShell manifests and Gallery
package versions only accept plain SemVer, so callers that need the module's own version use
Get-ModuleVersionString. Deriving both from the same composition keeps the prefix as the only
difference between them.

.OUTPUTS
String with the release tag.

.EXAMPLE
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10'

Returns 'v1.1.10'.

.EXAMPLE
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001'

Returns 'v1.1.10-mybranch001'.

.EXAMPLE
Get-ReleaseTag -VersionPrefix '' -ModuleVersion '1.1.10'

Returns '1.1.10'.
#>
[CmdletBinding()]
[OutputType([string])]
param(
# The module version from the compiled manifest, in Major.Minor.Patch format.
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $ModuleVersion,

# The prefix put in front of the version, for example 'v'. Empty for an unprefixed repository.
[Parameter()]
[AllowEmptyString()]
[AllowNull()]
[string] $VersionPrefix,

# The prerelease label from the compiled manifest. Empty for a stable release.
[Parameter()]
[AllowEmptyString()]
[AllowNull()]
[string] $Prerelease
)

"$($VersionPrefix.Trim())$(Get-ModuleVersionString -ModuleVersion $ModuleVersion -Prerelease $Prerelease)"
}
22 changes: 16 additions & 6 deletions .github/actions/Publish-PSModule/src/publish.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ param()
$PSStyle.OutputRendering = 'Ansi'

Import-Module -Name 'PSModule' -Force
Import-Module -Name "$PSScriptRoot/Publish-PSModule.Helpers.psm1" -Force

#region Load inputs
LogGroup 'Load inputs' {
Expand Down Expand Up @@ -58,10 +59,14 @@ LogGroup 'Load inputs' {
$usePRBodyAsReleaseNotes = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes -eq 'true'
$usePRTitleAsReleaseName = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName -eq 'true'
$usePRTitleAsNotesHeading = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsNotesHeading -eq 'true'
# The prefix the repository tags releases with, resolved by the Plan job from
# Settings.Publish.Module.VersionPrefix. Empty means the repository tags without a prefix.
$versionPrefix = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix

Write-Host "Module name: [$name]"
Write-Host "Module path: [$modulePath]"
Write-Host "WhatIf: [$whatIf]"
Write-Host "Module name: [$name]"
Write-Host "Module path: [$modulePath]"
Write-Host "Version prefix: [$versionPrefix]"
Write-Host "WhatIf: [$whatIf]"
}
#endregion Load inputs

Expand Down Expand Up @@ -129,12 +134,18 @@ LogGroup 'Resolve version from manifest' {
$createPrerelease = $true
}

$releaseTag = if ($createPrerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion }
# The PowerShell Gallery and the module manifest only accept plain SemVer, so the configured
# VersionPrefix is applied to the GitHub release tag and to nothing else. Both strings are derived
# from the same composition here, so the prefix is the only difference between them.
$publishPSVersion = Get-ModuleVersionString -ModuleVersion $moduleVersion -Prerelease $prerelease
$releaseTag = Get-ReleaseTag -VersionPrefix $versionPrefix -ModuleVersion $moduleVersion -Prerelease $prerelease

[PSCustomObject]@{
ModuleVersion = $moduleVersion
VersionPrefix = $versionPrefix
Prerelease = $prerelease
CreatePrerelease = $createPrerelease
GalleryVersion = $publishPSVersion
ReleaseTag = $releaseTag
PRNumber = $prNumber
PRHeadRef = $prHeadRef
Expand All @@ -154,7 +165,6 @@ LogGroup 'Install module dependencies' {
#region Publish to PSGallery
LogGroup 'Publish to PSGallery' {
$releaseType = if ($createPrerelease) { 'New prerelease' } else { 'New release' }
$publishPSVersion = if ($createPrerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion }
$psGalleryReleaseLink = "https://www.powershellgallery.com/packages/$name/$publishPSVersion"

Write-Host 'Publish module to PowerShell Gallery using API key from environment.'
Expand Down Expand Up @@ -280,4 +290,4 @@ LogGroup 'Create GitHub release' {
}
#endregion Create GitHub release

Write-Host "Publishing complete. Version: [$releaseTag]"
Write-Host "Publishing complete. PowerShell Gallery version: [$publishPSVersion]. GitHub release tag: [$releaseTag]."
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
ο»Ώ[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', '',
Justification = 'Variables are assigned in BeforeAll and used inside It blocks.'
)]
[CmdletBinding()]
param()

BeforeAll {
Import-Module -Name 'PSModule' -Force
Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '../src/Publish-PSModule.Helpers.psm1') -Force
}

Describe 'Publish-PSModule.Helpers' {
Describe 'Get-ModuleVersionString' {
Context 'Get-ModuleVersionString - SemVer only, never prefixed' {
It 'Get-ModuleVersionString - returns the module version for a stable release' {
Get-ModuleVersionString -ModuleVersion '1.1.10' | Should -Be '1.1.10'
}

It 'Get-ModuleVersionString - appends the prerelease label' {
Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease 'mybranch001' |
Should -Be '1.1.10-mybranch001'
}

It 'Get-ModuleVersionString - treats an empty prerelease label as a stable release' {
Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease '' | Should -Be '1.1.10'
}

It 'Get-ModuleVersionString - treats a whitespace-only prerelease label as a stable release' {
Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease ' ' | Should -Be '1.1.10'
}

It 'Get-ModuleVersionString - trims whitespace around the prerelease label' {
Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease ' mybranch001 ' |
Should -Be '1.1.10-mybranch001'
}

It 'Get-ModuleVersionString - takes no version prefix parameter at all' {
(Get-Command Get-ModuleVersionString).Parameters.Keys | Should -Not -Contain 'VersionPrefix'
}

It 'Get-ModuleVersionString - requires a module version' {
{ Get-ModuleVersionString -ModuleVersion '' } | Should -Throw
}
}
}

Describe 'Get-ReleaseTag' {
Context 'Get-ReleaseTag - repository with a version prefix' {
It 'Get-ReleaseTag - prefixes a stable release tag with the configured prefix' {
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' | Should -Be 'v1.1.10'
}

It 'Get-ReleaseTag - prefixes a stable release tag when the prerelease label is empty' {
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease '' | Should -Be 'v1.1.10'
}

It 'Get-ReleaseTag - prefixes a prerelease tag with the configured prefix' {
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001' |
Should -Be 'v1.1.10-mybranch001'
}

It 'Get-ReleaseTag - supports a multi-character prefix' {
Get-ReleaseTag -VersionPrefix 'release-v' -ModuleVersion '2.0.0' | Should -Be 'release-v2.0.0'
}
}

Context 'Get-ReleaseTag - repository without a version prefix' {
It 'Get-ReleaseTag - leaves a stable release tag unprefixed' {
Get-ReleaseTag -VersionPrefix '' -ModuleVersion '1.1.10' | Should -Be '1.1.10'
}

It 'Get-ReleaseTag - leaves a prerelease tag unprefixed' {
Get-ReleaseTag -VersionPrefix '' -ModuleVersion '1.1.10' -Prerelease 'mybranch001' |
Should -Be '1.1.10-mybranch001'
}

It 'Get-ReleaseTag - treats an absent prefix as no prefix' {
Get-ReleaseTag -ModuleVersion '1.1.10' | Should -Be '1.1.10'
}

It 'Get-ReleaseTag - treats a null prefix as no prefix' {
Get-ReleaseTag -VersionPrefix $null -ModuleVersion '1.1.10' | Should -Be '1.1.10'
}
}

Context 'Get-ReleaseTag - input normalization' {
It 'Get-ReleaseTag - trims whitespace around the prefix' {
Get-ReleaseTag -VersionPrefix ' v ' -ModuleVersion '1.1.10' | Should -Be 'v1.1.10'
}

It 'Get-ReleaseTag - trims whitespace around the prerelease label' {
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease ' mybranch001 ' |
Should -Be 'v1.1.10-mybranch001'
}

It 'Get-ReleaseTag - treats a whitespace-only prerelease label as a stable release' {
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease ' ' | Should -Be 'v1.1.10'
}

It 'Get-ReleaseTag - requires a module version' {
{ Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '' } | Should -Throw
}
}

# Cleanup-PSModulePrereleases selects the releases to delete with
# `tagName -like "*$prereleaseName*" -and tagName -ne $publishedReleaseTag`, where the published tag is
# the value publish.ps1 exports as PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag. Both halves of that
# filter have to keep working once the tag carries a prefix.
Context 'Get-ReleaseTag - AutoCleanup tag matching contract' {
It 'Get-ReleaseTag - keeps the prerelease name inside a prefixed tag so cleanup still matches it' {
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001' |
Should -BeLike '*mybranch*'
}

It 'Get-ReleaseTag - keeps the prerelease name inside an unprefixed tag so cleanup still matches it' {
Get-ReleaseTag -VersionPrefix '' -ModuleVersion '1.1.10' -Prerelease 'mybranch001' |
Should -BeLike '*mybranch*'
}

It 'Get-ReleaseTag - produces the same tag twice so cleanup can exclude the published release' {
$first = Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
$second = Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
$first | Should -Be $second
}
}

# The PowerShell Gallery and the module manifest only accept plain SemVer. The prefix therefore
# belongs to the GitHub release tag and to nothing else, and the two strings must differ by exactly
# the prefix - never by anything else, and never in the other direction.
Context 'Get-ReleaseTag - the prefix reaches the release tag and nothing else' {
It 'Get-ReleaseTag - the tag is the prefix followed by the module version string' -ForEach @(
@{ Prefix = 'v'; Version = '1.1.10'; Label = '' }
@{ Prefix = 'v'; Version = '1.1.10'; Label = 'mybranch001' }
@{ Prefix = ''; Version = '1.1.10'; Label = '' }
@{ Prefix = ''; Version = '1.1.10'; Label = 'mybranch001' }
@{ Prefix = 'release-v'; Version = '2.0.0'; Label = 'mybranch001' }
) {
$moduleVersion = Get-ModuleVersionString -ModuleVersion $Version -Prerelease $Label
$tag = Get-ReleaseTag -VersionPrefix $Prefix -ModuleVersion $Version -Prerelease $Label
$tag | Should -Be "$Prefix$moduleVersion"
}

It 'Get-ReleaseTag - the module version string never gains the prefix' -ForEach @(
@{ Prefix = 'v'; Version = '1.1.10'; Label = '' }
@{ Prefix = 'v'; Version = '1.1.10'; Label = 'mybranch001' }
@{ Prefix = 'release-v'; Version = '2.0.0'; Label = 'mybranch001' }
) {
$moduleVersion = Get-ModuleVersionString -ModuleVersion $Version -Prerelease $Label
$moduleVersion | Should -Not -BeLike "$Prefix*"
$moduleVersion | Should -Match '^\d+\.\d+\.\d+(-[0-9A-Za-z\-.]+)?$'
}

It 'Get-ReleaseTag - an unprefixed repository gets identical strings' {
$moduleVersion = Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
$tag = Get-ReleaseTag -VersionPrefix '' -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
$tag | Should -Be $moduleVersion
}

It 'Get-ReleaseTag - stripping the prefix from the tag yields the module version string' {
$moduleVersion = Get-ModuleVersionString -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
$tag = Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
$tag -replace '^v' | Should -Be $moduleVersion
}
}
}
}
1 change: 1 addition & 0 deletions .github/workflows/Publish-Module.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ jobs:
UsePRTitleAsReleaseName: ${{ fromJson(inputs.Settings).Publish.Module.UsePRTitleAsReleaseName }}
UsePRBodyAsReleaseNotes: ${{ fromJson(inputs.Settings).Publish.Module.UsePRBodyAsReleaseNotes }}
UsePRTitleAsNotesHeading: ${{ fromJson(inputs.Settings).Publish.Module.UsePRTitleAsNotesHeading }}
VersionPrefix: ${{ fromJson(inputs.Settings).Publish.Module.VersionPrefix }}
WorkingDirectory: ${{ fromJson(inputs.Settings).WorkingDirectory }}

- name: Cleanup prereleases
Expand Down
Loading