Skip to content

🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact#342

Draft
Marius Storhaug (MariusStorhaug) wants to merge 30 commits into
mainfrom
feat/326-resolve-psmodule-version
Draft

🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact#342
Marius Storhaug (MariusStorhaug) wants to merge 30 commits into
mainfrom
feat/326-resolve-psmodule-version

Conversation

@MariusStorhaug

@MariusStorhaug Marius Storhaug (MariusStorhaug) commented May 22, 2026

Copy link
Copy Markdown
Member

The version a module ships with is now decided in a new Plan job that runs before the module is built. The same artifact then flows through tests and publish without being mutated, so the artifact you tested is the artifact that lands in the PowerShell Gallery and on the GitHub Release.

New: Plan job replaces Get-Settings

Get-Settings.yml is renamed to Plan.yml. The Plan job runs two steps in sequence and exposes everything downstream jobs need:

  1. Get-PSModuleSettings — loads .github/PSModule.yml and emits the resolved Settings JSON.
  2. Resolve-PSModuleVersion — reads settings + PR labels, queries existing releases and the PowerShell Gallery, and emits the next version.

Plan job output: a single Settings JSON. The resolved version is merged into it as Settings.Module.{Version, Prerelease, FullVersion, ReleaseType, CreateRelease}, so every downstream job receives one self-contained object with no separate version outputs.

flowchart LR
  A[Plan<br/>Get-PSModuleSettings + Resolve-PSModuleVersion] --> B[Build-Module<br/>stamps version into manifest]
  B --> C[Test-* / Coverage]
  C --> D[Publish-Module<br/>publishes artifact unchanged]
Loading

Changed: Build-Module now stamps the real version

Build-Module.yml reads the version from Settings.Module.Version / Settings.Module.Prerelease and passes them to Build-PSModule. The built manifest contains the version the module will ship with before any test runs. The 999.0.0 placeholder only appears when no version is provided (for example, direct callers that bypass the Plan job).

Changed: Publish-Module no longer calculates or mutates versions

Publish-Module.yml drops every input that used to drive version calculation:

  • AutoPatching
  • DatePrereleaseFormat
  • IgnoreLabels
  • IncrementalPrerelease
  • MajorLabels / MinorLabels / PatchLabels
  • ReleaseType
  • VersionPrefix

Publish-Module now reads the version straight from the manifest that arrived from Build-Module and pushes it to the PowerShell Gallery as-is. The PR-title / PR-body / heading inputs are unchanged. The job also gains permission to upload release assets so the zipped module can be attached to the GitHub Release.

Fixed: Tested artifact equals published artifact

The placeholder-then-rewrite flow is gone. The bytes tested in Test-Module / Test-ModuleLocal are the same bytes published to the PowerShell Gallery and attached to the GitHub Release.

Technical Details

  • Plan.yml (renamed from Get-Settings.yml): adds a Resolve-Version step (runs unconditionally — it computes the release decision, including ReleaseType) followed by an Enrich-Settings step that merges the resolved version into Settings.Module.*. Plan exposes a single Settings output on both the job and workflow_call.
  • workflow.yml: the Get-Settings job is renamed to Plan and every downstream needs: / with: reference is repointed. Downstream jobs consume the single needs.Plan.outputs.Settings object (version available under Settings.Module.*).
  • Build-Module.yml: reads Settings.Module.Version / Settings.Module.Prerelease and passes them as Version / Prerelease to Build-PSModule, falling back to the 999.0.0 placeholder when a direct caller bypasses Plan and omits Settings.Module.
  • Publish-Module.yml: removes the seven version-calculation inputs and grants contents: write permission for release-asset uploads.
  • README.md: replaces the Get-Settings section with a Plan section that documents both steps and the artifact-integrity guarantee, and notes the new release-asset upload.
  • PSModule/Build-PSModule pinned to @v5.0.0 (SHA 672aaa7a91a379c4c6cd14494d03ab5e87e13c52).
  • PSModule/Publish-PSModule pinned to @v3.0.0 (SHA 03c0f8b53d0367c85a0f121f98af9b40c817b0e3).

Companion repos

Repo Change Link
PSModule/Resolve-PSModuleVersion New action — v1.1.0 released https://github.com/PSModule/Resolve-PSModuleVersion/releases/tag/v1.1.0
PSModule/Build-PSModule Major — adds Version / Prerelease inputs — v5.0.0 released https://github.com/PSModule/Build-PSModule/releases/tag/v5.0.0
PSModule/Publish-PSModule Major / BREAKING — drops version calculation, publishes pre-stamped artifact, uploads module zip to release — v3.0.0 released https://github.com/PSModule/Publish-PSModule/releases/tag/v3.0.0

