Skip to content

Move version calculation to a Plan job (Resolve-PSModuleVersion) so Build and Publish never calculate or mutate versions #326

Closed
1 of 1 issue completed

Description

Problem

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 tested
Set-ModuleManifest -Path $manifestFilePath -ModuleVersion $manifestNewVersion
Set-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:

  1. Get-PSModuleSettings loads and resolves settings.
  2. 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).


Implementation Plan

Resolve-PSModuleVersion (new action repo)

Implemented in PSModule/Resolve-PSModuleVersion#1 ΓÇö merged.

  • Create PSModule/Resolve-PSModuleVersion (LICENSE, README, action.yml, src/main.ps1, tests).
  • Inputs: Settings, Name, WorkingDirectory.
  • Outputs: Version, Prerelease, FullVersion, ReleaseType, CreateRelease.
  • Port logic from Publish-PSModule/src/init.ps1:
    • Install PSSemVer.
    • Query GitHub releases.
    • Query PSGallery.
    • Parse PR labels (major/minor/patch/ignore).
    • Apply bump logic.
    • Handle incremental prerelease numbering.
    • Honor DatePrereleaseFormat and VersionPrefix.
  • Add Pester tests for label parsing, bump selection, and prerelease sequencing.

Build-PSModule changes

Implemented in PSModule/Build-PSModule#136 ΓÇö merged.

  • Accept Version and Prerelease inputs.
  • Replace placeholder-only ModuleVersion behavior with:
    • passed-in version when provided
    • '999.0.0' fallback when not provided
  • Stamp prerelease metadata when provided.
  • Release as additive change (minor bump).

Publish-PSModule changes

