Skip to content

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

Closed
PSModule/PSSemVer
#54

Description

Context and request

Observed behavior: since v6, Publish-PSModule derives the git tag from the compiled manifest's ModuleVersion instead of from the resolved FullVersion. ModuleVersion is Major.Minor.Patch by definition — the script even enforces ^\d+\.\d+\.\d+$ — so the configured VersionPrefix is dropped and the release is tagged without it.

.github/actions/Publish-PSModule/src/publish.ps1 (v6.1.15):

$moduleVersion = $manifestData.ModuleVersion
if (-not ($moduleVersion -match '^\d+\.\d+\.\d+$')) { ... }
...
$releaseTag = if ($createPrerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion }
...
$releaseCreateCommand = @('release', 'create', $releaseTag)

The Plan job resolves the prefix correctly and passes it through. From the PSSemVer publish run for PR PSModule/PSSemVer#46:

VersionPrefix         : v
GitHub version: [v1.1.9]
New version: [v1.1.10]
"Resolution":{"Version":"1.1.10","Prerelease":"","FullVersion":"v1.1.10","ReleaseType":"Release","CreateRelease":true}

FullVersion is v1.1.10; the tag created is 1.1.10.

Expected behavior: the release tag is the resolved FullVersion, so VersionPrefix: 'v' keeps producing v1.1.10 and a repository's tag history stays consistent.

Reproduction: merge any release-labelled pull request in a module repository pinned to v6 whose settings leave VersionPrefix at the default v, then compare the new tag to the previous one.

Environment: Process-PSModule v6.0.0 through v6.1.15; any module repository using the default VersionPrefix: 'v'.

Regression: yes. v5 produced prefixed tags. Three repositories have already published on v6 and every one of them lost the prefix at the v6 boundary:

Repository Last v5 tag First v6 tag
PSModule/Toml v0.0.1 0.0.2
PSModule/Domeneshop v0.0.2 1.0.0, 1.0.1
PSModule/PSSemVer v1.1.9 1.1.10

This looks like a side effect of #326, which made publish read the version from the manifest read-only. The manifest is the right source for the module version, but it cannot carry the tag prefix.

Workaround: none that preserves history. Setting VersionPrefix: '' makes the repository consistent going forward but abandons every existing v-prefixed tag.

Acceptance criteria:

  • With VersionPrefix: 'v', a stable release is tagged v<Major>.<Minor>.<Patch> and a prerelease is tagged v<Major>.<Minor>.<Patch>-<label>.
  • With VersionPrefix: '', tags stay unprefixed.
  • AutoCleanup still matches and removes the prerelease tags it created, in both prefixed and unprefixed repositories.
  • A regression test covers a prefixed repository, so the prefix cannot be dropped silently again.

Technical decisions

The root cause is bounded to the tag derivation in .github/actions/Publish-PSModule/src/publish.ps1. The manifest check should stay — it is a valid guard that the artifact was stamped — but $releaseTag should come from the Plan job's FullVersion output (or from Version plus the settings VersionPrefix), not from $moduleVersion. The ModuleVersion validation and the 999.0.0 placeholder check remain unchanged.

Two decisions are needed:

  1. Whether the already-published unprefixed tags are corrected. Retagging PSSemVer 1.1.10, Toml 0.0.2, and Domeneshop 1.0.0/1.0.1 would restore consistency, but the releases and their attached artifacts are already public, and 1.1.10 is the tag the PowerShell Gallery listing links to. Leaving them and fixing forward is the lower-risk option; either way the choice should be explicit.
  2. Whether prefix consistency is enforced. Resolve-PSModuleVersion's Get-LatestGitHubVersion reads tagName and Get-LatestPublishedVersion sorts a prefixed and an unprefixed [PSSemVer] together — CompareTo ignores Prefix, so equal versions sort arbitrarily and the prefix can be lost from the resolved latest version. That is survivable because the prefix is reapplied from settings afterwards, but a repository with mixed tags is worth a warning.

Blast radius grows with #438: 48 of 58 module repositories are still on v5 and will hit this the first time they publish after migrating.

Implementation plan

Add a regression test that publishes from a fixture repository configured with VersionPrefix: 'v' and asserts the created tag, confirm it fails against current main, change $releaseTag to use the resolved full version, re-run the prefixed and unprefixed fixtures plus the AutoCleanup path, then record the decision on the already-published tags.

Activity

  1. MariusStorhaug commented on Aug 2, 2026

    @MariusStorhaug
    MemberAuthor

    Implementation plan progress — all steps except the recorded decision are done in #440.

    1. Regression test added, confirmed failing against the current derivation. .github/actions/Publish-PSModule/tests/Publish-PSModule.Helpers.Tests.ps1 covers a prefixed repository (stable and prerelease), an unprefixed repository, prefix/label normalization, and the tag-shape contract Cleanup-PSModulePrereleases depends on. The Test-Actions workflow ran red on the test-only commit and green on the fix commit, on this branch:

      Commit Change Test actions
      fd2c7d9 Extract tag derivation into a helper, no behavior change success
      d0f8f7a Add the regression test failure
      06b4714 Apply the configured version prefix success
    2. $releaseTag now carries the prefix. The manifest stays the source of the numeric version — the ^\d+\.\d+\.\d+$ guard and the 999.0.0 placeholder check are untouched — and only the prefix, the one thing ModuleVersion cannot carry, comes from Settings.Publish.Module.VersionPrefix. Publish-Module.yml passes it into a new VersionPrefix input on the Publish-PSModule action.

    3. Prefixed and unprefixed paths re-run. publish.ps1 was executed end to end in WhatIf mode against a fabricated artifact for all four combinations. The PSSemVer case now produces gh release create v1.1.10 and exports PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag=v1.1.10; with VersionPrefix: '' it still produces 1.1.10. The PowerShell Gallery version and the attached zip name stay unprefixed.

    4. AutoCleanup re-verified. cleanup.ps1 was executed against fixture release lists with the gh CLI shadowed. It selects and excludes correctly for a prefixed repository, an unprefixed repository, and a repository whose history still holds the unprefixed prerelease tags v6 created before this fix — the *$prereleaseName* match is prefix-agnostic and the exclusion compares the exported tag to tagName, and both sides are now prefixed together.

    Decision 1 — already-published unprefixed tags

    Recommendation: leave them and fix forward. PSSemVer 1.1.10, Toml 0.0.2, and Domeneshop 1.0.0/1.0.1 are public releases with attached artifacts, and the PowerShell Gallery listing links to 1.1.10. Retagging breaks those links and the artifact URLs for no functional gain; the only cost of leaving them is one cosmetic gap in each repository's tag history. Nothing in the pipeline reads tags by prefix — Get-LatestGitHubVersion reads tagName and parses it as a version — so an unprefixed tag in the history does not affect future version resolution.

    Decision 2 — prefix consistency enforcement

    Out of scope for this bounded fix; filed separately so this pull request stays reviewable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions