Skip to content

🪲 [Fix]: Release tags keep the configured version prefix - #440

Merged
Marius Storhaug (MariusStorhaug) merged 7 commits into
mainfrom
fix-version-prefix-release-tag
Aug 3, 2026
Merged

🪲 [Fix]: Release tags keep the configured version prefix#440
Marius Storhaug (MariusStorhaug) merged 7 commits into
mainfrom
fix-version-prefix-release-tag

Conversation

@MariusStorhaug

@MariusStorhaug Marius Storhaug (MariusStorhaug) commented Aug 2, 2026

Copy link
Copy Markdown
Member

Releases created by the module pipeline are tagged with the version prefix configured in .github/PSModule.yml again. A repository that keeps the default VersionPrefix: 'v' is tagged v1.1.10, not 1.1.10, and its prereleases are tagged v1.1.11-mybranch001. A repository that sets VersionPrefix: '' keeps its unprefixed tags exactly as before.

Fixed: release tags no longer lose the configured version prefix

Since v6 the release tag was built from the compiled manifest's ModuleVersion alone. That value is Major.Minor.Patch by definition, so the prefix had nowhere to live and every repository publishing on v6 with the default prefix picked up a tag that did not match its own history — PSModule/Toml went from v0.0.1 to 0.0.2, PSModule/Domeneshop from v0.0.2 to 1.0.0, PSModule/PSSemVer from v1.1.9 to 1.1.10.

Nothing needs to change in a module repository. The prefix is read from the setting that already exists:

Publish:
  Module:
    VersionPrefix: 'v'   # default; set to '' for unprefixed tags

The prefix applies to the GitHub release tag and to nothing else. A PowerShell module manifest's ModuleVersion and a PowerShell Gallery package version only accept plain SemVer, so the version published to the Gallery, the version in the Gallery link, and the name of the module zip attached to the release all stay unprefixed. With VersionPrefix: 'v' a release looks like this:

Value
GitHub release tag and title v1.1.10
PowerShell Gallery version 1.1.10
Manifest ModuleVersion 1.1.10
Attached artifact MyModule-1.1.10.zip

Repositories that already published an unprefixed tag on v6 keep it. Those releases are public, their artifacts are linked from the release pages, and the PowerShell Gallery listing points at them, so they are left alone and the prefix resumes from the next release. An unprefixed tag left in the history does not affect future version resolution.


Technical details