Implemented in PSModule/Publish-PSModule#71 ΓÇö merged.

  • Remove src/init.ps1.
  • 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 list before publish.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).
  • Guard Compress-Archive inside if (-not $whatIf) so WhatIf mode does not write a real zip file (PSModule/Publish-PSModule#71 review).
  • Validate $manifest.Version.Build -ge 0 before constructing the release tag to catch 2-part version strings (PSModule/Publish-PSModule#71 review).
  • Keep cleanup.ps1 behavior unchanged (gate cleanup on stable-release-only runs, not every invocation).
  • Release as major bump.

Process-PSModule workflow changes

Work in progress in PSModule/Process-PSModule#342 ΓÇö draft, awaiting end-to-end validation.

  • Rename Get-Settings job to Plan.
  • Keep Get-PSModuleSettings as first step in Plan.
  • 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):

  1. PSModule/Test-PSModule#120 — Pester 6 -ForEach fix. Independent (fixes Test-PSModule main, no version coupling); merge first.
  2. PSModule/Install-PSModuleHelpers#19 — root install-version fix. Merge + release → new tag.
  3. PSModule/Test-PSModule#119 — after Post test result summary as PR comment #120 is merged and 🪲 [Fix]: Setting PowerShellVersion correctly after tests #19 is released: repin Install-PSModuleHelpers to the release, then merge + release → new tag.
  4. PSModule/Document-PSModule#52 — after 🪲 [Fix]: Setting PowerShellVersion correctly after tests #19 is released: repin Install-PSModuleHelpers to the release, then merge + release → new tag.
  5. PSModule/Process-PSModule#342 — after 🚀[Feature]: Merge results on module tests to get correct coverage stats #119/Support passing arbitrary secrets as environment variables to test jobs #52/🪲 [Fix]: Setting PowerShellVersion correctly after tests #19 are released: repin Test-Module.yml → Test-PSModule release, Build-Docs.yml → Document-PSModule release, Test-ModuleLocal.yml → Install-PSModuleHelpers release. Review + merge, then close this issue.

Validation

  • Verify built artifact manifest contains final version before publish.
  • Verify publish logs contain no manifest mutation.
  • Verify prerelease cleanup behavior is unchanged.
  • Run end-to-end in Process-PSModule with a labeled PR.
  • Smoke test one downstream consumer workflow.
  • Verify the built module installs into a version folder that matches its stamped manifest version (no 999.0.0 mismatch).
  • Confirm Test-Module, Test-ModuleLocal, Get-TestResults, and Build-Docs pass 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.

Activity

  1. changed the title [-]Extract shared versioning and release logic from Publish-PSModule and Release-GHRepository[/-] [+]Move version calculation to Build-PSModule so Publish-PSModule never mutates the artifact[/+] on May 16, 2026
  2. MariusStorhaug commented on May 16, 2026

    @MariusStorhaug
    MemberAuthor

    Rewrote the issue description completely to reflect the correct problem framing.

    What changed:

    • Framing corrected: The original description framed this primarily as a DRY / code-duplication problem. The fundamental problem is CI/CD artifact integrity — the tested artifact (999.0.0) is never identical to the published artifact (the real version). DRY is now described as a downstream consequence, not the root issue.
    • Section 1 rewritten: The context and request are now expressed entirely from a user/consumer perspective — what the current pipeline does to break artifact integrity, what the desired pipeline looks like, and a side-by-side comparison table. No file paths or function internals appear in this section.
    • Section 2 added/revised: Technical decisions now explicitly document each architectural choice — what inputs are available at build time, why Build-PSModule becomes the single version authority, what Publish-PSModule reduces to, where PSSemVer moves, how workflow inputs shift, and the breaking-change implications.
    • Section 3 preserved with minor cleanup: The implementation plan task lists are largely unchanged from the prior version, with wording tightened to match the corrected framing.
    • Cross-references updated: Move version generation from Publish to Build step #176 and Separate GitHub Release creation from PowerShell Gallery publishing #210 are now described accurately — both are directly addressed by this change rather than merely "related."
    • The 999.0.0 pipeline diagram was removed from the body; the comparison table in Section 1 conveys the same information more concisely.
  3. changed the title [-]Move version calculation to Build-PSModule so Publish-PSModule never mutates the artifact[/-] [+]Move version calculation to Plan-PSModule so Build and Publish never calculate or mutate versions[/+] on May 16, 2026
  4. changed the title [-]Move version calculation to Plan-PSModule so Build and Publish never calculate or mutate versions[/-] [+]Move version calculation to a Plan job (Resolve-PSModuleVersion) so Build and Publish never calculate or mutate versions[/+] on May 16, 2026
  5. MariusStorhaug commented on Jul 9, 2026

    @MariusStorhaug
    MemberAuthor

    PR #342 — pulled from main, dependency audit, and full Copilot review ✅

    Branch head: dac345c · Status: MERGEABLE, awaiting maintainer approval (do not self-merge).

    Pulled from main

    Merged latest origin/main into the branch, including #367 (GitHub-Script v1.9.0) and #365 (TestData secret model). README conflicts resolved keeping both the #368 "Dependency tree" and #342 "Plan" content.

    Full dependency audit (all workflow action pins)

    All pins verified current. Three stale pins bumped to their released tags:

    • Build-Site.yml: Install-PSModuleHelpers v1.0.7 → v1.0.8
    • Test-SourceCode.yml: Test-PSModule v3.0.10 → v3.0.13
    • Test-ModuleLocal.yml: Invoke-Pester → v4.2.6

    Final companion versions: Resolve-PSModuleVersion v1.1.0, Build-PSModule v5.0.0, Publish-PSModule v3.0.0, Test-PSModule v3.0.13, Install-PSModuleHelpers v1.0.8, Document-PSModule v1.0.17, Invoke-Pester v4.2.6.

    CI

    Green on dac345c: 61 pass / 7 skip / 0 fail across both Workflow-Test suites (Default + WithManifest) plus linters, CodeQL, and Release.

    Copilot review — converged (round 11: "generated no new comments", 0 unresolved threads)

    Fixes applied during the loop:

    • Normalized the workflow_call secret declaration APIKEY → APIKey (cosmetic; GitHub secret names are case-insensitive).
    • README: replaced the stale ReleaseType output reference with Settings.Module.ReleaseType / CreateRelease.
    • Test-ModuleLocal.yml: quoted -RequiredVersion.
    • Build-Module.yml: aligned the no-version fallback 0.0.0 → 999.0.0 (matches the documented placeholder); reconciled the PR description (single Settings output with Settings.Module.*, v1.1.0 pin, unconditional Resolve-Version).
    • Grammar fix in the Plan section.

    Two comments were false positives, verified and resolved with evidence:

    1. "Publish-Module.yml never downloads the module artifact" — Publish-PSModule@v3.0.0's composite action downloads the module artifact internally (download-artifact → outputs/module), so tested bytes == published bytes holds.
    2. 8 comments: "hyphenated step/job IDs need bracket syntax" — GitHub Actions dot-notation supports hyphens for needs/steps/jobs; confirmed by green CI executing those exact expressions.

    This is the final item in the merge order for #326. Ready for maintainer review + merge.

  6. added a commit that references this issue on Jul 9, 2026
    0d7a2f0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions