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
6 changes: 5 additions & 1 deletion .github/actions/Cleanup-PSModulePrereleases/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ inputs:
description: Control whether to automatically delete prerelease tags.
required: false
default: 'true'
ReleaseTag:
description: GitHub release tag to retain during cleanup.
required: false
default: ''
WhatIf:
description: If specified, the action will only log the changes it would make.
required: false
Expand All @@ -28,5 +32,5 @@ runs:
working-directory: ${{ inputs.WorkingDirectory }}
env:
PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_WhatIf: ${{ inputs.WhatIf }}
PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag }}
PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ inputs.ReleaseTag }}
run: ${{ github.action_path }}/src/cleanup.ps1
32 changes: 32 additions & 0 deletions .github/actions/Publish-PSModule/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Publish-PSModule

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.

## Inputs

| Name | Description | Required | Default |
| --- | --- | --- | --- |
| `Name` | Name of the module to publish. | No | Repository name |
| `ModulePath` | Path containing the built `<Name>/` module directory. | No | `outputs/module` |
| `ArtifactName` | Name of the module artifact to download. | No | `module` |
| `APIKey` | PowerShell Gallery API key. | Yes | N/A |
| `WhatIf` | Logs publishing operations without publishing the module. | No | `false` |
| `WorkingDirectory` | Directory where the publishing script runs. | No | `.` |

## Outputs

This action does not provide outputs.

## Usage

```yaml
- name: Publish module
uses: ./.github/actions/Publish-PSModule
with:
Name: ExampleModule
ModulePath: outputs/module
ArtifactName: module
APIKey: ${{ secrets.APIKEY }}
```

Use [Release-PSModule](../Release-PSModule/README.md) in a separate workflow step to create the GitHub release from the same artifact.
27 changes: 2 additions & 25 deletions .github/actions/Publish-PSModule/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Publish-PSModule
description: Publish a pre-versioned PowerShell module artifact to the PowerShell Gallery and GitHub Releases.
description: Publish a pre-versioned PowerShell module artifact to the PowerShell Gallery.
author: PSModule

inputs:
Expand All @@ -14,36 +14,17 @@ inputs:
description: PowerShell Gallery API Key.
required: true
WhatIf:
description: If specified, the action will only log the changes it would make, but will not actually create or delete any releases or tags.
description: If specified, the action will only log the changes it would make, but will not publish the module.
required: false
default: 'false'
WorkingDirectory:
description: The working directory where the script will run from.
required: false
default: '.'
UsePRTitleAsReleaseName:
description: When enabled, uses the pull request title as the name for the GitHub release. If not set, the version string is used.
required: false
default: 'false'
UsePRBodyAsReleaseNotes:
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.
required: false
default: 'true'
UsePRTitleAsNotesHeading:
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.
required: false
default: 'true'
ArtifactName:
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 All @@ -69,8 +50,4 @@ runs:
PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath: ${{ inputs.ModulePath }}
PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey: ${{ inputs.APIKey }}
PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }}
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,62 +43,3 @@

"$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)"
}
138 changes: 4 additions & 134 deletions .github/actions/Publish-PSModule/src/publish.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,10 @@
'PSUseDeclaredVarsMoreThanAssignments', 'apiKey',
Justification = 'Variable is used in script blocks.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', 'usePRBodyAsReleaseNotes',
Justification = 'Variable is used in script blocks.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', 'usePRTitleAsReleaseName',
Justification = 'Variable is used in script blocks.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', 'usePRTitleAsNotesHeading',
Justification = 'Variable is used in script blocks.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', 'prNumber',
Justification = 'Variable is used in script blocks.'
)]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
'PSUseDeclaredVarsMoreThanAssignments', 'prHeadRef',
Justification = 'Variable is used in script blocks.'
)]
[CmdletBinding()]
param()

Expand Down Expand Up @@ -56,17 +40,10 @@ LogGroup 'Load inputs' {
$modulePath = Resolve-Path -Path $modulePathCandidate | Select-Object -ExpandProperty Path
$apiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey
$whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true'
$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 "Version prefix: [$versionPrefix]"
Write-Host "WhatIf: [$whatIf]"
Write-Host "Module name: [$name]"
Write-Host "Module path: [$modulePath]"
Write-Host "WhatIf: [$whatIf]"
}
#endregion Load inputs

Expand All @@ -79,7 +56,6 @@ LogGroup 'Load PR information' {
throw 'GitHub event does not contain pull_request data. This script must be run from a pull_request event.'
}
$prNumber = $pull_request.number
$prHeadRef = $pull_request.head.ref
}
#endregion Load PR information

Expand Down Expand Up @@ -134,25 +110,15 @@ LogGroup 'Resolve version from manifest' {
$createPrerelease = $true
}

# 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
} | Format-List | Out-String