What changed

  • .github/actions/Publish-PSModule/src/Publish-PSModule.Helpers.psm1 — new action-scoped helper module with Get-ModuleVersionString, which composes the module's SemVer string, and Get-ReleaseTag, which prefixes it.
  • .github/actions/Publish-PSModule/src/publish.ps1 — reads the new VersionPrefix input, derives both version strings from those helpers in one place, and reports both in the resolved-version summary and the closing log line.
  • .github/actions/Publish-PSModule/action.yml — new optional VersionPrefix input, defaulting to ''.
  • .github/workflows/Publish-Module.yml — passes Settings.Publish.Module.VersionPrefix into the action.
  • .github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 — new Pester suite, picked up automatically by the existing Test-Actions discovery over .github/actions/*/tests.

Five files, all on the tag-derivation path. No test fixture or pipeline behaviour outside it changes.

Approach

The manifest stays the source of the numeric version. ModuleVersion is what was built, tested, and pushed to the Gallery, so the tag has to agree with it — the ^\d+\.\d+\.\d+$ guard and the 999.0.0 placeholder check are unchanged. Only the prefix, the one piece of the tag the manifest cannot carry, is taken from the settings the Plan job already resolves. Composing the two is equivalent to using Resolution.FullVersion in the normal case and stays anchored to the artifact if the two ever disagree.

Keeping the prefix off the module version. publish.ps1 previously built the Gallery version with its own copy of the prerelease composition, independent of the tag. Two independent implementations of the same string is how they drift, and drift in this direction means a prefixed version reaching Publish-PSResource. Both now come from Get-ModuleVersionString; Get-ReleaseTag is that string with the prefix in front, so the prefix is the only possible difference between them, by construction rather than by convention.

The prefix reaches: the release tag, the release title fallback, the gh release upload target, the release URL, the GitHub half of the pull request comment, and PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag for cleanup. It reaches nothing else. publish.ps1 never writes to the manifest — it is read-only on the artifact by design — and Build-PSModule stamps the manifest from Resolution.Version and Resolution.Prerelease, which are unprefixed. Resolution.FullVersion, the one prefix-bearing value in the Settings object, is consumed by no downstream job.

Get-ModuleVersionString and Get-ReleaseTag both trim their inputs, because prefix and label arrive through environment variables, and both treat a whitespace-only prerelease label as a stable release.

Verification — unit tests, red then green in CI

Commit Change Test actions
fd2c7d9 Extract tag derivation into a helper, no behavior change success
d0f8f7a Add the regression test failure (13 of 15)
06b4714 Apply the configured version prefix success (15 of 15)

32 cases now. Alongside the prefixed and unprefixed tag shapes, absent and null prefixes, whitespace normalization, and the tag shape Cleanup-PSModulePrereleases depends on, a the prefix reaches the release tag and nothing else context pins the separation directly:

  • for five prefix/version/label combinations, the tag equals $Prefix + the module version string;
  • the module version string never begins with the prefix and always matches ^\d+\.\d+\.\d+(-[0-9A-Za-z\-.]+)?$;
  • Get-ModuleVersionString has no VersionPrefix parameter at all, so a caller cannot pass one;
  • an unprefixed repository gets two identical strings, and stripping v from a prefixed tag returns the module version string.

Verification — the wiring, observed once in CI on an interim commit

This bug was a wiring failure, not a logic failure. VersionPrefix was resolved correctly by the Plan job and present in the Settings JSON; it simply never reached the tag. Unit tests prove the helpers compose correctly given the right input — they cannot prove that fromJson(inputs.Settings).Publish.Module.VersionPrefix → action input → PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix$versionPrefix delivers the value.

Publish-Module is skipped at the job level in every self-test run — Publish.Module.Enabled is (ReleaseType -ne 'None') -or shouldAutoCleanup, and an open pull request satisfies neither without a prerelease label — so the self-test does not exercise that chain on this diff.

To close that gap once, an interim commit on this branch added Fix to a fixture's PrereleaseLabels, which made the self-test run the publish path under WhatIf. That commit has since been reset and is not part of this pull request; the observation below is from run 30759608449 and is reported as a one-time measurement, not as coverage this change carries forward.

Module name:    [PSModuleTest]
Version prefix: [v]
WhatIf:         [True]
ModuleVersion    : 6.1.16
VersionPrefix    : v
Prerelease       : fixversionprefixreleasetag001
CreatePrerelease : True
ReleaseTag       : v6.1.16-fixversionprefixreleasetag001
WhatIf: gh release create v6.1.16-fixversionprefixreleasetag001 --title v6.1.16-fixversionprefixreleasetag001 --notes-file /tmp/tmpjar7Eu.tmp --target fix-version-prefix-release-tag --prerelease

Nothing was published in that run: Publish-PSResource was logged rather than executed, no release or tag was created, and the Release workflow on the same push reported Create a prerelease: [False] / Skipping release creation.

Standing publish-path coverage in CI is the subject of #436.

Verification — locally, outside CI

publish.ps1 run end to end in WhatIf mode against a fabricated artifact, all four combinations, re-run after the separation change. Every line below is from those runs:

Prefix Prerelease Gallery version Release tag Artifact Exported …_CONTEXT_ReleaseTag
v 1.1.10 v1.1.10 PSModuleTest-1.1.10.zip v1.1.10
v mybranch001 1.1.11-mybranch001 v1.1.11-mybranch001 PSModuleTest-1.1.11-mybranch001.zip v1.1.11-mybranch001
'' 1.1.10 1.1.10 PSModuleTest-1.1.10.zip 1.1.10
'' mybranch001 1.1.11-mybranch001 1.1.11-mybranch001 PSModuleTest-1.1.11-mybranch001.zip 1.1.11-mybranch001

The first row is the PSModule/PSSemVer case from the bug report, which produced tag 1.1.10 before this change. The Gallery link and comment carried the unprefixed version in every run:

Publishing complete. PowerShell Gallery version: [1.1.10]. GitHub release tag: [v1.1.10].
gh pr comment 42 -b '✅ New release: PowerShell Gallery - [PSModuleTest 1.1.10](https://www.powershellgallery.com/packages/PSModuleTest/1.1.10)'

AutoCleanup was verified the same way, running cleanup.ps1 against fixture release lists with the gh CLI shadowed. It keys off tagName -like "*$prereleaseName*", which is prefix-agnostic, and excludes the published release by comparing tagName to PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag, which is now prefixed on both sides:

Repository Published tag Deleted Excluded
Prefixed v1.1.11-mybranch003 v1.1.11-mybranch002, v1.1.11-mybranch001 published tag, v1.1.10, another branch's prerelease
Unprefixed 1.1.11-mybranch003 1.1.11-mybranch002, 1.1.11-mybranch001 published tag, 1.1.10, another branch's prerelease
Mixed history — unprefixed leftovers from v6 v1.1.11-mybranch003 1.1.11-mybranch002, 1.1.11-mybranch001 published tag
Stable release run v1.1.11 all three branch prereleases published tag

The mixed row is the migration case: a repository whose open pull request already has unprefixed prerelease tags created by the current v6 still has them cleaned up after this change.

Invoke-ScriptAnalyzer -Recurse -Settings .github/linters/.powershell-psscriptanalyzer.psd1 reports no findings for the action.

What is verified where

Path Evidence
Settings → action input → env var → $versionPrefix One-time CI observation above; not covered by this diff going forward
Prefix reaches the release tag and nothing else Unit tests, plus all four local end-to-end runs
Prefixed stable and prerelease tags Unit tests, plus local end-to-end runs
Unprefixed stable and prerelease tags Unit tests, plus local end-to-end runs
AutoCleanup tag matching, prefixed / unprefixed / mixed history Local runs of cleanup.ps1 against fixture release lists

Implementation plan progress

Completes every step of the plan in #439 — regression test confirmed failing first, tag derivation changed, prefixed and unprefixed fixtures plus the AutoCleanup path re-run. The two decisions the issue records are answered in a comment on it: the already-published unprefixed tags are left in place, and the prefix-consistency warning is carried by #441 rather than widened into this pull request.

Standards and framework alignment

Changed surface Standards checked Framework docs checked Result
.github/actions/Publish-PSModule/src/** (PowerShell) PowerShell Functions, Naming, Messaging, Error Handling GitHub Actions — helper modules named after the action Aligned
.github/actions/Publish-PSModule/tests/** (Pester) Testing, PowerShell Testing Action unit-test discovery in Test-Actions.yml Aligned
.github/actions/Publish-PSModule/action.yml (GitHub Actions) GitHub Actions — behavior driven by inputs, defaults only at the interface layer Action contract Aligned
.github/actions/Publish-PSModule/ (folder conventions) GitHub Actions — entry script named main.ps1, README per action Action contract Exception — #442
.github/workflows/Publish-Module.yml (GitHub Actions) GitHub Actions Reusable workflow contract Aligned

The helper module is named after the action, per the standard, and matches the existing Resolve-PSModuleVersion.Helpers.psm1. The entry script here is src/publish.ps1 rather than src/main.ps1 and the action has no README; both predate this change and apply to Cleanup-PSModulePrereleases too, so they are carried by #442 instead of being renamed inside a bugfix.

Issue convergence sweep

Scoped to the publish and version-resolution surface: open issues in this repository touching Publish-PSModule, Resolve-PSModuleVersion, Cleanup-PSModulePrereleases, release tags, or versioning. #439 is the only one this diff fully satisfies. #438 (v5 to v6 migration) is affected by the fix but not delivered by it — the 48 repositories still on v5 need this merged and released before they migrate, so it is linked as context. #436 is linked as context only; this diff does not advance it. #441, #442, and #443 were opened by this session for findings deliberately left out of scope.

Relevant issues (or links)

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The test asserts the tag a repository with VersionPrefix 'v' expects, and keeps an unprefixed repository covered. It fails against the current derivation, which builds the tag from the manifest's Major.Minor.Patch only.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The Plan job resolves Publish.Module.VersionPrefix, but the publish action never received it, so the tag was built from the manifest's Major.Minor.Patch alone and repositories on the default 'v' prefix lost it. The prefix now flows from Settings through the action input into the tag.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Super-linter summary

Language Validation result
CHECKOV Pass ✅
GITLEAKS Pass ✅
GIT_MERGE_CONFLICT_MARKERS Pass ✅
MARKDOWN Pass ✅
NATURAL_LANGUAGE Pass ✅
POWERSHELL Pass ✅
PRE_COMMIT Pass ✅
SPELL_CODESPELL Pass ✅
TRIVY Pass ✅
YAML Pass ✅

All files and directories linted successfully

For more information, see the GitHub Actions workflow run

Powered by Super-linter

The publish job was skipped in every self-test run, so the Settings to action input to environment variable hop was never exercised in CI. That is the hop this pull request repairs. Adding 'Fix' to the Default fixture's PrereleaseLabels resolves ReleaseType to Prerelease for the framework's own bugfix pull requests, so Publish-PSModule runs under WhatIf and logs the tag it would create.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Restores the configured VersionPrefix when composing GitHub release tags in the Publish-PSModule pipeline, so repositories with historical vX.Y.Z tags continue tagging consistently (while keeping the module/gallery version unprefixed).

Changes:

  • Adds Get-ReleaseTag helper to compose tags from VersionPrefix + ModuleVersion (+ prerelease).
  • Wires Settings.Publish.Module.VersionPrefix through the reusable workflow into the Publish-PSModule action and publish.ps1.
  • Adds Pester coverage for Get-ReleaseTag and updates the self-test repo fixture to exercise the publish path in CI.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/srcTestRepo/.github/PSModule.yml Adjusts fixture prerelease labels so CI runs the publish path under WhatIf.
.github/workflows/Publish-Module.yml Passes Settings.Publish.Module.VersionPrefix into the publish action.
.github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 Adds unit tests for release tag composition behavior.
.github/actions/Publish-PSModule/src/publish.ps1 Imports helper, reads VersionPrefix input, and uses helper to build ReleaseTag.
.github/actions/Publish-PSModule/src/Publish-PSModule.Helpers.psm1 Introduces Get-ReleaseTag helper used by publish script.
.github/actions/Publish-PSModule/action.yml Adds VersionPrefix action input and passes it through to the script env var.

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

Comment thread .github/actions/Publish-PSModule/src/Publish-PSModule.Helpers.psm1 Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

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

  • Get-ReleaseTag allows $VersionPrefix to be $null ([AllowNull()]) and the unit tests explicitly call Get-ReleaseTag -VersionPrefix $null ..., but the implementation calls $VersionPrefix.Trim() unconditionally. This throws "You cannot call a method on a null-valued expression" and breaks publish/tagging when the input is omitted or null.
    $tag = "$($VersionPrefix.Trim())$ModuleVersion"

@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as ready for review August 2, 2026 17:43
@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as draft August 2, 2026 17:45
auto-merge was automatically disabled August 2, 2026 17:45

Pull request was converted to draft

Keeps the Default fixture at ReleaseType None so it stays available as the fixture that can exercise Cleanup-PSModulePrereleases, which is gated on ReleaseType != Prerelease. Both fixtures call the same workflow.yml, so the Settings to input to environment variable hop is proven identically either way.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

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

  • Get-ReleaseTag allows $VersionPrefix to be $null ([AllowNull()]) and the tests call it with $null, but the implementation does $VersionPrefix.Trim() which throws ("You cannot call a method on a null-valued expression"). This will also break publishing if the action input is omitted/unset and the env var is missing.

Handle $null by treating it as an empty string before trimming.

    $tag = "$($VersionPrefix.Trim())$ModuleVersion"

@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as ready for review August 2, 2026 17:55
Drops the PrereleaseLabels trigger, so the self-test returns to skipping Publish-Module at the job level and the pull request is limited to the tag-derivation path again.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as draft August 3, 2026 01:09
auto-merge was automatically disabled August 3, 2026 01:09

Pull request was converted to draft

The PowerShell Gallery and the module manifest only accept plain SemVer, so the prefix must not reach either. Both version strings now come from one composition, Get-ModuleVersionString, with Get-ReleaseTag adding the prefix on top, which removes the duplicated prerelease handling that could have drifted. The resolved-version summary and the closing log line report both strings so the separation is visible in the log.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

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

Suppressed comments (1)

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

  • Get-ReleaseTag calls $VersionPrefix.Trim() even when VersionPrefix is omitted or explicitly $null (both are exercised in the new tests). This will throw "You cannot call a method on a null-valued expression" and break publishing/tag creation for repos where the workflow doesn't pass a prefix (or passes null).
    "$($VersionPrefix.Trim())$(Get-ModuleVersionString -ModuleVersion $ModuleVersion -Prerelease $Prerelease)"

@MariusStorhaug
Marius Storhaug (MariusStorhaug) marked this pull request as ready for review August 3, 2026 01:17
@MariusStorhaug
Marius Storhaug (MariusStorhaug) merged commit 1653be8 into main Aug 3, 2026
73 checks passed
@MariusStorhaug
Marius Storhaug (MariusStorhaug) deleted the fix-version-prefix-release-tag branch August 3, 2026 01:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Restore the configured VersionPrefix on release tags created by Publish-PSModule

2 participants