Skip to content

Commit 15c2814

Browse files
⚙️ [Maintenance]: Publish and release execution paths are now decoupled (#407)
PowerShell Gallery publishing and GitHub Release creation now run as independent publish-pipeline responsibilities, so each operation reports its own outcome and a partial release can be safely retried. ## Changed: Publishing and release operations are separate `Publish-PSModule` now publishes the tested module artifact to PowerShell Gallery only. GitHub release creation, release notes, and release-asset upload run through the dedicated `Release-PSModule` action. ## Technical Details - The Plan job remains the single version authority: `Publish.Module.Resolution.FullVersion` is passed directly to `Release-PSModule` as the GitHub release tag. - `Release-PSModule` detects an existing tag, resumes the module ZIP upload, and exports the final tag and release URL as action outputs. - Gallery and release steps have independent failure behavior; cleanup only runs after a successful stable release, while abandoned-PR cleanup remains supported. - Cleanup receives the release tag through the release action's declared output rather than shared environment state. - The reusable workflow now uses job-scoped write permissions and a pinned Ubuntu runner; both publishing actions have action-local READMEs. - Pester covers prefixed prerelease `WhatIf` behavior and resuming an existing release without recreating it. Full live publishing remains intentionally out of scope. <details> <summary>Related issues</summary> - #406 </details> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 120554a commit 15c2814

11 files changed

Lines changed: 572 additions & 344 deletions

File tree

.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
@@ -28,5 +32,5 @@ runs:
2832
working-directory: ${{ inputs.WorkingDirectory }}
2933
env:
3034
PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_WhatIf: ${{ inputs.WhatIf }}
31-
PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag }}
35+
PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ inputs.ReleaseTag }}
3236
run: ${{ github.action_path }}/src/cleanup.ps1
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.
Lines changed: 2 additions & 25 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,36 +14,17 @@ 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
3927
default: module
40-
VersionPrefix:
41-
description: |
42-
Prefix put in front of the version in the git tag of the GitHub release, for example 'v'.
43-
Comes from Settings.Publish.Module.VersionPrefix, which the Plan job resolves. The compiled manifest carries the module version as
44-
Major.Minor.Patch and cannot carry the prefix, so it is supplied here. An empty value tags the release without a prefix.
45-
required: false
46-
default: ''
4728

4829
runs:
4930
using: composite
@@ -69,8 +50,4 @@ runs:
6950
PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath: ${{ inputs.ModulePath }}
7051
PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey: ${{ inputs.APIKey }}
7152
PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }}
72-
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes: ${{ inputs.UsePRBodyAsReleaseNotes }}
73-
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName: ${{ inputs.UsePRTitleAsReleaseName }}
74-
PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsNotesHeading: ${{ inputs.UsePRTitleAsNotesHeading }}
75-
PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix: ${{ inputs.VersionPrefix }}
7653
run: ${{ github.action_path }}/src/publish.ps1

.github/actions/Publish-PSModule/src/Publish-PSModule.Helpers.psm1

Lines changed: 0 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -43,62 +43,3 @@
4343

4444
"$ModuleVersion-$($Prerelease.Trim())"
4545
}
46-
47-
function Get-ReleaseTag {
48-
<#
49-
.SYNOPSIS
50-
Builds the git tag used for the GitHub release.
51-
52-
.DESCRIPTION
53-
Prefixes the module's SemVer version string with the configured version prefix. The version comes
54-
from the compiled manifest, which is the artifact that is published, so the tag always names the
55-
exact bytes that were tested and pushed to the PowerShell Gallery. The manifest's ModuleVersion is
56-
Major.Minor.Patch by definition and cannot carry the prefix, so the prefix is supplied from the
57-
resolved settings (Publish.Module.VersionPrefix) instead.
58-
59-
The prefix belongs to the GitHub release tag and to nothing else. PowerShell manifests and Gallery
60-
package versions only accept plain SemVer, so callers that need the module's own version use
61-
Get-ModuleVersionString. Deriving both from the same composition keeps the prefix as the only
62-
difference between them.
63-
64-
.OUTPUTS
65-
String with the release tag.
66-
67-
.EXAMPLE
68-
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10'
69-
70-
Returns 'v1.1.10'.
71-
72-
.EXAMPLE
73-
Get-ReleaseTag -VersionPrefix 'v' -ModuleVersion '1.1.10' -Prerelease 'mybranch001'
74-
75-
Returns 'v1.1.10-mybranch001'.
76-
77-
.EXAMPLE
78-
Get-ReleaseTag -VersionPrefix '' -ModuleVersion '1.1.10'
79-
80-
Returns '1.1.10'.
81-
#>
82-
[CmdletBinding()]
83-
[OutputType([string])]
84-
param(
85-
# The module version from the compiled manifest, in Major.Minor.Patch format.
86-
[Parameter(Mandatory)]
87-
[ValidateNotNullOrEmpty()]
88-
[string] $ModuleVersion,
89-
90-
# The prefix put in front of the version, for example 'v'. Empty for an unprefixed repository.
91-
[Parameter()]
92-
[AllowEmptyString()]
93-
[AllowNull()]
94-
[string] $VersionPrefix,
95-
96-
# The prerelease label from the compiled manifest. Empty for a stable release.
97-
[Parameter()]
98-
[AllowEmptyString()]
99-
[AllowNull()]
100-
[string] $Prerelease
101-
)
102-
103-
"$($VersionPrefix.Trim())$(Get-ModuleVersionString -ModuleVersion $ModuleVersion -Prerelease $Prerelease)"
104-
}

.github/actions/Publish-PSModule/src/publish.ps1

Lines changed: 4 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,10 @@
22
'PSUseDeclaredVarsMoreThanAssignments', 'apiKey',
33
Justification = 'Variable is used in script blocks.'
44
)]
5-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
6-
'PSUseDeclaredVarsMoreThanAssignments', 'usePRBodyAsReleaseNotes',
7-
Justification = 'Variable is used in script blocks.'
8-
)]
9-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
10-
'PSUseDeclaredVarsMoreThanAssignments', 'usePRTitleAsReleaseName',
11-
Justification = 'Variable is used in script blocks.'
12-
)]
13-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
14-
'PSUseDeclaredVarsMoreThanAssignments', 'usePRTitleAsNotesHeading',
15-
Justification = 'Variable is used in script blocks.'
16-
)]
175
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
186
'PSUseDeclaredVarsMoreThanAssignments', 'prNumber',
197
Justification = 'Variable is used in script blocks.'
208
)]
21-
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
22-
'PSUseDeclaredVarsMoreThanAssignments', 'prHeadRef',
23-
Justification = 'Variable is used in script blocks.'
24-
)]
259
[CmdletBinding()]
2610
param()
2711

@@ -56,17 +40,10 @@ LogGroup 'Load inputs' {
5640
$modulePath = Resolve-Path -Path $modulePathCandidate | Select-Object -ExpandProperty Path
5741
$apiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey
5842
$whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true'
59-
$usePRBodyAsReleaseNotes = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes -eq 'true'
60-
$usePRTitleAsReleaseName = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName -eq 'true'
61-
$usePRTitleAsNotesHeading = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsNotesHeading -eq 'true'
62-
# The prefix the repository tags releases with, resolved by the Plan job from
63-
# Settings.Publish.Module.VersionPrefix. Empty means the repository tags without a prefix.
64-
$versionPrefix = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix
6543

66-
Write-Host "Module name: [$name]"
67-
Write-Host "Module path: [$modulePath]"
68-
Write-Host "Version prefix: [$versionPrefix]"
69-
Write-Host "WhatIf: [$whatIf]"
44+
Write-Host "Module name: [$name]"
45+
Write-Host "Module path: [$modulePath]"
46+
Write-Host "WhatIf: [$whatIf]"
7047
}
7148
#endregion Load inputs
7249

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

@@ -134,25 +110,15 @@ LogGroup 'Resolve version from manifest' {
134110
$createPrerelease = $true
135111
}
136112

137-
# The PowerShell Gallery and the module manifest only accept plain SemVer, so the configured
138-
# VersionPrefix is applied to the GitHub release tag and to nothing else. Both strings are derived
139-
# from the same composition here, so the prefix is the only difference between them.
140113
$publishPSVersion = Get-ModuleVersionString -ModuleVersion $moduleVersion -Prerelease $prerelease
141-
$releaseTag = Get-ReleaseTag -VersionPrefix $versionPrefix -ModuleVersion $moduleVersion -Prerelease $prerelease
142114

143115
[PSCustomObject]@{
144116
ModuleVersion = $moduleVersion
145-
VersionPrefix = $versionPrefix
146117
Prerelease = $prerelease
147118
CreatePrerelease = $createPrerelease
148119
GalleryVersion = $publishPSVersion
149-
ReleaseTag = $releaseTag
150120
PRNumber = $prNumber
151-
PRHeadRef = $prHeadRef
152121
} | Format-List | Out-String
153-
154-
# Expose release tag to subsequent steps so cleanup can exclude the just-published tag.
155-
"PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag=$releaseTag" | Out-File -Path $env:GITHUB_ENV -Append -Encoding utf8NoBOM
156122
}
157123
#endregion Resolve version from manifest
158124