# Expose release tag to subsequent steps so cleanup can exclude the just-published tag.
"PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag=$releaseTag" | Out-File -Path $env:GITHUB_ENV -Append -Encoding utf8NoBOM
}
#endregion Resolve version from manifest

Expand Down Expand Up @@ -194,100 +160,4 @@ LogGroup 'Publish to PSGallery' {
}
}
#endregion Publish to PSGallery

#region Create GitHub release with module artifact attached
# A zip of the published module is uploaded so the GitHub Release page exposes the exact bytes
# that were tested and pushed to the PowerShell Gallery.
LogGroup 'Create GitHub release' {
$releaseCreateCommand = @('release', 'create', $releaseTag)
$notesFilePath = $null

if ($usePRTitleAsReleaseName -and $pull_request.title) {
$releaseCreateCommand += @('--title', $pull_request.title)
Write-Host "Using PR title as release name: [$($pull_request.title)]"
} else {
$releaseCreateCommand += @('--title', $releaseTag)
}

# Build release notes content. Uses temp file to avoid escaping issues with special characters.
# Precedence rules for the three UsePR* parameters:
# 1. UsePRTitleAsNotesHeading + UsePRBodyAsReleaseNotes: Creates "# Title (#PR)\n\nBody" format.
# 2. UsePRBodyAsReleaseNotes only: Uses PR body as-is.
# 3. Fallback: Auto-generates notes via GitHub's --generate-notes.
if ($usePRTitleAsNotesHeading -and $usePRBodyAsReleaseNotes -and $pull_request.title -and $pull_request.body) {
$notes = "# $($pull_request.title) (#$prNumber)`n`n$($pull_request.body)"
$notesFilePath = [System.IO.Path]::GetTempFileName()
Set-Content -Path $notesFilePath -Value $notes -Encoding utf8
$releaseCreateCommand += @('--notes-file', $notesFilePath)
Write-Host 'Using PR title as H1 heading with link and body as release notes'
} elseif ($usePRBodyAsReleaseNotes -and $pull_request.body) {
$notesFilePath = [System.IO.Path]::GetTempFileName()
Set-Content -Path $notesFilePath -Value $pull_request.body -Encoding utf8
$releaseCreateCommand += @('--notes-file', $notesFilePath)
Write-Host 'Using PR body as release notes'
} else {
$releaseCreateCommand += @('--generate-notes')
}

if ($createPrerelease) {
$releaseCreateCommand += @('--target', $prHeadRef, '--prerelease')
}

if ($whatIf) {
Write-Host "WhatIf: gh $($releaseCreateCommand -join ' ')"
$releaseURL = "https://github.com/$env:GITHUB_REPOSITORY/releases/tag/$releaseTag"
} else {
try {
$releaseURL = gh @releaseCreateCommand
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to create the release [$releaseTag]."
exit $LASTEXITCODE
}
} finally {
if ($notesFilePath -and (Test-Path -Path $notesFilePath)) {
Remove-Item -Path $notesFilePath -Force
}
}
}

# Attach the built module as a release artifact so consumers can download the exact
# bytes that were tested and published to the PowerShell Gallery.
$zipFileName = "$name-$publishPSVersion.zip"
$zipPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $zipFileName
if (Test-Path -Path $zipPath) {
Remove-Item -Path $zipPath -Force
}
if ($whatIf) {
Write-Host "WhatIf: Compress-Archive -Path $modulePath -DestinationPath $zipPath -Force"
Write-Host "WhatIf: gh release upload $releaseTag $zipPath --clobber"
} else {
Write-Host "Compressing module to [$zipPath]"
Compress-Archive -Path $modulePath -DestinationPath $zipPath -Force
try {
gh release upload $releaseTag $zipPath --clobber
if ($LASTEXITCODE -ne 0) {
Write-Error "Failed to upload module artifact to release [$releaseTag]."
exit $LASTEXITCODE
}
Write-Host "::notice title=📦 Attached module artifact to release::$zipFileName"
} finally {
if (Test-Path -Path $zipPath) {
Remove-Item -Path $zipPath -Force
}
}
}

if ($whatIf) {
Write-Host "gh pr comment $prNumber -b '✅ $($releaseType): GitHub - $name $releaseTag'"
} else {
gh pr comment $prNumber -b "✅ $releaseType`: GitHub - [$name $releaseTag]($releaseURL)"
if ($LASTEXITCODE -ne 0) {
Write-Error 'Failed to comment on the pull request.'
exit $LASTEXITCODE
}
}
Write-Host "::notice title=✅ $releaseType`: GitHub - $name $releaseTag::$releaseURL"
}
#endregion Create GitHub release

Write-Host "Publishing complete. PowerShell Gallery version: [$publishPSVersion]. GitHub release tag: [$releaseTag]."
Write-Host "Gallery publishing complete. Version: [$publishPSVersion]"
Loading
Loading