Implementation plan progress

  • Rename Get-Settings job to Plan and add Resolve-PSModuleVersion step
  • Expose a single Settings output (resolved version merged into Settings.Module.{Version, Prerelease, FullVersion, ReleaseType, CreateRelease})
  • Update Build-Module.yml to read the version from Settings.Module.* and stamp it
  • Strip version-calculation inputs from Publish-Module.yml
  • Update needs / with wiring across workflow.yml
  • Update README.md (Plan section + release-asset note)
  • Resolve-PSModuleVersion action implementation (v1.1.0 released)
  • Build-PSModule Version/Prerelease inputs (v5.0.0 released)
  • Publish-PSModule v3 release (v3.0.0 released — 🌟 [Major]: Version calculation removed — artifact must be pre-stamped before publish Publish-PSModule#71)
  • Repin @main references in Build-Module.yml and Publish-Module.yml to released SHAs
  • End-to-end run on a labeled PR

Notes

@MariusStorhaug Marius Storhaug (MariusStorhaug) changed the title WIP: Plan job (#326) 🚀 [Minor]: Plan job decides version before build so tested artifact equals published artifact (#326) May 22, 2026
Comment thread .github/workflows/Build-Module.yml Fixed
Comment thread .github/workflows/Plan.yml Fixed
Comment thread .github/workflows/Publish-Module.yml Fixed
@github-actions

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

@github-actions

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

Marius Storhaug (MariusStorhaug) added a commit to PSModule/Resolve-PSModuleVersion that referenced this pull request May 22, 2026
…tion (#1)

Module version resolution is now available as a standalone action.
Workflows can call it before building so the resolved version is stamped
into the artifact at build time, making the bytes that are tested the
bytes that ship.

- Resolves PSModule/Process-PSModule#326

## New: Standalone `Resolve-PSModuleVersion` action

The action consumes the JSON `Settings` output from
[`PSModule/Get-PSModuleSettings`](https://github.com/PSModule/Get-PSModuleSettings)
and emits:

| Output | Description |
| --- | --- |
| `Version` | `Major.Minor.Patch` portion of the resolved version. |
| `Prerelease` | Prerelease tag, empty when not a prerelease. |
| `FullVersion` | Full version string including `VersionPrefix` and
prerelease tag. |
| `ReleaseType` | `Release`, `Prerelease`, or `None` when no version
bump label is present. |
| `CreateRelease` | `true` when a release or prerelease should be
created. |

Typical usage in the Plan job:

```yaml
- name: Resolve module version
  id: resolve
  uses: PSModule/Resolve-PSModuleVersion@v1
  env:
    GH_TOKEN: ${{ github.token }}
  with:
    Settings: ${{ steps.settings.outputs.Settings }}

- name: Build module
  uses: PSModule/Build-PSModule@v5
  with:
    Version: ${{ steps.resolve.outputs.Version }}
    Prerelease: ${{ steps.resolve.outputs.Prerelease }}
```

The action validates `Settings.Publish.Module.ReleaseType`, applies
`IgnoreLabels` overrides, picks the bump type from PR labels
(`MajorLabels` > `MinorLabels` > `PatchLabels` / `AutoPatching`), then
computes the next version from the higher of the latest GitHub Release
and the latest PowerShell Gallery version. For prereleases it appends
the sanitized branch name, optional `DatePrereleaseFormat` timestamp,
and an incremental counter calculated from existing prereleases on the
same baseline + branch.

## Technical Details

- `action.yml`: composite action with inputs `Settings` (required JSON),
`Name`, `WorkingDirectory`, `Debug`, `Verbose`, `Version`, `Prerelease`,
plus `EventPath` and `EventJson` (both optional, for test overrides —
`EventJson` takes precedence over reading the file at `EventPath`). All
`${{ }}` template expressions are isolated in `env:` sections per zizmor
template-injection requirements. Installs
`PSModule/Install-PSModuleHelpers` and `PSSemVer` before running the
script.
- `scripts/main.ps1`: ports the version-resolution logic that previously
lived in `Publish-PSModule/src/init.ps1`. Reads configuration from
`PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_Settings` JSON instead of
separate env vars. Reads the PR event from
`PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_EventJson` when set, falling
back to the file at `GITHUB_EVENT_PATH`. Emits outputs via
`$env:GITHUB_OUTPUT`. Cleanup-tag discovery stays in
`Publish-PSModule/cleanup.ps1` and is intentionally out of scope here.
- `.github/workflows/Action-Test.yml`: 6 test jobs covering patch,
minor, major, auto-patch, ignore-label, and None scenarios. The
ignore-label job passes the fake PR event as a JSON string via
`EventJson` to bypass the runner's real event file, which cannot be
reliably overridden at the file-system level.
- `README.md`: replaces the template scaffold with the action's contract
and usage examples.

**Implementation plan progress** (PSModule/Process-PSModule#326):
- ✅ Create `Resolve-PSModuleVersion` (LICENSE, README, `action.yml`,
`scripts/main.ps1`, Action-Test workflow)
- ✅ Inputs: `Settings`, `Name`, `WorkingDirectory` (plus
`EventPath`/`EventJson` for test overrides)
- ✅ Outputs: `Version`, `Prerelease`, `FullVersion`, `ReleaseType`,
`CreateRelease`
- ✅ Port version-resolution logic from `Publish-PSModule/src/init.ps1`
(PSSemVer install, GitHub Releases query, PSGallery query, PR-label
parsing, bump selection, prerelease sequencing, `DatePrereleaseFormat`,
`VersionPrefix`)
- ⬜ Dedicated Pester unit tests for label parsing, bump selection, and
prerelease sequencing — covered by the six integration test jobs; a
focused unit-test suite remains open

Related PRs:
- PSModule/Process-PSModule#342 — rewires the workflow's Plan → Build →
Test → Publish chain to consume the resolved version.
- PSModule/Build-PSModule#136 — accepts `Version` / `Prerelease` inputs
and stamps them into the manifest at build time.
- PSModule/Publish-PSModule#71 — removes the version-calculation logic
that moved here.
@github-actions

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

@github-actions

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

Marius Storhaug (MariusStorhaug) added a commit to PSModule/Build-PSModule that referenced this pull request May 24, 2026
…t build time (#136)

Module manifests are now stamped with the resolved version and
prerelease tag at build time. The resulting artifact contains its final
`ModuleVersion` (and `PrivateData.PSData.Prerelease`) before tests run,
so the bytes that are tested are the bytes that ship.

- Fixes PSModule/Process-PSModule#326

## Inputs on `Build-PSModule`

`Build-PSModule` now exposes new module-centric inputs:

| Input | Required | Description |
| --- | --- | --- |
| `Name` | No | Name of the module to build. Defaults to the repository
name. |
| `Version` | **Yes** | Module version (`Major.Minor.Patch`) to stamp
into the manifest. Build fails with a clear error when omitted or
malformed. |
| `Prerelease` | No | Prerelease tag (for example `mybranch001`) to
stamp into `PrivateData.PSData.Prerelease`. When empty, no prerelease
tag is written. |
| `OutputFolder` | No | Path (relative to `WorkingDirectory`) where the
built module is placed. Defaults to `outputs/module`. |

Typical usage downstream of
[`PSModule/Resolve-PSModuleVersion`](https://github.com/PSModule/Resolve-PSModuleVersion):

```yaml
- name: Build module
  uses: PSModule/Build-PSModule@v5
  with:
    Version: ${{ steps.resolve.outputs.Version }}
    Prerelease: ${{ steps.resolve.outputs.Prerelease }}
```

## Breaking changes

- `Version` is now **required**. Callers that previously omitted it
(relying on the `999.0.0` placeholder) must now pass an explicit version
in `Major.Minor.Patch` format. Builds fail immediately with a clear
error when `Version` is missing or malformed.

## Technical details

- `action.yml`: adds `OutputFolder` (default `outputs/module`),
`Version` (`required: true`), and `Prerelease` inputs; `Name` remains
optional and still defaults to the repository name.
- `src/main.ps1`: reads `OutputFolder`, `Version`, and `Prerelease` from
env; throws immediately when `Version` is missing or not in
`Major.Minor.Patch` format.
- `src/helpers/Build-PSModule.ps1`: `ModuleVersion` parameter is now
`[Parameter(Mandatory)]`.
- `src/helpers/Build/Build-PSModuleManifest.ps1`: `ModuleVersion` is
`[Parameter(Mandatory)]`; the `999.0.0` fallback is removed — the
version is assigned directly.

Related PRs:
- PSModule/Resolve-PSModuleVersion#1 — emits the `Version` and
`Prerelease` values consumed here.
- PSModule/Publish-PSModule#71 — drops its own version stamping; expects
the artifact to arrive pre-stamped.
- PSModule/Process-PSModule#342 — wires the workflow end-to-end.
Comment thread .github/workflows/Build-Module.yml Fixed
@github-actions

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

@github-actions

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

Comment thread .github/workflows/Build-Module.yml Fixed
v1.1.0 removed the Version/Prerelease inputs and added Name (defaults to repo name). Pass Name from Settings.Name so the fixture/module version is resolved for the correct module, and drop the now-invalid Version/Prerelease inputs.
Copilot AI review requested due to automatic review settings July 8, 2026 00:16

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 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread .github/workflows/workflow.yml Outdated
Comment thread .github/workflows/Build-Module.yml
…lpers to PR#19 branch

Fix the hard-coded Import-Module -RequiredVersion 999.0.0 in the Pester Prescript to use the resolved module version. Temporarily pin Install-PSModuleHelpers to the fix/install-real-module-version branch (PSModule/Install-PSModuleHelpers#19) to validate end-to-end; will repin to the released tag before merge.
… for Test-Module

Point Test-Module at the Test-PSModule branch that installs built modules at their real version, and advance the Test-ModuleLocal Install-PSModuleHelpers pin to the latest fix commit. Both are temporary branch pins pending releases.
Copilot AI review requested due to automatic review settings July 8, 2026 01:21

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 9 out of 9 changed files in this pull request and generated 3 comments.

Comment thread .github/workflows/Build-Module.yml
Comment thread .github/workflows/Test-ModuleLocal.yml Outdated
Comment thread .github/workflows/workflow.yml
Point Build-Docs at the Document-PSModule branch that installs built modules at their real version. Temporary branch pin pending release.
Copilot AI review requested due to automatic review settings July 8, 2026 01:32

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 10 out of 10 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (1)

.github/workflows/Test-ModuleLocal.yml:101

  • This Import-Module command references fromJson(inputs.Settings).Module.Version directly. If Module is missing (e.g., when running without the new Plan enrichment), expression evaluation can fail and the tests won't start. Guard the lookup and use the same placeholder version as the build step when no resolved version exists.
          Prescript: | # This is to speed up module loading in Pester.
            Install-PSResource -Repository PSGallery -TrustRepository -Name PSCustomObject
            Import-Module -Name '${{ steps.import-module.outputs.name }}' -RequiredVersion ${{ fromJson(inputs.Settings).Module.Version }}

Comment thread .github/workflows/workflow.yml
Comment thread .github/workflows/Build-Module.yml
Comment thread .github/workflows/Test-ModuleLocal.yml
Comment thread .github/workflows/Test-Module.yml
Comment thread .github/workflows/Build-Docs.yml
@MariusStorhaug Marius Storhaug (MariusStorhaug) marked this pull request as draft July 9, 2026 02:34
- Reconcile with #365 (TestData secret inputs) — kept both the Plan/version wiring and the new TestData inputs in workflow.yml/Test-ModuleLocal.yml; README secrets section adopts the TestData model.
- Repin to released tags: Test-PSModule v3.0.13, Install-PSModuleHelpers v1.0.8, Document-PSModule v1.0.17 (replacing the temporary branch pins).
Full dependency audit: bring the last stale first-party pins up to current releases for consistency with the rest of the branch. Build-Site Install-PSModuleHelpers v1.0.7->v1.0.8; Test-SourceCode Test-PSModule v3.0.10->v3.0.13; Test-ModuleLocal Invoke-Pester v4.2.5->v4.2.6. All other pins already up to date.
The reusable workflow declared the gallery secret as APIKEY while every usage, the nested Publish-Module workflow, the test callers, and the README use APIKey. GitHub secret names are case-insensitive so publishing worked, but this removes the inconsistency Copilot flagged.

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 12 out of 12 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

.github/workflows/workflow.yml:8

  • The workflow_call secret is renamed to APIKEY, but downstream still references secrets.APIKey (e.g. Publish-Module job). This will fail at runtime (secrets.APIKey will be empty/undefined) and also breaks the stated “public input/output contract … unchanged”. Keep the secret name as APIKey (existing contract) or update all references consistently.
  workflow_call:
    secrets:
      APIKey:
        description: The API key for the PowerShell Gallery.
        required: true

Comment thread .github/workflows/Build-Module.yml
Comment thread README.md

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 12 out of 12 changed files in this pull request and generated 4 comments.

Comment thread .github/workflows/Plan.yml
Comment thread .github/workflows/Plan.yml
Comment thread .github/workflows/Build-Module.yml Outdated
Comment thread .github/workflows/Test-ModuleLocal.yml 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 12 out of 12 changed files in this pull request and generated 1 comment.

Comment thread README.md
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.

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

3 participants