@@ -194,100 +160,4 @@ LogGroup 'Publish to PSGallery' {
194160
}
195161
}
196162
#endregion Publish to PSGallery
197-
198-
#region Create GitHub release with module artifact attached
199-
# A zip of the published module is uploaded so the GitHub Release page exposes the exact bytes
200-
# that were tested and pushed to the PowerShell Gallery.
201-
LogGroup 'Create GitHub release' {
202-
$releaseCreateCommand = @('release', 'create', $releaseTag)
203-
$notesFilePath = $null
204-
205-
if ($usePRTitleAsReleaseName -and $pull_request.title) {
206-
$releaseCreateCommand += @('--title', $pull_request.title)
207-
Write-Host "Using PR title as release name: [$($pull_request.title)]"
208-
} else {
209-
$releaseCreateCommand += @('--title', $releaseTag)
210-
}
211-
212-
# Build release notes content. Uses temp file to avoid escaping issues with special characters.
213-
# Precedence rules for the three UsePR* parameters:
214-
# 1. UsePRTitleAsNotesHeading + UsePRBodyAsReleaseNotes: Creates "# Title (#PR)\n\nBody" format.
215-
# 2. UsePRBodyAsReleaseNotes only: Uses PR body as-is.
216-
# 3. Fallback: Auto-generates notes via GitHub's --generate-notes.
217-
if ($usePRTitleAsNotesHeading -and $usePRBodyAsReleaseNotes -and $pull_request.title -and $pull_request.body) {
218-
$notes = "# $($pull_request.title) (#$prNumber)`n`n$($pull_request.body)"
219-
$notesFilePath = [System.IO.Path]::GetTempFileName()
220-
Set-Content -Path $notesFilePath -Value $notes -Encoding utf8
221-
$releaseCreateCommand += @('--notes-file', $notesFilePath)
222-
Write-Host 'Using PR title as H1 heading with link and body as release notes'
223-
} elseif ($usePRBodyAsReleaseNotes -and $pull_request.body) {
224-
$notesFilePath = [System.IO.Path]::GetTempFileName()
225-
Set-Content -Path $notesFilePath -Value $pull_request.body -Encoding utf8
226-
$releaseCreateCommand += @('--notes-file', $notesFilePath)
227-
Write-Host 'Using PR body as release notes'
228-
} else {
229-
$releaseCreateCommand += @('--generate-notes')
230-
}
231-
232-
if ($createPrerelease) {
233-
$releaseCreateCommand += @('--target', $prHeadRef, '--prerelease')
234-
}
235-
236-
if ($whatIf) {
237-
Write-Host "WhatIf: gh $($releaseCreateCommand -join ' ')"
238-
$releaseURL = "https://github.com/$env:GITHUB_REPOSITORY/releases/tag/$releaseTag"
239-
} else {
240-
try {
241-
$releaseURL = gh @releaseCreateCommand
242-
if ($LASTEXITCODE -ne 0) {
243-
Write-Error "Failed to create the release [$releaseTag]."
244-
exit $LASTEXITCODE
245-
}
246-
} finally {
247-
if ($notesFilePath -and (Test-Path -Path $notesFilePath)) {
248-
Remove-Item -Path $notesFilePath -Force
249-
}
250-
}
251-
}
252-
253-
# Attach the built module as a release artifact so consumers can download the exact
254-
# bytes that were tested and published to the PowerShell Gallery.
255-
$zipFileName = "$name-$publishPSVersion.zip"
256-
$zipPath = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath $zipFileName
257-
if (Test-Path -Path $zipPath) {
258-
Remove-Item -Path $zipPath -Force
259-
}
260-
if ($whatIf) {
261-
Write-Host "WhatIf: Compress-Archive -Path $modulePath -DestinationPath $zipPath -Force"
262-
Write-Host "WhatIf: gh release upload $releaseTag $zipPath --clobber"
263-
} else {
264-
Write-Host "Compressing module to [$zipPath]"
265-
Compress-Archive -Path $modulePath -DestinationPath $zipPath -Force
266-
try {
267-
gh release upload $releaseTag $zipPath --clobber
268-
if ($LASTEXITCODE -ne 0) {
269-
Write-Error "Failed to upload module artifact to release [$releaseTag]."
270-
exit $LASTEXITCODE
271-
}
272-
Write-Host "::notice title=📦 Attached module artifact to release::$zipFileName"
273-
} finally {
274-
if (Test-Path -Path $zipPath) {
275-
Remove-Item -Path $zipPath -Force
276-
}
277-
}
278-
}
279-
280-
if ($whatIf) {
281-
Write-Host "gh pr comment $prNumber -b '✅ $($releaseType): GitHub - $name $releaseTag'"
282-
} else {
283-
gh pr comment $prNumber -b "$releaseType`: GitHub - [$name $releaseTag]($releaseURL)"
284-
if ($LASTEXITCODE -ne 0) {
285-
Write-Error 'Failed to comment on the pull request.'
286-
exit $LASTEXITCODE
287-
}
288-
}
289-
Write-Host "::notice title=✅ $releaseType`: GitHub - $name $releaseTag::$releaseURL"
290-
}
291-
#endregion Create GitHub release
292-
293-
Write-Host "Publishing complete. PowerShell Gallery version: [$publishPSVersion]. GitHub release tag: [$releaseTag]."
163+
Write-Host "Gallery publishing complete. Version: [$publishPSVersion]"

0 commit comments

Comments
 (0)