Skip to content

💥 [Major]: Drop version calculation, publish pre-stamped artifact, upload module zip to release (PSModule/Process-PSModule#326)#71

Open
Marius Storhaug (MariusStorhaug) wants to merge 1 commit into
mainfrom
feat/326-resolve-psmodule-version
Open

💥 [Major]: Drop version calculation, publish pre-stamped artifact, upload module zip to release (PSModule/Process-PSModule#326)#71
Marius Storhaug (MariusStorhaug) wants to merge 1 commit into
mainfrom
feat/326-resolve-psmodule-version

Conversation

@MariusStorhaug
Copy link
Copy Markdown
Member

@MariusStorhaug Marius Storhaug (MariusStorhaug) commented May 17, 2026

Companion to PSModule/Process-PSModule#342 (PSModule/Process-PSModule#326).

⚠️ BREAKING CHANGE — v3.0.0

Publish-PSModule no longer calculates the next version or mutates the module manifest. The artifact passed in must already contain the final ModuleVersion (and Prerelease tag, if any). Use PSModule/Resolve-PSModuleVersion to compute the version and PSModule/Build-PSModule v5+ to stamp it before testing.

Removed inputs

  • AutoPatching
  • IncrementalPrerelease
  • DatePrereleaseFormat
  • VersionPrefix
  • MajorLabels, MinorLabels, PatchLabels, IgnoreLabels
  • ReleaseType

Why

Previously the version a module is published with was calculated after tests passed, inside Publish-PSModule. Build-PSModule stamped 999.0.0, the tests ran against that, and Publish-PSModule then rewrote the manifest before pushing to the Gallery. This breaks CI/CD's first principle: the artifact you tested is not the artifact you ship.

What

  • Deletes src/init.ps1 (the old version-calculation script).
  • src/publish.ps1 now reads ModuleVersion and Prerelease directly from the downloaded manifest, then publishes the artifact untouched via Publish-PSResource.
  • After gh release create, the module folder is zipped (<Name>-<Version>.zip) and uploaded as a release asset via gh release upload <tag> <zip> --clobber so the GitHub Release page exposes the exact bytes that were tested and pushed to the Gallery (fulfils the second half of Move version calculation to a Plan job (Resolve-PSModuleVersion) so Build and Publish never calculate or mutate versions Process-PSModule#326).
  • src/cleanup.ps1 no longer relies on the removed PUBLISH_CONTEXT_* environment variables; the prerelease name is derived from the PR head ref.
  • action.yml drops the version-calculation inputs listed above and the corresponding Initialize step.
  • README rewritten with a migration note.

Migration

Consumers on PSModule/Process-PSModule get this for free — the workflow has been updated end-to-end in the companion PR. Direct callers of Publish-PSModule outside of Process-PSModule need to provide a pre-stamped artifact (use Resolve-PSModuleVersionBuild-PSModule v5+ on the way in).

Related PRs

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Major-version bump that removes all version-calculation responsibility from Publish-PSModule. The action now consumes a pre-stamped module artifact (built by Build-PSModule v5+ after Resolve-PSModuleVersion), publishes it untouched to the PSGallery, creates a matching GitHub Release, and uploads the module as a zip asset so the released bytes match the tested bytes. The downstream Cleanup Prereleases step is reworked to derive the prerelease name from the PR head ref instead of stale PUBLISH_CONTEXT_* env vars.

Changes:

  • Delete src/init.ps1 and remove all version/label/release-type inputs from action.yml.
  • Rewrite src/publish.ps1 to read ModuleVersion/Prerelease from the manifest, publish via Publish-PSResource, and upload <Name>-<Version>.zip to the release.
  • Update src/cleanup.ps1 to derive $prereleaseName from the PR head ref and call gh release list itself.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
src/publish.ps1 Reads stamped version from manifest, no longer mutates it; adds zip-and-upload to release.
src/init.ps1 Deleted; version calculation moved out of this action.
src/cleanup.ps1 Self-contained: derives prerelease name from PR head ref and lists releases via gh.
action.yml Drops 9 version-calculation inputs and the Initialize step; updates cleanup gating.
README.md Rewrites docs with v3.0.0 migration note and inputs table.
Comments suppressed due to low confidence (1)

src/cleanup.ps1:40

  • $prereleaseName is derived from the PR head ref using a broad pattern (-like "*$prereleaseName*"). If the sanitized branch name is short or generic (e.g. a branch named fix or dev), this will match unrelated release tags (e.g. 1.2.3-fixes-001) and delete them. The previous implementation suffered the same risk, but the new model makes it easier to trigger because the pattern is computed every run from the branch name without any safeguards. Consider anchoring the match to the prerelease tag segment (e.g. -$prereleaseName with a numeric suffix) instead of a wildcard substring.
    $prereleasesToCleanup = $releases | Where-Object { $_.tagName -like "*$prereleaseName*" }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/cleanup.ps1
Comment on lines +34 to +41
$releases = gh release list --json 'createdAt,isDraft,isLatest,isPrerelease,name,publishedAt,tagName' | ConvertFrom-Json
if ($LASTEXITCODE -ne 0) {
Write-Error 'Failed to list releases for the repository.'
exit $LASTEXITCODE
}

$prereleasesToCleanup = $releases | Where-Object { $_.tagName -like "*$prereleaseName*" }
$tagsToDelete = @($prereleasesToCleanup | ForEach-Object { $_.tagName } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
Comment thread action.yml
Comment on lines 78 to +79
- name: Cleanup Prereleases
if: env.PUBLISH_CONTEXT_ShouldCleanup == 'true' || inputs.WhatIf == 'true'
if: inputs.AutoCleanup == 'true' || inputs.WhatIf == 'true'
Comment thread src/publish.ps1
Comment on lines +194 to +209
if (Test-Path -Path $zipPath) {
Remove-Item -Path $zipPath -Force
}
Write-Host "Compressing module to [$zipPath]"
Compress-Archive -Path (Join-Path $modulePath '*') -DestinationPath $zipPath -Force

if ($whatIf) {
Write-Host "WhatIf: gh release upload $releaseTag $zipPath --clobber"
} else {
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"
}
Comment thread src/publish.ps1
Comment on lines +200 to +208
if ($whatIf) {
Write-Host "WhatIf: gh release upload $releaseTag $zipPath --clobber"
} else {
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"
Comment thread src/publish.ps1
Remove-Item -Path $zipPath -Force
}
Write-Host "Compressing module to [$zipPath]"
Compress-Archive -Path (Join-Path $modulePath '*') -DestinationPath $zipPath -Force
Comment thread src/publish.ps1
Set-ModuleManifest -Path $manifestFilePath -Prerelease $($newVersion.Prerelease) -Verbose:$false
Show-FileContent -Path $manifestFilePath

$manifest = Test-ModuleManifest -Path $manifestFilePath -ErrorAction Stop
Comment thread README.md
Comment on lines +42 to +52
| Name | Description | Required | Default |
| -------------------------- | ------------------------------------------------------------------------------------------ | -------- | ---------------- |
| `Name` | Name of the module. Defaults to the repository name. | No | Repository name |
| `ModulePath` | Path to the downloaded module folder. | No | `outputs/module` |
| `APIKey` | PowerShell Gallery API key. | Yes | |
| `AutoCleanup` | Delete prerelease tags matching the PR branch after a stable release. | No | `true` |
| `WhatIf` | Log the changes that would be made without publishing, creating, or deleting anything. | No | `false` |
| `WorkingDirectory` | The working directory where the script will run from. | No | `.` |
| `UsePRTitleAsReleaseName` | Use the PR title as the release name (otherwise the version string is used). | No | `false` |
| `UsePRBodyAsReleaseNotes` | Use the PR body as the release notes (otherwise `--generate-notes` is used). | No | `true` |
| `UsePRTitleAsNotesHeading` | Prefix the release notes with the PR title as an H1 heading linking to the PR. | No | `true` |
Comment thread action.yml
Comment on lines 59 to 76
@@ -126,7 +76,7 @@ runs:
run: ${{ github.action_path }}/src/publish.ps1
Marius Storhaug (MariusStorhaug) added a commit to PSModule/Resolve-PSModuleVersion that referenced this pull request May 22, 2026
…tion (#1)

Module version resolution is now available as a standalone action.
Workflows can call it before building so the resolved version is stamped
into the artifact at build time, making the bytes that are tested the
bytes that ship.

- Resolves PSModule/Process-PSModule#326

## New: Standalone `Resolve-PSModuleVersion` action

The action consumes the JSON `Settings` output from
[`PSModule/Get-PSModuleSettings`](https://github.com/PSModule/Get-PSModuleSettings)
and emits:

| Output | Description |
| --- | --- |
| `Version` | `Major.Minor.Patch` portion of the resolved version. |
| `Prerelease` | Prerelease tag, empty when not a prerelease. |
| `FullVersion` | Full version string including `VersionPrefix` and
prerelease tag. |
| `ReleaseType` | `Release`, `Prerelease`, or `None` when no version
bump label is present. |
| `CreateRelease` | `true` when a release or prerelease should be
created. |

Typical usage in the Plan job:

```yaml
- name: Resolve module version
  id: resolve
  uses: PSModule/Resolve-PSModuleVersion@v1
  env:
    GH_TOKEN: ${{ github.token }}
  with:
    Settings: ${{ steps.settings.outputs.Settings }}

- name: Build module
  uses: PSModule/Build-PSModule@v5
  with:
    Version: ${{ steps.resolve.outputs.Version }}
    Prerelease: ${{ steps.resolve.outputs.Prerelease }}
```

The action validates `Settings.Publish.Module.ReleaseType`, applies
`IgnoreLabels` overrides, picks the bump type from PR labels
(`MajorLabels` > `MinorLabels` > `PatchLabels` / `AutoPatching`), then
computes the next version from the higher of the latest GitHub Release
and the latest PowerShell Gallery version. For prereleases it appends
the sanitized branch name, optional `DatePrereleaseFormat` timestamp,
and an incremental counter calculated from existing prereleases on the
same baseline + branch.

## Technical Details

- `action.yml`: composite action with inputs `Settings` (required JSON),
`Name`, `WorkingDirectory`, `Debug`, `Verbose`, `Version`, `Prerelease`,
plus `EventPath` and `EventJson` (both optional, for test overrides —
`EventJson` takes precedence over reading the file at `EventPath`). All
`${{ }}` template expressions are isolated in `env:` sections per zizmor
template-injection requirements. Installs
`PSModule/Install-PSModuleHelpers` and `PSSemVer` before running the
script.
- `scripts/main.ps1`: ports the version-resolution logic that previously
lived in `Publish-PSModule/src/init.ps1`. Reads configuration from
`PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_Settings` JSON instead of
separate env vars. Reads the PR event from
`PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_EventJson` when set, falling
back to the file at `GITHUB_EVENT_PATH`. Emits outputs via
`$env:GITHUB_OUTPUT`. Cleanup-tag discovery stays in
`Publish-PSModule/cleanup.ps1` and is intentionally out of scope here.
- `.github/workflows/Action-Test.yml`: 6 test jobs covering patch,
minor, major, auto-patch, ignore-label, and None scenarios. The
ignore-label job passes the fake PR event as a JSON string via
`EventJson` to bypass the runner's real event file, which cannot be
reliably overridden at the file-system level.
- `README.md`: replaces the template scaffold with the action's contract
and usage examples.

**Implementation plan progress** (PSModule/Process-PSModule#326):
- ✅ Create `Resolve-PSModuleVersion` (LICENSE, README, `action.yml`,
`scripts/main.ps1`, Action-Test workflow)
- ✅ Inputs: `Settings`, `Name`, `WorkingDirectory` (plus
`EventPath`/`EventJson` for test overrides)
- ✅ Outputs: `Version`, `Prerelease`, `FullVersion`, `ReleaseType`,
`CreateRelease`
- ✅ Port version-resolution logic from `Publish-PSModule/src/init.ps1`
(PSSemVer install, GitHub Releases query, PSGallery query, PR-label
parsing, bump selection, prerelease sequencing, `DatePrereleaseFormat`,
`VersionPrefix`)
- ⬜ Dedicated Pester unit tests for label parsing, bump selection, and
prerelease sequencing — covered by the six integration test jobs; a
focused unit-test suite remains open

Related PRs:
- PSModule/Process-PSModule#342 — rewires the workflow's Plan → Build →
Test → Publish chain to consume the resolved version.
- PSModule/Build-PSModule#136 — accepts `Version` / `Prerelease` inputs
and stamps them into the manifest at build time.
- PSModule/Publish-PSModule#71 — removes the version-calculation logic
that moved here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants