You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Build-PSModule stamps every built module manifest with ModuleVersion = '999.0.0' as a placeholder.
The artifact is uploaded with this fake version, and all tests run against 999.0.0.
Later, Publish-PSModule (publish.ps1) calls Set-ModuleManifest to patch the real version into the manifest after all tests have passed.
# publish.ps1 mutates the artifact that was testedSet-ModuleManifest-Path $manifestFilePath-ModuleVersion $manifestNewVersionSet-ModuleManifest-Path $manifestFilePath-Prerelease $($newVersion.Prerelease)
This means the artifact that was tested (999.0.0) is not the artifact that ships (for example 1.4.0).
That violates a core CI/CD principle: test what you ship.
Version calculation is also duplicated across Publish-PSModule/init.ps1 and Publish-PSModule/publish.ps1.
Moving version decision logic into one planning point simplifies maintenance and clarifies responsibilities.
Current Pipeline
Get-Settings
- reads configuration
- outputs settings
Build-PSModule
- stamps ModuleVersion = '999.0.0' (placeholder)
- uploads artifact
Test-Module / Test-ModuleLocal
- tests the 999.0.0 artifact (not the version that ships)
Publish-PSModule (init.ps1)
- queries GitHub Releases + PSGallery
- reads PR labels
- calculates new version (for example v1.4.0)
Publish-PSModule (publish.ps1)
- downloads artifact
- patches ModuleVersion in manifest (mutates tested artifact)
- publishes mutated artifact to PSGallery
- creates GitHub release
Desired Pipeline
Plan (single job, two steps)
- Get-PSModuleSettings
reads configuration and emits settings
- Resolve-PSModuleVersion
reads settings + PR labels
queries GitHub Releases + PSGallery
outputs Version / Prerelease / FullVersion / ReleaseType / CreateRelease
Build-PSModule
- receives Version + Prerelease from Plan
- stamps real version into manifest
- uploads immutable artifact
Test-Module / Test-ModuleLocal
- tests the same artifact that will be shipped
Publish-PSModule
- downloads artifact (already correctly versioned)
- publishes to PSGallery as-is
- creates GitHub release
flowchart LR
A[Plan<br/>Get-Settings + Resolve-PSModuleVersion] --> B[Build-Module<br/>stamps version into manifest]
B --> C[Test-* / Coverage]
C --> D[Publish-Module<br/>publishes artifact + uploads zip to release]
Loading
Why This Matters
Concern
Current
Desired
Artifact integrity
Tested != Published
Tested = Published
Version decision point
Scattered in Publish (init.ps1 + publish.ps1)
Single point in Plan (Resolve-PSModuleVersion)
Publish complexity
Calculates version, patches manifest, publishes
Publishes already-versioned artifact
Maintainability
Version logic in multiple scripts
Version logic in one action
Traceability
Artifact version not final until publish
Artifact version is authoritative
Technical Decisions
Inputs Are Available At Planning Time
Everything needed to calculate the next version is available in the Plan job:
PR labels (major / minor / patch)
ReleaseType (Release / Prerelease / None) from settings
Latest GitHub release (gh release list)
Latest PSGallery version (Find-PSResource)
Single Decision Point In One Job
The Plan job contains two steps:
Get-PSModuleSettings loads and resolves settings.
Resolve-PSModuleVersion consumes settings + GitHub context and outputs:
Version
Prerelease
FullVersion
ReleaseType
CreateRelease
The Plan job surfaces both settings and version outputs for downstream jobs.
Build And Publish Become Executors
Build-PSModule stamps version information into the manifest and uploads the artifact.
Publish-PSModule publishes that artifact without mutating version metadata.
Consumer Compatibility
Process-PSModule/.github/workflows/workflow.yml keeps the same public input/output contract for external consumers.
This refactor is internal wiring.
Action-Level Breaking Change
Publish-PSModule removes version-calculation inputs and becomes a publish-only action.
That action receives a major version bump.
Direct users of Publish-PSModule must run Resolve-PSModuleVersion first.
Prerelease Counter
Prerelease sequencing keeps the same race characteristics as current behavior (no regression introduced by this plan).
Remove manifest mutation (Set-ModuleManifest) from src/publish.ps1.
Read version from downloaded manifest (Test-ModuleManifest + prerelease metadata).
Remove version-calculation inputs from action.yml.
Fix cleanup regression: snapshot gh release listbeforepublish.ps1 runs so the just-published release tag is not included in $tagsToDelete (PSModule/Publish-PSModule#71 review).
Fix Compress-Archive path: zip $modulePath (the folder) not $modulePath/* (its contents) so the archive extracts to a <Name>/ directory (PSModule/Publish-PSModule#71 review).
Add Resolve-PSModuleVersion as second step in Plan.
Expose Plan outputs: Settings, Version, Prerelease, FullVersion, ReleaseType, CreateRelease.
Update Build-Module.yml to pass Version and Prerelease into Build-PSModule.
Update Publish-Module.yml to remove version-calculation inputs.
Update needs and with wiring for downstream jobs to consume Plan outputs.
Repin @main references in Build-Module.yml, Plan.yml, and Publish-Module.yml to released SHAs once all companion repos are released.
Documentation
Update Process-PSModule/README.md:
show Plan job with two steps
move version-bump explanation into Plan section
document artifact-integrity guarantee
[ ] Add Resolve-PSModuleVersion/README.md with inputs, outputs, and examples.(not needed ΓÇö all changes are internal to the packaged workflow)
[ ] Update Publish-PSModule/README.md migration notes for removed inputs.(not needed ΓÇö all changes are internal to the packaged workflow)
Test harness version-awareness (discovered 2026-07-08)
Because the built artifact now carries the real resolved version instead of the fixed 999.0.0 placeholder, the shared test and coverage tooling must stop assuming 999.0.0. On #342 the Plan and Build-Module jobs already succeed (the manifest is correctly stamped with the resolved version), but Test-Module, Test-ModuleLocal, Get-TestResults, and Build-Docs fail because the built module is installed into a 999.0.0 folder that no longer matches its manifest, so Import-Module rejects it as an invalid manifest.
End-to-end status (2026-07-08): with all four companion changes temporarily branch-pinned, the full Workflow-Test suite on #342 is green (Plan, Build-Module, Test-Module, Test-ModuleLocal, Get-CodeCoverage, Get-TestResults, Build-Docs — 61 pass / 7 publish-skipped / 0 fail). Remaining work is to merge + release the companion PRs and repin #342 to the released tags.
Merge order (post-approval — nothing merges without sign-off):
PSModule/Test-PSModule#120 — Pester 6 -ForEach fix. Independent (fixes Test-PSModule main, no version coupling); merge first.
Install-PSModuleHelpers: Install-PSModule (src/Helpers/Helpers.psm1) copies the built module into a hard-coded .../<name>/999.0.0 folder before Import-Module, which fails once the manifest carries the real version. Install into the folder that matches the manifest ModuleVersion (via the existing Get-ModuleManifest), then release and repin. Blocks Test-Module and Test-ModuleLocal. Status (2026-07-08): implemented in PSModule/Install-PSModuleHelpers#19 (own CI green; Copilot review converged; also hardened against path traversal from a malformed manifest version). Validated end-to-end on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 via a temporary branch pin — Test-ModuleLocal is green. Awaiting merge + release, then repin to the released tag.
Get-PesterCodeCoverage: coverage file paths are normalized by splitting on 999.0.0; derive the version dynamically so coverage paths resolve with real versions, then release and repin. Status (2026-07-08): likely not required for green — Get-CodeCoverage and Get-TestResults pass on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 with real versions. The split just no longer strips a prefix, so coverage file paths may be absolute rather than module-relative (cosmetic). To confirm/clean up as a follow-up.
This refactor centralizes version decisions in the Plan job (Resolve-PSModuleVersion), improving maintainability while enforcing tested-artifact equals shipped-artifact behavior.
Problem
Build-PSModulestamps every built module manifest withModuleVersion = '999.0.0'as a placeholder.The artifact is uploaded with this fake version, and all tests run against
999.0.0.Later,
Publish-PSModule(publish.ps1) callsSet-ModuleManifestto patch the real version into the manifest after all tests have passed.This means the artifact that was tested (
999.0.0) is not the artifact that ships (for example1.4.0).That violates a core CI/CD principle: test what you ship.
Version calculation is also duplicated across
Publish-PSModule/init.ps1andPublish-PSModule/publish.ps1.Moving version decision logic into one planning point simplifies maintenance and clarifies responsibilities.
Current Pipeline
Desired Pipeline
Why This Matters
init.ps1+publish.ps1)Resolve-PSModuleVersion)Technical Decisions
Inputs Are Available At Planning Time
Everything needed to calculate the next version is available in the Plan job:
major/minor/patch)ReleaseType(Release/Prerelease/None) from settingsgh release list)Find-PSResource)Single Decision Point In One Job
The Plan job contains two steps:
Get-PSModuleSettingsloads and resolves settings.Resolve-PSModuleVersionconsumes settings + GitHub context and outputs:VersionPrereleaseFullVersionReleaseTypeCreateReleaseThe Plan job surfaces both settings and version outputs for downstream jobs.
Build And Publish Become Executors
Build-PSModulestamps version information into the manifest and uploads the artifact.Publish-PSModulepublishes that artifact without mutating version metadata.Consumer Compatibility
Process-PSModule/.github/workflows/workflow.ymlkeeps the same public input/output contract for external consumers.This refactor is internal wiring.
Action-Level Breaking Change
Publish-PSModuleremoves version-calculation inputs and becomes a publish-only action.That action receives a major version bump.
Direct users of
Publish-PSModulemust runResolve-PSModuleVersionfirst.Prerelease Counter
Prerelease sequencing keeps the same race characteristics as current behavior (no regression introduced by this plan).
Implementation Plan
Resolve-PSModuleVersion (new action repo)
Implemented in PSModule/Resolve-PSModuleVersion#1 ΓÇö merged.
PSModule/Resolve-PSModuleVersion(LICENSE, README,action.yml,src/main.ps1, tests).Settings,Name,WorkingDirectory.Version,Prerelease,FullVersion,ReleaseType,CreateRelease.Publish-PSModule/src/init.ps1:PSSemVer.major/minor/patch/ignore).DatePrereleaseFormatandVersionPrefix.Build-PSModule changes
Implemented in PSModule/Build-PSModule#136 ΓÇö merged.
VersionandPrereleaseinputs.ModuleVersionbehavior with:'999.0.0'fallback when not providedPublish-PSModule changes
Implemented in PSModule/Publish-PSModule#71 ΓÇö merged.
src/init.ps1.Set-ModuleManifest) fromsrc/publish.ps1.Test-ModuleManifest+ prerelease metadata).action.yml.gh release listbeforepublish.ps1runs so the just-published release tag is not included in$tagsToDelete(PSModule/Publish-PSModule#71 review).Compress-Archivepath: zip$modulePath(the folder) not$modulePath/*(its contents) so the archive extracts to a<Name>/directory (PSModule/Publish-PSModule#71 review).Compress-Archiveinsideif (-not $whatIf)so WhatIf mode does not write a real zip file (PSModule/Publish-PSModule#71 review).$manifest.Version.Build -ge 0before constructing the release tag to catch 2-part version strings (PSModule/Publish-PSModule#71 review).cleanup.ps1behavior unchanged (gate cleanup on stable-release-only runs, not every invocation).Process-PSModule workflow changes
Work in progress in PSModule/Process-PSModule#342 ΓÇö draft, awaiting end-to-end validation.
Get-Settingsjob toPlan.Get-PSModuleSettingsas first step inPlan.Resolve-PSModuleVersionas second step inPlan.Settings,Version,Prerelease,FullVersion,ReleaseType,CreateRelease.Build-Module.ymlto passVersionandPrereleaseintoBuild-PSModule.Publish-Module.ymlto remove version-calculation inputs.needsandwithwiring for downstream jobs to consume Plan outputs.@mainreferences inBuild-Module.yml,Plan.yml, andPublish-Module.ymlto released SHAs once all companion repos are released.Documentation
Process-PSModule/README.md:[ ] Add(not needed ΓÇö all changes are internal to the packaged workflow)Resolve-PSModuleVersion/README.mdwith inputs, outputs, and examples.[ ] Update(not needed ΓÇö all changes are internal to the packaged workflow)Publish-PSModule/README.mdmigration notes for removed inputs.Test harness version-awareness (discovered 2026-07-08)
Because the built artifact now carries the real resolved version instead of the fixed
999.0.0placeholder, the shared test and coverage tooling must stop assuming999.0.0. On #342 thePlanandBuild-Modulejobs already succeed (the manifest is correctly stamped with the resolved version), butTest-Module,Test-ModuleLocal,Get-TestResults, andBuild-Docsfail because the built module is installed into a999.0.0folder that no longer matches its manifest, soImport-Modulerejects it as an invalid manifest.End-to-end status (2026-07-08): with all four companion changes temporarily branch-pinned, the full
Workflow-Testsuite on #342 is green (Plan, Build-Module, Test-Module, Test-ModuleLocal, Get-CodeCoverage, Get-TestResults, Build-Docs — 61 pass / 7 publish-skipped / 0 fail). Remaining work is to merge + release the companion PRs and repin #342 to the released tags.Merge order (post-approval — nothing merges without sign-off):
-ForEachfix. Independent (fixes Test-PSModulemain, no version coupling); merge first.Install-PSModuleHelpersto the release, then merge + release → new tag.Install-PSModuleHelpersto the release, then merge + release → new tag.Test-Module.yml→ Test-PSModule release,Build-Docs.yml→ Document-PSModule release,Test-ModuleLocal.yml→ Install-PSModuleHelpers release. Review + merge, then close this issue.Plan.ymlto theResolve-PSModuleVersionv1.1.0 interface (v1.1.0 dropped theVersion/Prereleaseinputs and addedName) and repin it to v1.1.0. (done in 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342)Install-PSModuleHelpers:Install-PSModule(src/Helpers/Helpers.psm1) copies the built module into a hard-coded.../<name>/999.0.0folder beforeImport-Module, which fails once the manifest carries the real version. Install into the folder that matches the manifestModuleVersion(via the existingGet-ModuleManifest), then release and repin. BlocksTest-ModuleandTest-ModuleLocal. Status (2026-07-08): implemented in PSModule/Install-PSModuleHelpers#19 (own CI green; Copilot review converged; also hardened against path traversal from a malformed manifest version). Validated end-to-end on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 via a temporary branch pin —Test-ModuleLocalis green. Awaiting merge + release, then repin to the released tag.Process-PSModule/.github/workflows/Test-ModuleLocal.yml: replace the hard-codedImport-Module -RequiredVersion 999.0.0with the resolved version from the Plan settings. Status (2026-07-08): done on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 (the Prescript now importsfromJson(Settings).Module.Version);Install-PSModuleHelpersis temporarily pinned to the 🪲 [Fix]: Setting PowerShellVersion correctly after tests #19 branch until it releases.Get-PesterCodeCoverage: coverage file paths are normalized by splitting on999.0.0; derive the version dynamically so coverage paths resolve with real versions, then release and repin. Status (2026-07-08): likely not required for green —Get-CodeCoverageandGet-TestResultspass on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 with real versions. The split just no longer strips a prefix, so coverage file paths may be absolute rather than module-relative (cosmetic). To confirm/clean up as a follow-up.Test-PSModule: re-release and repin if required after theInstall-PSModuleHelpersfix (it depends onInstall-PSModule). Status (2026-07-08): implemented in PSModule/Test-PSModule#119 (bumps theInstall-PSModuleHelperspin).Test-Module(andGet-TestResults/Get-CodeCoverage) validated green on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 via a temporary branch pin; Copilot review converged. Test-PSModule's ownAction-Test-outputswas pre-existing red onmain(a Pester 6 empty-ForEachinPSModule.Tests.ps1, unrelated to this refactor); fixed separately in PSModule/Test-PSModule#120, which must merge before 🚀[Feature]: Merge results on module tests to get correct coverage stats #119.Document-PSModule: repinInstall-PSModuleHelpers(it also installs the built module viaInstall-PSModule, forBuild-Docs). Status (2026-07-08): implemented in PSModule/Document-PSModule#52; own CI green; Copilot review converged;Build-Docsvalidated green on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342 via a temporary branch pin. Awaiting merge + release, then repin.Validation
Process-PSModulewith a labeled PR.999.0.0mismatch).Test-Module,Test-ModuleLocal,Get-TestResults, andBuild-Docspass end-to-end on 🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact #342.Note
This refactor centralizes version decisions in the Plan job (
Resolve-PSModuleVersion), improving maintainability while enforcing tested-artifact equals shipped-artifact behavior.