🚀 [Feature]: Plan job decides version before build so tested artifact equals published artifact#342
Conversation
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
…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.
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
…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.
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
There was a problem hiding this comment.
Pull request overview
This PR restructures the reusable GitHub Actions workflow so module versioning is decided before building, enabling the same built artifact to flow through tests and publishing without version mutation.
Changes:
- Replaces
Get-Settingswith a newPlanworkflow that resolves settings and enriches them with the computed module version. - Updates the main orchestrator workflow to depend on
Planoutputs instead ofGet-Settings. - Pins updated action versions and removes version-calculation inputs from
Publish-Module, relying on the manifest-stamped version.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Updates docs to describe the new Plan job and artifact integrity/publish behavior; refreshes secrets table and workflow matrix. |
| .vscode/settings.json | Removes VS Code workspace chat/tooling settings from the repo. |
| .github/workflows/workflow.yml | Renames the initial job from Get-Settings to Plan and rewires downstream needs/with references. |
| .github/workflows/Publish-Module.yml | Pins PSModule/Publish-PSModule to v3 and removes deprecated version-calculation inputs; updates permissions comment. |
| .github/workflows/Plan.yml | Adds new reusable workflow to load settings, resolve version, and merge version info into a single Settings JSON output. |
| .github/workflows/Get-Settings.yml | Removes the previous reusable workflow that only loaded settings. |
| .github/workflows/Build-Module.yml | Pins PSModule/Build-PSModule to v5 and passes version/prerelease from Settings.Module.* into the build action. |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
| workflow_call: | ||
| secrets: | ||
| APIKey: | ||
| APIKEY: |
| if: fromJson(needs.Plan.outputs.Settings).Run.PublishModule && needs.Plan.result == 'success' && !cancelled() && (needs.Get-TestResults.result == 'success' || needs.Get-TestResults.result == 'skipped') && (needs.Get-CodeCoverage.result == 'success' || needs.Get-CodeCoverage.result == 'skipped') && (needs.Build-Site.result == 'success' || needs.Build-Site.result == 'skipped') | ||
| uses: ./.github/workflows/Publish-Module.yml | ||
| secrets: | ||
| APIKey: ${{ secrets.APIKey }} |
| uses: PSModule/Build-PSModule@672aaa7a91a379c4c6cd14494d03ab5e87e13c52 # v5.0.0 | ||
| with: | ||
| Name: ${{ fromJson(inputs.Settings).Name }} | ||
| Version: ${{ fromJson(inputs.Settings).Module.Version != '' && fromJson(inputs.Settings).Module.Version || '0.0.0' }} |
…#358) Consolidates the currently-passing Dependabot GitHub Actions updates into one change, so the pipeline moves to the latest action versions in a single release instead of separate bumps. It also configures Dependabot to group future GitHub Actions updates into one pull request, so this consolidated form happens automatically from now on. **Superseded Dependabot PRs:** #351, #353, #354, #355 — Dependabot closes these automatically once this merges. >⚠️ **Build-PSModule v5.0.0 (#350) is intentionally excluded.** v5 makes `moduleVersion` a required input, which the current `Build-Module.yml` does not provide, so it fails CI (6 checks on #350's own PR too). Adopting it needs the "decide version before build" work in #342 and should ship with/after that PR. #350 stays open to track it. ## Updated GitHub Actions | Action | From | To | Bump | | --- | --- | --- | --- | | actions/checkout | v6.0.2 | v7.0.0 | Major | | PSModule/Publish-PSModule | v2.2.4 | v3.0.0 | Major | | super-linter/super-linter | v8.6.0 | v8.7.0 | Minor | | super-linter/super-linter/slim | v8.6.0 | v8.7.0 | Minor | This bundle includes two major action upgrades, so it is labelled `Major` and cuts a major release of Process-PSModule on merge. ## Changed: Dependabot groups GitHub Actions updates `.github/dependabot.yml` now groups all `github-actions` updates into a single grouped pull request via a `groups` block. The `github_actions` label is also corrected — it was `github-actions`, which does not match the repository label and was silently dropped by Dependabot. ## Technical Details - Each bump is a cherry-pick of the original Dependabot commit, preserving authorship and the `Bump X from A to B` messages for a clean linear history. - All bumps touch only `uses:` pins under `.github/workflows/`; no logic changes. - Dependabot grouping added: ```yaml groups: github-actions: patterns: - "*" ``` This groups every GitHub Actions ecosystem update (all semver levels) into one PR. To keep major bumps as separate PRs for individual review, add `update-types: ["minor", "patch"]` under the group — majors then continue to open on their own. - No linked issue: this is Dependabot-driven maintenance; the superseded PRs above are the traceability. --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…363) The module publishing pipeline once again calculates and stamps the real release version before publishing, so consumer releases are versioned and tagged correctly instead of shipping the build-time placeholder. This reverts the premature `Publish-PSModule` major upgrade that left `main` in an inconsistent state. - Relates to #326 (move version calculation ahead of the build step) - Contains the regression introduced by #358 ## Fixed: Releases are versioned correctly again `Publish-PSModule` is pinned back to `v2.2.4`, which calculates the release version (from labels and tags) and stamps it into the module manifest at publish time. On `main`, `Build-PSModule` (v4) only stamps a `999.0.0` placeholder and does not compute the real version, and `Publish-PSModule` v3.0.0 is publish-only — it expects the manifest to already carry the final version. With v3.0.0 in place, nothing in the pipeline computed the real version, so a real consumer release would have published and tagged `999.0.0`. Reverting to `v2.2.4` restores the fully consistent old pipeline: Get-Settings → Build (v4, `999.0.0`) → Publish (v2.2.4, calculates + stamps). Only the `Publish-PSModule` pin is reverted. The other action bumps from #358 (`actions/checkout` v7.0.0, `super-linter` v8.7.0) are left in place. ## Technical Details - `.github/workflows/Publish-Module.yml`: `PSModule/Publish-PSModule` pin reverted `03c0f8b… # v3.0.0` → `8917aed… # v2.2.4`. This is the exact pin #358 replaced; no `with:` inputs changed — v2.2.4 consumes the version-calculation inputs the workflow already passes, whereas v3.0.0 silently ignored them. - `.github/workflows/Publish-Module.yml`: normalized the `APIKey` action input from `secrets.APIKEY` to `secrets.APIKey` to match the `APIKey` secret declaration and the other workflows (`workflow.yml`, `Workflow-Test-*.yml`). GitHub Actions secret names are case-insensitive, so this is a consistency-only change with no behavioral effect (flagged during Copilot review). - This is step 1 (containment) of #326. Follow-ups: release `Resolve-PSModuleVersion` with the #348 fix, then rebase and land PR #342 (Plan job + Build v5 + publish-only Publish) and re-pin the first-party actions together.
- Resolve Get-Settings.yml modify/delete conflict (keep deletion; replaced by Plan.yml) - Keep publish-only Publish-Module.yml (Publish-PSModule v3.0.0) over main's #363 revert - Bump Resolve-PSModuleVersion pin v1.0.1 -> v1.1.0 (#348 always-emit fix) - Align Plan.yml checkout to v7.0.0 (matches main)
Super-linter summary
All files and directories linted successfully For more information, see the GitHub Actions workflow run Powered by Super-linter |
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.
| workflow_call: | ||
| secrets: | ||
| APIKey: | ||
| APIKEY: |
| Name: ${{ fromJson(inputs.Settings).Name }} | ||
| Version: ${{ fromJson(inputs.Settings).Module.Version != '' && fromJson(inputs.Settings).Module.Version || '0.0.0' }} | ||
| Prerelease: ${{ fromJson(inputs.Settings).Module.Prerelease }} |
…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.
| Name: ${{ fromJson(inputs.Settings).Name }} | ||
| Version: ${{ fromJson(inputs.Settings).Module.Version != '' && fromJson(inputs.Settings).Module.Version || '0.0.0' }} | ||
| Prerelease: ${{ fromJson(inputs.Settings).Module.Prerelease }} | ||
| ArtifactName: ${{ inputs.ArtifactName }} |
| 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 999.0.0 | ||
| Import-Module -Name '${{ steps.import-module.outputs.name }}' -RequiredVersion ${{ fromJson(inputs.Settings).Module.Version }} |
| secrets: | ||
| APIKey: ${{ secrets.APIKey }} |
Point Build-Docs at the Document-PSModule branch that installs built modules at their real version. Temporary branch pin pending release.
There was a problem hiding this comment.
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-Modulecommand referencesfromJson(inputs.Settings).Module.Versiondirectly. IfModuleis 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 }}
| secrets: | ||
| APIKey: ${{ secrets.APIKey }} |
| Name: ${{ fromJson(inputs.Settings).Name }} | ||
| Version: ${{ fromJson(inputs.Settings).Module.Version != '' && fromJson(inputs.Settings).Module.Version || '0.0.0' }} | ||
| Prerelease: ${{ fromJson(inputs.Settings).Module.Prerelease }} |
| - name: Install-PSModuleHelpers | ||
| uses: PSModule/Install-PSModuleHelpers@ed79b6e3aa8c9cd3d30ab2bf02ea6bd4687b9c74 # v1.0.7 | ||
| uses: PSModule/Install-PSModuleHelpers@383aa36cd1b9dc838157cc4d08bb98cebc603216 # TEMP: fix/install-real-module-version (PSModule/Install-PSModuleHelpers#19) | ||
|
|
|
|
||
| - name: Test-Module | ||
| uses: PSModule/Test-PSModule@8c3337136dc7cf320da39eeb50e776d04bc9ac73 # v3.0.10 | ||
| uses: PSModule/Test-PSModule@0cd150aa3b484167766f0d1ce3a95dc18277ce4d # TEMP: fix/bump-install-psmodulehelpers (PSModule/Test-PSModule#119) | ||
| with: |
|
|
||
| - name: Document module | ||
| uses: PSModule/Document-PSModule@dc5b329f840f7803ec02d34a42ee725bca39db5f # v1.0.16 | ||
| uses: PSModule/Document-PSModule@be618c276f7c495744e109b4309792b1e35c47af # TEMP: fix/bump-install-psmodulehelpers (PSModule/Document-PSModule#52) | ||
| with: |
The version a module ships with is now decided in a new
Planjob 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.ymlis renamed toPlan.yml. The Plan job runs two steps in sequence and exposes everything downstream jobs need:Get-PSModuleSettings— loads.github/PSModule.ymland emits the resolvedSettingsJSON.Resolve-PSModuleVersion— reads settings + PR labels, queries existing releases and the PowerShell Gallery, and emits the next version.Plan job outputs:
Settings,ModuleVersion,ModulePrerelease,ModuleFullVersion,ReleaseType,CreateRelease.Changed: Build-Module now stamps the real version
Build-Module.ymlaccepts newModuleVersionandModulePrereleaseinputs and forwards them toBuild-PSModule. The built manifest contains the version the module will ship with before any test runs. The999.0.0placeholder only appears when no version is provided (for example, direct callers that opt out of the Plan job).Changed: Publish-Module no longer calculates or mutates versions
Publish-Module.ymldrops every input that used to drive version calculation:AutoPatchingDatePrereleaseFormatIgnoreLabelsIncrementalPrereleaseMajorLabels/MinorLabels/PatchLabelsReleaseTypeVersionPrefixPublish-Modulenow reads the version straight from the manifest that arrived fromBuild-Moduleand 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-ModuleLocalare the same bytes published to the PowerShell Gallery and attached to the GitHub Release.Technical Details
Plan.yml(renamed fromGet-Settings.yml): adds theResolve-Versionstep guarded byPublish.Module.ReleaseType != 'None'. New job outputs are wired throughoutputs:on both the job andworkflow_call.workflow.yml: theGet-Settingsjob is renamed toPlanand every downstreamneeds:/with:reference is repointed.Build-Modulenow receivesModuleVersionandModulePrereleasefromneeds.Plan.outputs.Build-Module.yml: addsModuleVersion/ModulePrereleaseinputs (defaulting to empty for backward compatibility with direct callers) and passes them asVersion/PrereleasetoBuild-PSModule.Publish-Module.yml: removes the seven version-calculation inputs and grantscontents: writepermission for release-asset uploads.README.md: replaces theGet-Settingssection with aPlansection that documents both steps and the artifact-integrity guarantee, and notes the new release-asset upload.PSModule/Build-PSModulepinned to@v5.0.0(SHA672aaa7a91a379c4c6cd14494d03ab5e87e13c52).PSModule/Publish-PSModulepinned to@v3.0.0(SHA03c0f8b53d0367c85a0f121f98af9b40c817b0e3).Companion repos
PSModule/Resolve-PSModuleVersionPSModule/Build-PSModuleVersion/Prereleaseinputs — v5.0.0 releasedPSModule/Publish-PSModuleImplementation plan progress
Get-Settingsjob toPlanand addResolve-PSModuleVersionstepSettings,ModuleVersion,ModulePrerelease,ModuleFullVersion,ReleaseType,CreateRelease)Build-Module.ymlto accept and forward version inputsPublish-Module.ymlneeds/withwiring acrossworkflow.ymlREADME.md(Plan section + release-asset note)Resolve-PSModuleVersionaction implementation (v1.0.1 released)Build-PSModuleVersion/Prereleaseinputs (v5.0.0 released)Publish-PSModulev3 release (v3.0.0 released — 🌟 [Major]: Version calculation removed — artifact must be pre-stamped before publish Publish-PSModule#71)@mainreferences inBuild-Module.ymlandPublish-Module.ymlto released SHAsNotes
workflow.ymlis unchanged for external consumers — this is internal wiring.main.