Skip to content

Commit 03c0f8b

Browse files
🌟 [Major]: Version calculation removed — artifact must be pre-stamped before publish (#71)
`Publish-PSModule` no longer calculates or mutates the module version. The artifact passed in must already contain the final `ModuleVersion` (and `Prerelease` tag, if any) stamped by the upstream build. Published GitHub Releases now include a downloadable zip of the exact module folder that was tested and pushed to the Gallery. ## Breaking Changes Version-calculation inputs have been removed. Callers must supply a pre-stamped artifact: **Removed inputs:** - `AutoPatching` - `IncrementalPrerelease` - `DatePrereleaseFormat` - `VersionPrefix` - `MajorLabels`, `MinorLabels`, `PatchLabels`, `IgnoreLabels` - `ReleaseType` **Migration:** Consumers on `PSModule/Process-PSModule` get this for free — the workflow resolves the version in the Plan job and stamps it during Build. Direct callers outside of `Process-PSModule` must use [`Resolve-PSModuleVersion`](https://github.com/PSModule/Resolve-PSModuleVersion) to compute the version and [`Build-PSModule`](https://github.com/PSModule/Build-PSModule) v5+ to stamp it before invoking this action. ## New: Module zip uploaded to GitHub Release After creating a GitHub Release, the module folder is zipped (`<Name>-<Version>.zip`) and uploaded as a release asset. The zip preserves the `<Name>/` directory structure so it can be extracted directly into a PowerShell module path. ## Changed: Cleanup only runs after stable releases The cleanup step (which removes old prerelease tags/releases) now only executes when the publish was a stable release. Previously it could inadvertently delete the just-published prerelease. Cleanup also filters on `isPrerelease` to avoid accidentally deleting stable releases whose tag happens to match the derived prerelease name. ## Technical Details - Deleted `src/init.ps1` (the old version-calculation script). - `src/publish.ps1` reads `ModuleVersion` and `Prerelease` directly from the downloaded manifest via `Import-PowerShellDataFile`, validates 3-part format, then publishes untouched via `Publish-PSResource`. - `Test-ModuleManifest` is called as advisory validation (non-terminating) since the built artifact may reference `RequiredModules` not installed on the runner. Structural validation is enforced by explicit regex guards on `ModuleVersion` and `Prerelease`. - `src/cleanup.ps1` derives the prerelease name from the PR head ref, filters on `isPrerelease`, and explicitly excludes the just-published release tag from deletion. - `action.yml` cleanup step gated on `env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_IsPrerelease != 'true'`. - `GITHUB_ENV` writes use `utf8NoBOM` encoding to prevent BOM corruption. - Zip upload and temp file cleanup wrapped in `try/finally` for reliable cleanup on failure. <details><summary>Related issues</summary> - Fixes PSModule/Process-PSModule#326 - PSModule/Process-PSModule#342 - PSModule/Resolve-PSModuleVersion#1 - PSModule/Build-PSModule#136 </details>
1 parent 72572ee commit 03c0f8b

10 files changed

Lines changed: 426 additions & 567 deletions

File tree

‎.github/workflows/Action-Test.yml‎

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: Action-Test
22

3-
run-name: "Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}"
3+
run-name: 'Action-Test - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}'
44

55
on:
66
workflow_dispatch:
@@ -29,7 +29,7 @@ jobs:
2929
- name: Upload module artifact
3030
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
3131
with:
32-
name: module
32+
name: module-default
3333
path: tests/outputs/module
3434

3535
- name: Action-Test
@@ -38,6 +38,149 @@ jobs:
3838
GITHUB_TOKEN: ${{ github.token }}
3939
with:
4040
Name: PSModuleTest
41+
ArtifactName: module-default
4142
WorkingDirectory: tests
4243
APIKey: ${{ secrets.APIKEY }} # zizmor: ignore[secrets-outside-env] saved in org secrets as an intentional choice
4344
WhatIf: true
45+
46+
- name: Validate module version is stamped
47+
shell: pwsh
48+
run: |
49+
$manifestPath = Join-Path $env:GITHUB_WORKSPACE 'outputs' 'module' 'PSModuleTest' 'PSModuleTest.psd1'
50+
$manifest = Import-PowerShellDataFile -Path $manifestPath
51+
$version = $manifest.ModuleVersion
52+
$releaseTag = $env:PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag
53+
54+
Write-Host "Module version in manifest: [$version]"
55+
Write-Host "Release tag from action: [$releaseTag]"
56+
57+
if ([string]::IsNullOrEmpty($releaseTag)) {
58+
Write-Error 'Release tag is empty — the action did not export PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag.'
59+
exit 1
60+
}
61+
if ($version -eq '999.0.0') {
62+
Write-Error "Module version is still the placeholder [999.0.0]. The artifact was not stamped with a real version."
63+
exit 1
64+
}
65+
if ($releaseTag -match '^999\.0\.0') {
66+
Write-Error "Release tag [$releaseTag] is based on the placeholder version. The artifact was not stamped."
67+
exit 1
68+
}
69+
if ($releaseTag -ne $version) {
70+
Write-Error "Release tag [$releaseTag] does not match expected module version [$version]."
71+
exit 1
72+
}
73+
Write-Host "Version validation passed: [$version] (tag: [$releaseTag])"
74+
75+
ActionTestPrerelease:
76+
name: Action-Test - [Prerelease]
77+
runs-on: ubuntu-latest
78+
steps:
79+
- name: Checkout repo
80+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
81+
with:
82+
persist-credentials: false
83+
84+
- name: Upload module artifact
85+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
86+
with:
87+
name: module-prerelease
88+
path: tests/outputs/module-prerelease
89+
90+
- name: Action-Test
91+
uses: ./
92+
env:
93+
GITHUB_TOKEN: ${{ github.token }}
94+
with:
95+
Name: PSModuleTest
96+
ArtifactName: module-prerelease
97+
ModulePath: outputs/module-prerelease
98+
WorkingDirectory: tests
99+
APIKey: ${{ secrets.APIKEY }} # zizmor: ignore[secrets-outside-env] saved in org secrets as an intentional choice
100+
WhatIf: true
101+
102+
- name: Validate prerelease version
103+
shell: pwsh
104+
run: |
105+
$manifestPath = Join-Path $env:GITHUB_WORKSPACE 'outputs' 'module-prerelease' 'PSModuleTest' 'PSModuleTest.psd1'
106+
$manifest = Import-PowerShellDataFile -Path $manifestPath
107+
$version = $manifest.ModuleVersion
108+
$prerelease = $manifest.PrivateData.PSData.Prerelease
109+
$releaseTag = $env:PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag
110+
$isPrerelease = $env:PSMODULE_PUBLISH_PSMODULE_CONTEXT_IsPrerelease
111+
112+
Write-Host "Module version in manifest: [$version]"
113+
Write-Host "Prerelease label: [$prerelease]"
114+
Write-Host "Release tag from action: [$releaseTag]"
115+
Write-Host "IsPrerelease: [$isPrerelease]"
116+
117+
if ($version -eq '999.0.0') {
118+
Write-Error "Module version is still the placeholder [999.0.0]. The artifact was not stamped with a real version."
119+
exit 1
120+
}
121+
if ([string]::IsNullOrEmpty($prerelease)) {
122+
Write-Error 'Prerelease label is missing from manifest.'
123+
exit 1
124+
}
125+
if ($releaseTag -ne "$version-$prerelease") {
126+
Write-Error "Release tag [$releaseTag] does not match expected [$version-$prerelease]."
127+
exit 1
128+
}
129+
if ($isPrerelease -ne 'true') {
130+
Write-Error "IsPrerelease context should be 'true' but got [$isPrerelease]."
131+
exit 1
132+
}
133+
Write-Host "Prerelease validation passed: [$version-$prerelease] (tag: [$releaseTag])"
134+
135+
ActionTestUnstamped:
136+
name: Action-Test - [Unstamped - expect failure]
137+
runs-on: ubuntu-latest
138+
steps:
139+
- name: Checkout repo
140+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
141+
with:
142+
persist-credentials: false
143+
144+
- name: Upload module artifact
145+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
146+
with:
147+
name: module-unstamped
148+
path: tests/outputs/module-unstamped
149+
150+
- name: Action-Test
151+
id: publish
152+
uses: ./
153+
continue-on-error: true
154+
env:
155+
GITHUB_TOKEN: ${{ github.token }}
156+
with:
157+
Name: PSModuleTest
158+
ArtifactName: module-unstamped
159+
ModulePath: outputs/module-unstamped
160+
WorkingDirectory: tests
161+
APIKey: ${{ secrets.APIKEY }} # zizmor: ignore[secrets-outside-env] saved in org secrets as an intentional choice
162+
WhatIf: true
163+
164+
- name: Validate unstamped artifact is rejected
165+
shell: pwsh
166+
env:
167+
PUBLISH_OUTCOME: ${{ steps.publish.outcome }}
168+
run: |
169+
$manifestPath = Join-Path $env:GITHUB_WORKSPACE 'outputs' 'module-unstamped' 'PSModuleTest' 'PSModuleTest.psd1'
170+
$manifest = Import-PowerShellDataFile -Path $manifestPath
171+
$version = $manifest.ModuleVersion
172+
$releaseTag = $env:PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag
173+
174+
Write-Host "Module version in manifest: [$version]"
175+
Write-Host "Release tag from action: [$releaseTag]"
176+
Write-Host "Publish step outcome: [$env:PUBLISH_OUTCOME]"
177+
178+
if ($version -ne '999.0.0') {
179+
Write-Error "Expected placeholder version [999.0.0] but got [$version]."
180+
exit 1
181+
}
182+
if ($env:PUBLISH_OUTCOME -eq 'success') {
183+
Write-Error 'The publish action should have failed for an unstamped artifact but it succeeded.'
184+
exit 1
185+
}
186+
Write-Host 'Unstamped artifact was correctly rejected by the action.'

‎action.yml‎

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

55
inputs:
66
Name:
77
description: Name of the module to publish.
88
required: false
99
ModulePath:
10-
description: Path to the module to publish.
10+
description: Path to the folder containing the <Name>/ module subdirectory from Build-PSModule. A module artifact must exist before invoking this action.
1111
required: false
1212
default: outputs/module
1313
APIKey:
@@ -17,42 +17,6 @@ inputs:
1717
description: Control whether to automatically delete the prerelease tags after the stable release is created.
1818
required: false
1919
default: 'true'
20-
AutoPatching:
21-
description: Control whether to automatically handle patches. If disabled, the action will only create a patch release if the pull request has a 'patch' label.
22-
required: false
23-
default: 'true'
24-
IncrementalPrerelease:
25-
description: Control whether to automatically increment the prerelease number. If disabled, the action will ensure only one prerelease exists for a given branch.
26-
required: false
27-
default: 'true'
28-
DatePrereleaseFormat:
29-
description: If specified, uses a date based prerelease scheme. The format should be a valid .NET format string like 'yyyyMMddHHmm'.
30-
required: false
31-
default: ''
32-
VersionPrefix:
33-
description: The prefix to use for the version number.
34-
required: false
35-
default: v
36-
MajorLabels:
37-
description: A comma separated list of labels that trigger a major release.
38-
required: false
39-
default: major, breaking
40-
MinorLabels:
41-
description: A comma separated list of labels that trigger a minor release.
42-
required: false
43-
default: minor, feature
44-
PatchLabels:
45-
description: A comma separated list of labels that trigger a patch release.
46-
required: false
47-
default: patch, fix
48-
IgnoreLabels:
49-
description: A comma separated list of labels that do not trigger a release.
50-
required: false
51-
default: NoRelease
52-
ReleaseType:
53-
description: The type of release to create. Values are 'Release' (stable), 'Prerelease', or 'None'.
54-
required: false
55-
default: Release
5620
WhatIf:
5721
description: If specified, the action will only log the changes it would make, but will not actually create or delete any releases or tags.
5822
required: false
@@ -73,6 +37,10 @@ inputs:
7337
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.
7438
required: false
7539
default: 'true'
40+
ArtifactName:
41+
description: Name of the uploaded artifact to download. Must match the name used in the upstream upload-artifact step.
42+
required: false
43+
default: module
7644

7745
runs:
7846
using: composite
@@ -85,34 +53,13 @@ runs:
8553
run: |
8654
Install-PSResource -Name Microsoft.PowerShell.PSResourceGet -Repository PSGallery -TrustRepository
8755
88-
- name: Initialize Publish Context
89-
id: init
90-
shell: pwsh
91-
working-directory: ${{ inputs.WorkingDirectory }}
92-
env:
93-
PSMODULE_PUBLISH_PSMODULE_INPUT_Name: ${{ inputs.Name }}
94-
PSMODULE_PUBLISH_PSMODULE_INPUT_AutoCleanup: ${{ inputs.AutoCleanup }}
95-
PSMODULE_PUBLISH_PSMODULE_INPUT_AutoPatching: ${{ inputs.AutoPatching }}
96-
PSMODULE_PUBLISH_PSMODULE_INPUT_DatePrereleaseFormat: ${{ inputs.DatePrereleaseFormat }}
97-
PSMODULE_PUBLISH_PSMODULE_INPUT_IgnoreLabels: ${{ inputs.IgnoreLabels }}
98-
PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseType: ${{ inputs.ReleaseType }}
99-
PSMODULE_PUBLISH_PSMODULE_INPUT_IncrementalPrerelease: ${{ inputs.IncrementalPrerelease }}
100-
PSMODULE_PUBLISH_PSMODULE_INPUT_MajorLabels: ${{ inputs.MajorLabels }}
101-
PSMODULE_PUBLISH_PSMODULE_INPUT_MinorLabels: ${{ inputs.MinorLabels }}
102-
PSMODULE_PUBLISH_PSMODULE_INPUT_PatchLabels: ${{ inputs.PatchLabels }}
103-
PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix: ${{ inputs.VersionPrefix }}
104-
PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }}
105-
run: ${{ github.action_path }}/src/init.ps1
106-
10756
- name: Download module artifact
108-
if: env.PUBLISH_CONTEXT_ShouldPublish == 'true' || inputs.WhatIf == 'true'
10957
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
11058
with:
111-
name: module
59+
name: ${{ inputs.ArtifactName }}
11260
path: ${{ inputs.ModulePath }}
11361

11462
- name: Publish Module
115-
if: env.PUBLISH_CONTEXT_ShouldPublish == 'true' || inputs.WhatIf == 'true'
11663
shell: pwsh
11764
working-directory: ${{ inputs.WorkingDirectory }}
11865
env:
@@ -126,9 +73,10 @@ runs:
12673
run: ${{ github.action_path }}/src/publish.ps1
12774

12875
- name: Cleanup Prereleases
129-
if: env.PUBLISH_CONTEXT_ShouldCleanup == 'true' || inputs.WhatIf == 'true'
76+
if: env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_IsPrerelease != 'true' && (inputs.AutoCleanup == 'true' || inputs.WhatIf == 'true')
13077
shell: pwsh
13178
working-directory: ${{ inputs.WorkingDirectory }}
13279
env:
13380
PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }}
81+
PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag: ${{ env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag }}
13482
run: ${{ github.action_path }}/src/cleanup.ps1

‎src/cleanup.ps1‎

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,48 @@ $PSStyle.OutputRendering = 'Ansi'
55

66
Import-Module -Name 'Helpers' -Force
77

8-
$prereleaseName = $env:PUBLISH_CONTEXT_PrereleaseName
9-
$prereleaseTagsToCleanup = $env:PUBLISH_CONTEXT_PrereleaseTagsToCleanup
10-
$whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true'
8+
#region Load inputs
9+
LogGroup 'Load inputs' {
10+
$whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true'
1111

12-
if ([string]::IsNullOrWhiteSpace($prereleaseName)) {
13-
Write-Error 'PUBLISH_CONTEXT_PrereleaseName is not set. Run init.ps1 first.'
14-
exit 1
12+
$githubEventJson = Get-Content -Raw $env:GITHUB_EVENT_PATH
13+
$githubEvent = $githubEventJson | ConvertFrom-Json
14+
$pull_request = $githubEvent.pull_request
15+
if (-not $pull_request) {
16+
throw 'GitHub event does not contain pull_request data. This script must be run from a pull_request event.'
17+
}
18+
$prHeadRef = $pull_request.head.ref
19+
$prereleaseName = $prHeadRef -replace '[^a-zA-Z0-9]'
20+
21+
if ([string]::IsNullOrWhiteSpace($prereleaseName)) {
22+
Write-Host "No prerelease tag derivable from PR head ref [$prHeadRef]. Nothing to cleanup."
23+
exit 0
24+
}
25+
26+
Write-Host "PR head ref: [$prHeadRef]"
27+
Write-Host "Prerelease name: [$prereleaseName]"
28+
Write-Host "WhatIf: [$whatIf]"
29+
30+
$publishedReleaseTag = $env:PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag
31+
if (-not [string]::IsNullOrWhiteSpace($publishedReleaseTag)) {
32+
Write-Host "Published tag: [$publishedReleaseTag] (excluded from cleanup)"
33+
}
1534
}
35+
#endregion Load inputs
1636

17-
LogGroup "Cleanup prereleases for [$prereleaseName]" {
18-
if ([string]::IsNullOrWhiteSpace($prereleaseTagsToCleanup)) {
19-
Write-Host "No prereleases found to cleanup for [$prereleaseName]."
20-
return
37+
#region Find prereleases to cleanup
38+
LogGroup "Find prereleases to cleanup for [$prereleaseName]" {
39+
$releaseListOutput = gh release list --json 'createdAt,isDraft,isLatest,isPrerelease,name,publishedAt,tagName'
40+
if ($LASTEXITCODE -ne 0) {
41+
Write-Error 'Failed to list releases for the repository.'
42+
exit $LASTEXITCODE
2143
}
44+
$releases = $releaseListOutput | ConvertFrom-Json
2245

23-
$tagsToDelete = $prereleaseTagsToCleanup -split ',' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
46+
$prereleasesToCleanup = $releases | Where-Object {
47+
$_.isPrerelease -eq $true -and $_.tagName -like "*$prereleaseName*" -and $_.tagName -ne $publishedReleaseTag
48+
}
49+
$tagsToDelete = @($prereleasesToCleanup | ForEach-Object { $_.tagName } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
2450

2551
if ($tagsToDelete.Count -eq 0) {
2652
Write-Host "No prereleases found to cleanup for [$prereleaseName]."
@@ -29,8 +55,11 @@ LogGroup "Cleanup prereleases for [$prereleaseName]" {
2955

3056
Write-Host "Found $($tagsToDelete.Count) prereleases to cleanup:"
3157
$tagsToDelete | ForEach-Object { Write-Host " - $_" }
32-
Write-Host ''
58+
}
59+
#endregion Find prereleases to cleanup
3360

61+
#region Delete prereleases
62+
LogGroup "Delete prereleases for [$prereleaseName]" {
3463
foreach ($tag in $tagsToDelete) {
3564
Write-Host "Deleting prerelease: [$tag]"
3665
if ($whatIf) {
@@ -47,3 +76,4 @@ LogGroup "Cleanup prereleases for [$prereleaseName]" {
4776

4877
Write-Host "::notice::Cleaned up $($tagsToDelete.Count) prerelease(s) for [$prereleaseName]."
4978
}
79+
#endregion Delete prereleases

0 commit comments

Comments
 (0)