Publish-PSModule and Release-GHRepository independently implement the same core logic for semantic versioning, release type determination, GitHub release creation, prerelease cleanup, and PR commenting. A comparison of the two actions reveals 347+ lines of near-identical code.
Request
Current experience
Both actions independently implement:
| Responsibility |
Release-GHRepository/src/main.ps1 |
Publish-PSModule/src/init.ps1 + publish.ps1 + cleanup.ps1 |
| Version calculation |
Lines 178–217 |
init.ps1 lines 209–354 |
| Release type determination |
Lines 128–150 |
init.ps1 lines 124–186 |
| Label analysis |
Lines 139–141 |
init.ps1 lines 145–172 |
| GitHub release creation |
Lines 219–285 |
publish.ps1 lines 140–216 |
| Prerelease cleanup |
Lines 367–388 |
cleanup.ps1 lines 17–49 |
| PR commenting |
Lines 278–284 |
publish.ps1 lines 131–137, 209–214 |
The version calculation logic is nearly character-for-character identical:
# Release-GHRepository (lines 178-190)
LogGroup 'Calculate new version' {
$latestVersion = New-PSSemVer -Version $latestVersion
$newVersion = New-PSSemVer -Version $latestVersion
$newVersion.Prefix = $versionPrefix
if ($majorRelease) {
Write-Output 'Incrementing major version.'
$newVersion.BumpMajor()
} elseif ($minorRelease) { ... }
# Publish-PSModule/init.ps1 (lines 269-285)
LogGroup 'Calculate new version' {
$newVersion = New-PSSemVer -Version $latestVersion
$newVersion.Prefix = $versionPrefix
if ($majorRelease) {
Write-Host 'Incrementing major version.'
$newVersion.BumpMajor()
} elseif ($minorRelease) { ... }
A bug fix or feature added to one action (e.g., date-based prerelease formatting, incremental prerelease numbering) must be manually replicated in the other. Divergence has already occurred — Publish-PSModule uses Write-Host where Release-GHRepository uses Write-Output.
Desired experience
Shared versioning, release, and cleanup logic lives in a single location. Both actions consume the shared logic rather than reimplementing it.
Acceptance criteria
- Version calculation logic exists in exactly one place
- Release type determination (from PR labels) exists in exactly one place
- GitHub release creation logic exists in exactly one place
- Prerelease cleanup logic exists in exactly one place
- Both
Publish-PSModule and Release-GHRepository produce identical results for the same inputs
- A bug fix to shared logic automatically applies to both actions
Technical decisions
Approach: Extract the shared logic into a dedicated PowerShell module (e.g., PSModuleRelease or extend the existing Helpers module) that both actions import. The module provides functions such as:
Get-ReleaseType — determines release/prerelease/none from PR labels and merge status
Get-NextVersion — calculates the next semantic version given the current version and release type
New-GitHubRelease — creates a GitHub release with configurable notes
Remove-PrereleaseVersions — cleans up old prerelease tags
Alternative considered: Making Publish-PSModule depend on Release-GHRepository as a sub-action. Rejected because Publish-PSModule has the additional responsibility of PowerShell Gallery publishing, and tight coupling between two composite actions creates a fragile dependency chain.
Alternative considered: Moving all shared logic into the Helpers module installed by Install-PSModuleHelpers. This is feasible but would increase the Helpers module scope significantly. A dedicated module with a clear boundary is preferred.
Relationship to #210: This issue provides the architectural foundation that makes #210 (separate release creation from publishing) straightforward to implement. Once shared logic is extracted, Publish-PSModule becomes: "publish to Gallery" + "call shared release logic."
Implementation plan
Shared module
Release-GHRepository changes
Publish-PSModule changes
Testing
Publish-PSModuleandRelease-GHRepositoryindependently implement the same core logic for semantic versioning, release type determination, GitHub release creation, prerelease cleanup, and PR commenting. A comparison of the two actions reveals 347+ lines of near-identical code.Request
Current experience
Both actions independently implement:
Release-GHRepository/src/main.ps1Publish-PSModule/src/init.ps1+publish.ps1+cleanup.ps1init.ps1lines 209–354init.ps1lines 124–186init.ps1lines 145–172publish.ps1lines 140–216cleanup.ps1lines 17–49publish.ps1lines 131–137, 209–214The version calculation logic is nearly character-for-character identical:
A bug fix or feature added to one action (e.g., date-based prerelease formatting, incremental prerelease numbering) must be manually replicated in the other. Divergence has already occurred —
Publish-PSModuleusesWrite-HostwhereRelease-GHRepositoryusesWrite-Output.Desired experience
Shared versioning, release, and cleanup logic lives in a single location. Both actions consume the shared logic rather than reimplementing it.
Acceptance criteria
Publish-PSModuleandRelease-GHRepositoryproduce identical results for the same inputsNote
This violates the DRY principle and Single Responsibility Principle. It is closely related to #210 (separate release from publish) and #176 (move version generation to build).
Technical decisions
Approach: Extract the shared logic into a dedicated PowerShell module (e.g.,
PSModuleReleaseor extend the existingHelpersmodule) that both actions import. The module provides functions such as:Get-ReleaseType— determines release/prerelease/none from PR labels and merge statusGet-NextVersion— calculates the next semantic version given the current version and release typeNew-GitHubRelease— creates a GitHub release with configurable notesRemove-PrereleaseVersions— cleans up old prerelease tagsAlternative considered: Making
Publish-PSModuledepend onRelease-GHRepositoryas a sub-action. Rejected becausePublish-PSModulehas the additional responsibility of PowerShell Gallery publishing, and tight coupling between two composite actions creates a fragile dependency chain.Alternative considered: Moving all shared logic into the
Helpersmodule installed byInstall-PSModuleHelpers. This is feasible but would increase the Helpers module scope significantly. A dedicated module with a clear boundary is preferred.Relationship to #210: This issue provides the architectural foundation that makes #210 (separate release creation from publishing) straightforward to implement. Once shared logic is extracted,
Publish-PSModulebecomes: "publish to Gallery" + "call shared release logic."Implementation plan
Shared module
Get-ReleaseTypefrom the label-analysis logic in both actionsGet-NextVersionfrom the version-calculation logic in both actionsNew-GitHubReleasefrom the release-creation logic in both actionsRemove-PrereleaseVersionsfrom the cleanup logic in both actionsRelease-GHRepository changes
Publish-PSModule changes
Publish-PSModule's unique responsibility)Testing
Release-GHRepositoryproduces identical releases as beforePublish-PSModuleproduces identical releases and publishes as before