Commit 1653be8
🪲 [Fix]: Release tags keep the configured version prefix (#440)
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:
```yaml
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.
---
<details>
<summary>Technical details</summary>
**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](https://github.com/PSModule/Process-PSModule/actions/runs/30759608449)
and is reported as a one-time measurement, not as coverage this change
carries forward.
```text
Module name: [PSModuleTest]
Version prefix: [v]
WhatIf: [True]
```
```text
ModuleVersion : 6.1.16
VersionPrefix : v
Prerelease : fixversionprefixreleasetag001
CreatePrerelease : True
ReleaseTag : v6.1.16-fixversionprefixreleasetag001
```
```text
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:
```text
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](#439 (comment)):
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.
</details>
<details>
<summary>Relevant issues (or links)</summary>
- Fixes #439
- #436
- #438
- #441
- #442
- #443
</details>
---------
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>1 parent 688896d commit 1653be8
5 files changed
Lines changed: 296 additions & 6 deletions
File tree
- .github
- actions/Publish-PSModule
- src
- tests
- workflows
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
40 | 47 | | |
41 | 48 | | |
42 | 49 | | |
| |||
65 | 72 | | |
66 | 73 | | |
67 | 74 | | |
| 75 | + | |
68 | 76 | | |
Lines changed: 104 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
| 31 | + | |
31 | 32 | | |
32 | 33 | | |
33 | 34 | | |
| |||
58 | 59 | | |
59 | 60 | | |
60 | 61 | | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
61 | 65 | | |
62 | | - | |
63 | | - | |
64 | | - | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
65 | 70 | | |
66 | 71 | | |
67 | 72 | | |
| |||
129 | 134 | | |
130 | 135 | | |
131 | 136 | | |
132 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
133 | 142 | | |
134 | 143 | | |
135 | 144 | | |
| 145 | + | |
136 | 146 | | |
137 | 147 | | |
| 148 | + | |
138 | 149 | | |
139 | 150 | | |
140 | 151 | | |
| |||
154 | 165 | | |
155 | 166 | | |
156 | 167 | | |
157 | | - | |
158 | 168 | | |
159 | 169 | | |
160 | 170 | | |
| |||
280 | 290 | | |
281 | 291 | | |
282 | 292 | | |
283 | | - | |
| 293 | + | |
Lines changed: 167 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
53 | 54 | | |
54 | 55 | | |
55 | 56 | | |
| |||
0 commit comments