diff --git a/README.md b/README.md index c48bccc..2c67408 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,52 @@ # Publish-PSModule This GitHub Action is a part of the [PSModule framework](https://github.com/PSModule). + +It publishes a **pre-versioned** PowerShell module artifact to the PowerShell Gallery and creates a matching GitHub +Release. The compressed module is also uploaded as a release asset so the GitHub Release page exposes the exact bytes +that were tested and pushed to the Gallery. + +## Breaking change — v3.0.0 + +`Publish-PSModule` no longer calculates the next version or mutates the module manifest. The artifact passed in must +already contain the final `ModuleVersion` (and `Prerelease` tag, if any). + +The following inputs were **removed**: + +- `AutoPatching` +- `IncrementalPrerelease` +- `DatePrereleaseFormat` +- `VersionPrefix` +- `MajorLabels`, `MinorLabels`, `PatchLabels`, `IgnoreLabels` +- `ReleaseType` + +To migrate, run [`PSModule/Resolve-PSModuleVersion`](https://github.com/PSModule/Resolve-PSModuleVersion) to compute +the version and pass it to [`PSModule/Build-PSModule`](https://github.com/PSModule/Build-PSModule) so the artifact is +stamped before it is tested. The publish action then ships that artifact without any post-build manipulation. + +This makes the tested artifact identical to the published artifact (see +[PSModule/Process-PSModule#326](https://github.com/PSModule/Process-PSModule/issues/326)). + +## What it does + +1. Downloads the `module` artifact uploaded by `Build-PSModule`. +2. Reads `ModuleVersion` and `PrivateData.PSData.Prerelease` from the downloaded manifest. +3. Installs `RequiredModules` declared by the manifest. +4. Publishes the module to the PowerShell Gallery (`Publish-PSResource`). +5. Creates a GitHub Release with the same tag. +6. Zips the module folder and uploads it as a release asset (`-.zip`). +7. Optionally cleans up prerelease tags whose name matches the current PR branch. + +## Inputs + +| Name | Description | Required | Default | +| -------------------------- | ------------------------------------------------------------------------------------------ | -------- | ---------------- | +| `Name` | Name of the module. Defaults to the repository name. | No | Repository name | +| `ModulePath` | Path to the downloaded module folder. | No | `outputs/module` | +| `APIKey` | PowerShell Gallery API key. | Yes | | +| `AutoCleanup` | Delete prerelease tags matching the PR branch after a stable release. | No | `true` | +| `WhatIf` | Log the changes that would be made without publishing, creating, or deleting anything. | No | `false` | +| `WorkingDirectory` | The working directory where the script will run from. | No | `.` | +| `UsePRTitleAsReleaseName` | Use the PR title as the release name (otherwise the version string is used). | No | `false` | +| `UsePRBodyAsReleaseNotes` | Use the PR body as the release notes (otherwise `--generate-notes` is used). | No | `true` | +| `UsePRTitleAsNotesHeading` | Prefix the release notes with the PR title as an H1 heading linking to the PR. | No | `true` | diff --git a/action.yml b/action.yml index bfadb09..768898c 100644 --- a/action.yml +++ b/action.yml @@ -1,7 +1,14 @@ name: Publish-PSModule -description: Publish a PowerShell module to the PowerShell Gallery. +description: Publish a pre-versioned PowerShell module artifact to the PowerShell Gallery and GitHub Releases. author: PSModule +# BREAKING CHANGE (v3.0.0): +# This action no longer calculates the next version or mutates the module manifest. The artifact passed in +# must already contain the final ModuleVersion (and Prerelease tag, if any). Use PSModule/Resolve-PSModuleVersion +# to compute the version and PSModule/Build-PSModule (v5+) to stamp it before testing. The version-calculation +# inputs (AutoPatching, IncrementalPrerelease, DatePrereleaseFormat, VersionPrefix, *Labels, ReleaseType, IgnoreLabels) +# have been removed. + inputs: Name: description: Name of the module to publish. @@ -17,42 +24,6 @@ inputs: description: Control whether to automatically delete the prerelease tags after the stable release is created. required: false default: 'true' - AutoPatching: - description: Control whether to automatically handle patches. If disabled, the action will only create a patch release if the pull request has a 'patch' label. - required: false - default: 'true' - IncrementalPrerelease: - description: Control whether to automatically increment the prerelease number. If disabled, the action will ensure only one prerelease exists for a given branch. - required: false - default: 'true' - DatePrereleaseFormat: - description: If specified, uses a date based prerelease scheme. The format should be a valid .NET format string like 'yyyyMMddHHmm'. - required: false - default: '' - VersionPrefix: - description: The prefix to use for the version number. - required: false - default: v - MajorLabels: - description: A comma separated list of labels that trigger a major release. - required: false - default: major, breaking - MinorLabels: - description: A comma separated list of labels that trigger a minor release. - required: false - default: minor, feature - PatchLabels: - description: A comma separated list of labels that trigger a patch release. - required: false - default: patch, fix - IgnoreLabels: - description: A comma separated list of labels that do not trigger a release. - required: false - default: NoRelease - ReleaseType: - description: The type of release to create. Values are 'Release' (stable), 'Prerelease', or 'None'. - required: false - default: Release WhatIf: description: If specified, the action will only log the changes it would make, but will not actually create or delete any releases or tags. required: false @@ -85,34 +56,13 @@ runs: run: | Install-PSResource -Name Microsoft.PowerShell.PSResourceGet -Repository PSGallery -TrustRepository - - name: Initialize Publish Context - id: init - shell: pwsh - working-directory: ${{ inputs.WorkingDirectory }} - env: - PSMODULE_PUBLISH_PSMODULE_INPUT_Name: ${{ inputs.Name }} - PSMODULE_PUBLISH_PSMODULE_INPUT_AutoCleanup: ${{ inputs.AutoCleanup }} - PSMODULE_PUBLISH_PSMODULE_INPUT_AutoPatching: ${{ inputs.AutoPatching }} - PSMODULE_PUBLISH_PSMODULE_INPUT_DatePrereleaseFormat: ${{ inputs.DatePrereleaseFormat }} - PSMODULE_PUBLISH_PSMODULE_INPUT_IgnoreLabels: ${{ inputs.IgnoreLabels }} - PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseType: ${{ inputs.ReleaseType }} - PSMODULE_PUBLISH_PSMODULE_INPUT_IncrementalPrerelease: ${{ inputs.IncrementalPrerelease }} - PSMODULE_PUBLISH_PSMODULE_INPUT_MajorLabels: ${{ inputs.MajorLabels }} - PSMODULE_PUBLISH_PSMODULE_INPUT_MinorLabels: ${{ inputs.MinorLabels }} - PSMODULE_PUBLISH_PSMODULE_INPUT_PatchLabels: ${{ inputs.PatchLabels }} - PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix: ${{ inputs.VersionPrefix }} - PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }} - run: ${{ github.action_path }}/src/init.ps1 - - name: Download module artifact - if: env.PUBLISH_CONTEXT_ShouldPublish == 'true' || inputs.WhatIf == 'true' uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: module path: ${{ inputs.ModulePath }} - name: Publish Module - if: env.PUBLISH_CONTEXT_ShouldPublish == 'true' || inputs.WhatIf == 'true' shell: pwsh working-directory: ${{ inputs.WorkingDirectory }} env: @@ -126,7 +76,7 @@ runs: run: ${{ github.action_path }}/src/publish.ps1 - name: Cleanup Prereleases - if: env.PUBLISH_CONTEXT_ShouldCleanup == 'true' || inputs.WhatIf == 'true' + if: inputs.AutoCleanup == 'true' || inputs.WhatIf == 'true' shell: pwsh working-directory: ${{ inputs.WorkingDirectory }} env: diff --git a/src/cleanup.ps1 b/src/cleanup.ps1 index cc2fc6e..68286bf 100644 --- a/src/cleanup.ps1 +++ b/src/cleanup.ps1 @@ -5,22 +5,40 @@ $PSStyle.OutputRendering = 'Ansi' Import-Module -Name 'Helpers' -Force -$prereleaseName = $env:PUBLISH_CONTEXT_PrereleaseName -$prereleaseTagsToCleanup = $env:PUBLISH_CONTEXT_PrereleaseTagsToCleanup -$whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true' +#region Load inputs +LogGroup 'Load inputs' { + $whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true' -if ([string]::IsNullOrWhiteSpace($prereleaseName)) { - Write-Error 'PUBLISH_CONTEXT_PrereleaseName is not set. Run init.ps1 first.' - exit 1 -} + $githubEventJson = Get-Content $env:GITHUB_EVENT_PATH + $githubEvent = $githubEventJson | ConvertFrom-Json + $pull_request = $githubEvent.pull_request + if (-not $pull_request) { + throw 'GitHub event does not contain pull_request data. This script must be run from a pull_request event.' + } + $prHeadRef = $pull_request.head.ref + $prereleaseName = $prHeadRef -replace '[^a-zA-Z0-9]' -LogGroup "Cleanup prereleases for [$prereleaseName]" { - if ([string]::IsNullOrWhiteSpace($prereleaseTagsToCleanup)) { - Write-Host "No prereleases found to cleanup for [$prereleaseName]." + if ([string]::IsNullOrWhiteSpace($prereleaseName)) { + Write-Host "No prerelease tag derivable from PR head ref [$prHeadRef]. Nothing to cleanup." return } - $tagsToDelete = $prereleaseTagsToCleanup -split ',' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } + Write-Host "PR head ref: [$prHeadRef]" + Write-Host "Prerelease name: [$prereleaseName]" + Write-Host "WhatIf: [$whatIf]" +} +#endregion Load inputs + +#region Find prereleases to cleanup +LogGroup "Find prereleases to cleanup for [$prereleaseName]" { + $releases = gh release list --json 'createdAt,isDraft,isLatest,isPrerelease,name,publishedAt,tagName' | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + Write-Error 'Failed to list releases for the repository.' + exit $LASTEXITCODE + } + + $prereleasesToCleanup = $releases | Where-Object { $_.tagName -like "*$prereleaseName*" } + $tagsToDelete = @($prereleasesToCleanup | ForEach-Object { $_.tagName } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) if ($tagsToDelete.Count -eq 0) { Write-Host "No prereleases found to cleanup for [$prereleaseName]." @@ -29,8 +47,11 @@ LogGroup "Cleanup prereleases for [$prereleaseName]" { Write-Host "Found $($tagsToDelete.Count) prereleases to cleanup:" $tagsToDelete | ForEach-Object { Write-Host " - $_" } - Write-Host '' +} +#endregion Find prereleases to cleanup +#region Delete prereleases +LogGroup "Delete prereleases for [$prereleaseName]" { foreach ($tag in $tagsToDelete) { Write-Host "Deleting prerelease: [$tag]" if ($whatIf) { @@ -47,3 +68,4 @@ LogGroup "Cleanup prereleases for [$prereleaseName]" { Write-Host "::notice::Cleaned up $($tagsToDelete.Count) prerelease(s) for [$prereleaseName]." } +#endregion Delete prereleases diff --git a/src/init.ps1 b/src/init.ps1 deleted file mode 100644 index a02a411..0000000 --- a/src/init.ps1 +++ /dev/null @@ -1,406 +0,0 @@ -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'pull_request', - Justification = 'Variable is used in script blocks.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'prereleaseName', - Justification = 'Variable is used in script blocks.' -)] -[CmdletBinding()] -param() - -$PSStyle.OutputRendering = 'Ansi' - -Import-Module -Name 'Helpers' -Force - -LogGroup 'Install dependencies' { - $retryCount = 5 - $retryDelay = 10 - for ($i = 0; $i -lt $retryCount; $i++) { - try { - Install-PSResource -Name 'PSSemVer' -TrustRepository -Repository PSGallery - break - } catch { - Write-Warning "Installation of PSSemVer failed with error: $_" - if ($i -eq $retryCount - 1) { - throw - } - Write-Warning "Retrying in $retryDelay seconds..." - Start-Sleep -Seconds $retryDelay - } - } -} - -LogGroup 'Load inputs' { - $env:GITHUB_REPOSITORY_NAME = $env:GITHUB_REPOSITORY -replace '.+/' - - $name = if ([string]::IsNullOrEmpty($env:PSMODULE_PUBLISH_PSMODULE_INPUT_Name)) { - $env:GITHUB_REPOSITORY_NAME - } else { - $env:PSMODULE_PUBLISH_PSMODULE_INPUT_Name - } - Write-Host "Module name: [$name]" -} - -LogGroup 'Set configuration' { - $autoCleanup = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_AutoCleanup -eq 'true' - $autoPatching = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_AutoPatching -eq 'true' - $incrementalPrerelease = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_IncrementalPrerelease -eq 'true' - $datePrereleaseFormat = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_DatePrereleaseFormat - $versionPrefix = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_VersionPrefix - $whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true' - $ignoreLabels = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_IgnoreLabels -split ',' | ForEach-Object { $_.Trim() } - $releaseType = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_ReleaseType # 'Release', 'Prerelease', or 'None' - $majorLabels = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_MajorLabels -split ',' | ForEach-Object { $_.Trim() } - $minorLabels = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_MinorLabels -split ',' | ForEach-Object { $_.Trim() } - $patchLabels = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_PatchLabels -split ',' | ForEach-Object { $_.Trim() } - - if ($whatIf) { - $message = 'WhatIf mode is enabled. No actual releases will be created, ' + - 'no modules will be published, and no tags will be deleted.' - Write-Host "::warning::$message" - } - - Write-Host '-------------------------------------------------' - [pscustomobject]@{ - AutoCleanup = $autoCleanup - AutoPatching = $autoPatching - IncrementalPrerelease = $incrementalPrerelease - DatePrereleaseFormat = $datePrereleaseFormat - VersionPrefix = $versionPrefix - WhatIf = $whatIf - IgnoreLabels = $ignoreLabels - ReleaseType = $releaseType - MajorLabels = $majorLabels - MinorLabels = $minorLabels - PatchLabels = $patchLabels - } | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -LogGroup 'Event information - JSON' { - $githubEventJson = Get-Content $env:GITHUB_EVENT_PATH - Write-Host '-------------------------------------------------' - $githubEventJson | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -LogGroup 'Event information - Object' { - $githubEvent = $githubEventJson | ConvertFrom-Json - $pull_request = $githubEvent.pull_request - Write-Host '-------------------------------------------------' - $githubEvent | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -LogGroup 'Event information - Details' { - if (-not $pull_request) { - throw 'GitHub event does not contain pull_request data. This script must be run from a pull_request event.' - } - $prHeadRef = $pull_request.head.ref - - Write-Host '-------------------------------------------------' - [PSCustomObject]@{ - PRHeadRef = $prHeadRef - ReleaseType = $releaseType - } | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -LogGroup 'Pull request - details' { - Write-Host '-------------------------------------------------' - $pull_request | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -LogGroup 'Pull request - Labels' { - $labels = @() - $labels += $pull_request.labels.name - Write-Host '-------------------------------------------------' - $labels | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -LogGroup 'Determine release configuration' { - $prereleaseName = $prHeadRef -replace '[^a-zA-Z0-9]' - - # Validate ReleaseType input from Get-PSModuleSettings. - # The ReleaseType is pre-calculated based on PR state and labels by the settings action, - # so we trust it here rather than recalculating from labels. - $validReleaseTypes = @('Release', 'Prerelease', 'None') - if ([string]::IsNullOrWhiteSpace($releaseType)) { - Write-Error "ReleaseType input is required. Valid values are: $($validReleaseTypes -join ', ')" - exit 1 - } - if ($releaseType -notin $validReleaseTypes) { - Write-Error "Invalid ReleaseType: [$releaseType]. Valid values are: $($validReleaseTypes -join ', ')" - exit 1 - } - - $createRelease = $releaseType -eq 'Release' - $createPrerelease = $releaseType -eq 'Prerelease' - $shouldPublish = $createRelease -or $createPrerelease - - # Check for ignore labels that override the release type - $ignoreRelease = ($labels | Where-Object { $ignoreLabels -contains $_ }).Count -gt 0 - if ($ignoreRelease -and $shouldPublish) { - Write-Host 'Ignoring release creation due to ignore label.' - $shouldPublish = $false - } - - # Determine version bump type from labels (when publishing or in WhatIf mode to show what would happen) - $majorRelease = $false - $minorRelease = $false - $patchRelease = $false - $hasVersionBump = $false - - if ($shouldPublish -or $whatIf) { - $majorRelease = ($labels | Where-Object { $majorLabels -contains $_ }).Count -gt 0 - $minorRelease = ($labels | Where-Object { $minorLabels -contains $_ }).Count -gt 0 -and -not $majorRelease - $patchRelease = ( - ($labels | Where-Object { $patchLabels -contains $_ } - ).Count -gt 0 -or $autoPatching) -and -not $majorRelease -and -not $minorRelease - - $hasVersionBump = $majorRelease -or $minorRelease -or $patchRelease - if (-not $hasVersionBump -and $shouldPublish) { - Write-Host 'No version bump label found and AutoPatching is disabled. Skipping publish.' - $shouldPublish = $false - } - } elseif (-not $ignoreRelease) { - Write-Host "ReleaseType is [$releaseType]. No publishing required." - } - - Write-Host '-------------------------------------------------' - [PSCustomObject]@{ - ReleaseType = $releaseType - AutoCleanup = $autoCleanup - ShouldPublish = $shouldPublish - CreateRelease = $createRelease - CreatePrerelease = $createPrerelease - CreateMajor = $majorRelease - CreateMinor = $minorRelease - CreatePatch = $patchRelease - } | Format-List | Out-String - Write-Host '-------------------------------------------------' -} -#endregion Calculate release type - -# Initialize version-related variables with defaults -$newVersion = $null -$releases = @() -$prereleaseTagsToCleanup = '' - -# Fetch releases if publishing OR if cleanup is enabled OR WhatIf mode (to show what would happen) -if ($shouldPublish -or $autoCleanup -or $whatIf) { - #region Get releases - LogGroup 'Get all releases - GitHub' { - $releases = gh release list --json 'createdAt,isDraft,isLatest,isPrerelease,name,publishedAt,tagName' | ConvertFrom-Json - if ($LASTEXITCODE -ne 0) { - Write-Error 'Failed to list all releases for the repo.' - exit $LASTEXITCODE - } - Write-Host '-------------------------------------------------' - $releases | Select-Object -Property name, isPrerelease, isLatest, publishedAt | Format-Table | Out-String - Write-Host '-------------------------------------------------' - } - #endregion Get releases -} - -# Version calculation is needed when publishing OR in WhatIf mode (to show what would be created) -if ($shouldPublish -or $whatIf) { - #region Get versions - LogGroup 'Get latest version - GitHub' { - $latestRelease = $releases | Where-Object { $_.isLatest -eq $true } - Write-Host '-------------------------------------------------' - $latestRelease | Format-List | Out-String - $ghReleaseVersionString = $latestRelease.tagName - if (-not [string]::IsNullOrEmpty($ghReleaseVersionString)) { - $ghReleaseVersion = New-PSSemVer -Version $ghReleaseVersionString - } else { - Write-Warning 'Could not find the latest release version. Using ''0.0.0'' as the version.' - $ghReleaseVersion = New-PSSemVer -Version '0.0.0' - } - Write-Host '-------------------------------------------------' - Write-Host 'GitHub version:' - Write-Host $ghReleaseVersion.ToString() - Write-Host '-------------------------------------------------' - } - - LogGroup 'Get latest version - PSGallery' { - $count = 5 - $delay = 10 - for ($i = 1; $i -le $count; $i++) { - try { - Write-Host "Finding module [$name] in the PowerShell Gallery." - $latest = Find-PSResource -Name $name -Repository PSGallery -Verbose:$false - Write-Host "$($latest | Format-Table | Out-String)" - break - } catch { - if ($i -eq $count) { - Write-Warning "Failed to find the module [$name] in the PowerShell Gallery." - Write-Warning $_.Exception.Message - } - Start-Sleep -Seconds $delay - } - } - if ($latest.Version) { - $psGalleryVersion = New-PSSemVer -Version ($latest.Version).ToString() - } else { - Write-Warning 'Could not find module online. Using ''0.0.0'' as the version.' - $psGalleryVersion = New-PSSemVer -Version '0.0.0' - } - Write-Host '-------------------------------------------------' - Write-Host 'PSGallery version:' - Write-Host $psGalleryVersion.ToString() - Write-Host '-------------------------------------------------' - } - - LogGroup 'Get latest version' { - Write-Host "GitHub: [$($ghReleaseVersion.ToString())]" - Write-Host "PSGallery: [$($psGalleryVersion.ToString())]" - $latestVersion = New-PSSemVer -Version ($psGalleryVersion, $ghReleaseVersion | Sort-Object -Descending | Select-Object -First 1) - Write-Host '-------------------------------------------------' - Write-Host 'Latest version:' - Write-Host ($latestVersion | Format-Table | Out-String) - Write-Host $latestVersion.ToString() - Write-Host '-------------------------------------------------' - } - - LogGroup 'Calculate new version' { - # - Increment based on label on PR - $newVersion = New-PSSemVer -Version $latestVersion - $newVersion.Prefix = $versionPrefix - if ($majorRelease) { - Write-Host 'Incrementing major version.' - $newVersion.BumpMajor() - } elseif ($minorRelease) { - Write-Host 'Incrementing minor version.' - $newVersion.BumpMinor() - } elseif ($patchRelease) { - Write-Host 'Incrementing patch version.' - $newVersion.BumpPatch() - } else { - Write-Host 'No version bump required.' - } - - Write-Host "Partial new version: [$newVersion]" - - if ($createPrerelease -and $hasVersionBump) { - Write-Host "Adding a prerelease tag to the version using the branch name [$prereleaseName]." - Write-Host ($releases | Where-Object { $_.tagName -like "*$prereleaseName*" } | - Select-Object -Property name, isPrerelease, isLatest, publishedAt | Format-Table -AutoSize | Out-String) - - $newVersion.Prerelease = $prereleaseName - Write-Host "Partial new version: [$newVersion]" - - if (-not [string]::IsNullOrEmpty($datePrereleaseFormat)) { - Write-Host "Using date-based prerelease: [$datePrereleaseFormat]." - $newVersion.Prerelease += "$(Get-Date -Format $datePrereleaseFormat)" - Write-Host "Partial new version: [$newVersion]" - } - - if ($incrementalPrerelease) { - # Find the latest prerelease version - $newVersionString = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)" - - # PowerShell Gallery - $params = @{ - Name = $name - Version = '*' - Prerelease = $true - Repository = 'PSGallery' - Verbose = $false - ErrorAction = 'SilentlyContinue' - } - Write-Host 'Finding the latest prerelease version in the PowerShell Gallery.' - Write-Host ($params | Format-Table | Out-String) - $psGalleryPrereleases = Find-PSResource @params - $psGalleryPrereleases = $psGalleryPrereleases | Where-Object { $_.Version -like "$newVersionString" } - $psGalleryPrereleases = $psGalleryPrereleases | Where-Object { $_.Prerelease -like "$prereleaseName*" } - $latestPSGalleryPrerelease = $psGalleryPrereleases.Prerelease | ForEach-Object { - [int]($_ -replace $prereleaseName) - } | Sort-Object | Select-Object -Last 1 - Write-Host "PSGallery prerelease: [$latestPSGalleryPrerelease]" - - # GitHub - $ghPrereleases = $releases | Where-Object { $_.tagName -like "*$newVersionString*" } - $ghPrereleases = $ghPrereleases | Where-Object { $_.tagName -like "*$prereleaseName*" } - $latestGHPrereleases = $ghPrereleases.tagName | ForEach-Object { - $number = $_ - $number = $number -replace '\.' - $number = ($number -split $prereleaseName, 2)[-1] - [int]$number - } | Sort-Object | Select-Object -Last 1 - Write-Host "GitHub prerelease: [$latestGHPrereleases]" - - # Handle null values explicitly to ensure Math.Max works correctly - if ($null -eq $latestPSGalleryPrerelease) { $latestPSGalleryPrerelease = 0 } - if ($null -eq $latestGHPrereleases) { $latestGHPrereleases = 0 } - - $latestPrereleaseNumber = [Math]::Max($latestPSGalleryPrerelease, $latestGHPrereleases) - $latestPrereleaseNumber++ - $latestPrereleaseNumber = ([string]$latestPrereleaseNumber).PadLeft(3, '0') - $newVersion.Prerelease += $latestPrereleaseNumber - } - } - Write-Host '-------------------------------------------------' - Write-Host 'New version:' - $newVersion | Format-Table | Out-String - Write-Host '-------------------------------------------------' - Write-Host $newVersion.ToString() - Write-Host '-------------------------------------------------' - } - #endregion Calculate new version -} - -#region Find prereleases to cleanup -# This runs independently when cleanup is enabled, even if not publishing -if ($autoCleanup) { - LogGroup 'Find prereleases to cleanup' { - $prereleasesToCleanup = $releases | Where-Object { $_.tagName -like "*$prereleaseName*" } - Write-Host '-------------------------------------------------' - $prereleasesToCleanup | Select-Object -Property name, publishedAt, isPrerelease, isLatest | Format-Table | Out-String - Write-Host '-------------------------------------------------' - $prereleaseTagsToCleanup = ($prereleasesToCleanup | ForEach-Object { $_.tagName }) -join ',' - Write-Host "Prereleases to cleanup: [$prereleaseTagsToCleanup]" - } -} -#endregion Find prereleases to cleanup - -LogGroup 'Store context in environment variables' { - # Store values for subsequent steps by appending to GITHUB_ENV - $newVersionString = if ($newVersion) { $newVersion.ToString() } else { '' } - - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_ShouldPublish=$($shouldPublish.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_ShouldCleanup=$($autoCleanup.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_CreateRelease=$($createRelease.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_CreatePrerelease=$($createPrerelease.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_MajorRelease=$($majorRelease.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_MinorRelease=$($minorRelease.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_PatchRelease=$($patchRelease.ToString().ToLower())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_NewVersion=$newVersionString" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_PrereleaseName=$prereleaseName" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_PrereleaseTagsToCleanup=$prereleaseTagsToCleanup" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_PRNumber=$($pull_request.number.ToString())" - Add-Content -Path $env:GITHUB_ENV -Value "PUBLISH_CONTEXT_PRHeadRef=$prHeadRef" - - Write-Host '-------------------------------------------------' - Write-Host 'Stored environment variables:' - [PSCustomObject]@{ - ShouldPublish = $shouldPublish - ShouldCleanup = $autoCleanup - CreateRelease = $createRelease - CreatePrerelease = $createPrerelease - MajorRelease = $majorRelease - MinorRelease = $minorRelease - PatchRelease = $patchRelease - NewVersion = $newVersionString - PrereleaseName = $prereleaseName - PrereleaseTagsToCleanup = $prereleaseTagsToCleanup - PRNumber = $pull_request.number - PRHeadRef = $prHeadRef - } | Format-List | Out-String - Write-Host '-------------------------------------------------' -} - -Write-Host "Context initialization complete. ShouldPublish=[$shouldPublish], ShouldCleanup=[$autoCleanup]" diff --git a/src/publish.ps1 b/src/publish.ps1 index 7226c1f..a683857 100644 --- a/src/publish.ps1 +++ b/src/publish.ps1 @@ -21,6 +21,7 @@ $PSStyle.OutputRendering = 'Ansi' Import-Module -Name 'Helpers' -Force +#region Load inputs LogGroup 'Load inputs' { $env:GITHUB_REPOSITORY_NAME = $env:GITHUB_REPOSITORY -replace '.+/' @@ -31,43 +32,18 @@ LogGroup 'Load inputs' { } $modulePath = Resolve-Path -Path "$env:PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath/$name" | Select-Object -ExpandProperty Path $apiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey - - Write-Host "Module name: [$name]" - Write-Host "Module path: [$modulePath]" -} - -LogGroup 'Load publish context from environment' { - $shouldPublish = $env:PUBLISH_CONTEXT_ShouldPublish -eq 'true' - $createRelease = $env:PUBLISH_CONTEXT_CreateRelease -eq 'true' - $createPrerelease = $env:PUBLISH_CONTEXT_CreatePrerelease -eq 'true' - $newVersionString = $env:PUBLISH_CONTEXT_NewVersion - $prNumber = $env:PUBLISH_CONTEXT_PRNumber - $prHeadRef = $env:PUBLISH_CONTEXT_PRHeadRef $whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true' $usePRBodyAsReleaseNotes = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes -eq 'true' $usePRTitleAsReleaseName = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName -eq 'true' $usePRTitleAsNotesHeading = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsNotesHeading -eq 'true' - if ([string]::IsNullOrWhiteSpace($newVersionString)) { - Write-Error 'PUBLISH_CONTEXT_NewVersion is not set. Run init.ps1 first.' - exit 1 - } - - $newVersion = New-PSSemVer -Version $newVersionString - - Write-Host '-------------------------------------------------' - [PSCustomObject]@{ - ShouldPublish = $shouldPublish - CreateRelease = $createRelease - CreatePrerelease = $createPrerelease - NewVersion = $newVersion.ToString() - PRNumber = $prNumber - PRHeadRef = $prHeadRef - WhatIf = $whatIf - } | Format-List | Out-String - Write-Host '-------------------------------------------------' + Write-Host "Module name: [$name]" + Write-Host "Module path: [$modulePath]" + Write-Host "WhatIf: [$whatIf]" } +#endregion Load inputs +#region Load PR information LogGroup 'Load PR information' { $githubEventJson = Get-Content $env:GITHUB_EVENT_PATH $githubEvent = $githubEventJson | ConvertFrom-Json @@ -75,9 +51,15 @@ LogGroup 'Load PR information' { if (-not $pull_request) { throw 'GitHub event does not contain pull_request data. This script must be run from a pull_request event.' } + $prNumber = $pull_request.number + $prHeadRef = $pull_request.head.ref } +#endregion Load PR information -LogGroup 'Validate manifest and set module path' { +#region Resolve version from manifest +# The manifest was stamped with the final version during Build-PSModule. This step is read-only +# to preserve artifact integrity (the tested artifact is identical to the published artifact). +LogGroup 'Resolve version from manifest' { Add-PSModulePath -Path (Split-Path -Path $modulePath -Parent) $manifestFilePath = Join-Path $modulePath "$name.psd1" Write-Host "Module manifest file path: [$manifestFilePath]" @@ -85,32 +67,45 @@ LogGroup 'Validate manifest and set module path' { Write-Error "Module manifest file not found at [$manifestFilePath]" exit 1 } -} -LogGroup 'Update module manifest' { - Write-Host 'Bump module version -> module metadata: Update-ModuleMetadata' - $manifestNewVersion = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)" - Set-ModuleManifest -Path $manifestFilePath -ModuleVersion $manifestNewVersion -Verbose:$false - if ($createPrerelease) { - Write-Host "Prerelease is: [$($newVersion.Prerelease)]" - Set-ModuleManifest -Path $manifestFilePath -Prerelease $($newVersion.Prerelease) -Verbose:$false + Show-FileContent -Path $manifestFilePath + + $manifest = Test-ModuleManifest -Path $manifestFilePath -ErrorAction Stop + $moduleVersion = "$($manifest.Version.Major).$($manifest.Version.Minor).$($manifest.Version.Build)" + $manifestData = Import-PowerShellDataFile -Path $manifestFilePath + $prerelease = $manifestData.PrivateData.PSData.Prerelease + if ([string]::IsNullOrWhiteSpace($prerelease)) { + $prerelease = '' + $createPrerelease = $false + } else { + $createPrerelease = $true } - Show-FileContent -Path $manifestFilePath + $releaseTag = if ($createPrerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion } + + [PSCustomObject]@{ + ModuleVersion = $moduleVersion + Prerelease = $prerelease + CreatePrerelease = $createPrerelease + ReleaseTag = $releaseTag + PRNumber = $prNumber + PRHeadRef = $prHeadRef + } | Format-List | Out-String } +#endregion Resolve version from manifest +#region Install module dependencies LogGroup 'Install module dependencies' { Resolve-PSModuleDependency -ManifestFilePath $manifestFilePath } +#endregion Install module dependencies -LogGroup 'Publish-ToPSGallery' { +#region Publish to PSGallery +LogGroup 'Publish to PSGallery' { $releaseType = if ($createPrerelease) { 'New prerelease' } else { 'New release' } - if ($createPrerelease) { - $publishPSVersion = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)-$($newVersion.Prerelease)" - } else { - $publishPSVersion = "$($newVersion.Major).$($newVersion.Minor).$($newVersion.Patch)" - } + $publishPSVersion = if ($createPrerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion } $psGalleryReleaseLink = "https://www.powershellgallery.com/packages/$name/$publishPSVersion" + Write-Host 'Publish module to PowerShell Gallery using API key from environment.' if ($whatIf) { Write-Host "Publish-PSResource -Path $modulePath -Repository PSGallery -ApiKey ***" @@ -122,6 +117,7 @@ LogGroup 'Publish-ToPSGallery' { exit 1 } } + if ($whatIf) { Write-Host ( "gh pr comment $prNumber -b " + @@ -136,83 +132,96 @@ LogGroup 'Publish-ToPSGallery' { } } } +#endregion Publish to PSGallery -LogGroup 'New-GitHubRelease' { - Write-Host 'Create new GitHub release' - $releaseCreateCommand = @('release', 'create', $newVersion.ToString()) +#region Create GitHub release with module artifact attached +# A zip of the published module is uploaded so the GitHub Release page exposes the exact bytes +# that were tested and pushed to the PowerShell Gallery. +LogGroup 'Create GitHub release' { + $releaseCreateCommand = @('release', 'create', $releaseTag) $notesFilePath = $null - # Add title parameter if ($usePRTitleAsReleaseName -and $pull_request.title) { - $prTitle = $pull_request.title - $releaseCreateCommand += @('--title', $prTitle) - Write-Host "Using PR title as release name: [$prTitle]" + $releaseCreateCommand += @('--title', $pull_request.title) + Write-Host "Using PR title as release name: [$($pull_request.title)]" } else { - $releaseCreateCommand += @('--title', $newVersion.ToString()) + $releaseCreateCommand += @('--title', $releaseTag) } # Build release notes content. Uses temp file to avoid escaping issues with special characters. # Precedence rules for the three UsePR* parameters: # 1. UsePRTitleAsNotesHeading + UsePRBodyAsReleaseNotes: Creates "# Title (#PR)\n\nBody" format. - # Requires both parameters enabled AND both PR title and body to be present. - # 2. UsePRBodyAsReleaseNotes only: Uses PR body as-is for release notes. - # Takes effect when heading option is disabled/missing title, but body is available. - # 3. Fallback: Auto-generates notes via GitHub's --generate-notes when no PR content is used. + # 2. UsePRBodyAsReleaseNotes only: Uses PR body as-is. + # 3. Fallback: Auto-generates notes via GitHub's --generate-notes. if ($usePRTitleAsNotesHeading -and $usePRBodyAsReleaseNotes -and $pull_request.title -and $pull_request.body) { - # Path 1: Full PR-based notes with title as H1 heading and PR number link - $prTitle = $pull_request.title - $prBody = $pull_request.body - $notes = "# $prTitle (#$prNumber)`n`n$prBody" + $notes = "# $($pull_request.title) (#$prNumber)`n`n$($pull_request.body)" $notesFilePath = [System.IO.Path]::GetTempFileName() Set-Content -Path $notesFilePath -Value $notes -Encoding utf8 $releaseCreateCommand += @('--notes-file', $notesFilePath) Write-Host 'Using PR title as H1 heading with link and body as release notes' } elseif ($usePRBodyAsReleaseNotes -and $pull_request.body) { - # Path 2: PR body only - no heading, just the body content - $prBody = $pull_request.body $notesFilePath = [System.IO.Path]::GetTempFileName() - Set-Content -Path $notesFilePath -Value $prBody -Encoding utf8 + Set-Content -Path $notesFilePath -Value $pull_request.body -Encoding utf8 $releaseCreateCommand += @('--notes-file', $notesFilePath) Write-Host 'Using PR body as release notes' } else { - # Path 3: Fallback to GitHub's auto-generated release notes $releaseCreateCommand += @('--generate-notes') } - # Add remaining parameters if ($createPrerelease) { $releaseCreateCommand += @('--target', $prHeadRef, '--prerelease') } if ($whatIf) { Write-Host "WhatIf: gh $($releaseCreateCommand -join ' ')" + $releaseURL = "https://github.com/$env:GITHUB_REPOSITORY/releases/tag/$releaseTag" } else { $releaseURL = gh @releaseCreateCommand if ($LASTEXITCODE -ne 0) { - Write-Error "Failed to create the release [$newVersion]." + Write-Error "Failed to create the release [$releaseTag]." exit $LASTEXITCODE } } - # Clean up temporary notes file if created if ($notesFilePath -and (Test-Path -Path $notesFilePath)) { Remove-Item -Path $notesFilePath -Force } - $releaseType = if ($createPrerelease) { 'New prerelease' } else { 'New release' } + # Attach the built module as a release artifact so consumers can download the exact + # bytes that were tested and published to the PowerShell Gallery. + $zipFileName = "$name-$publishPSVersion.zip" + $zipPath = Join-Path ([System.IO.Path]::GetTempPath()) $zipFileName + if (Test-Path -Path $zipPath) { + Remove-Item -Path $zipPath -Force + } + Write-Host "Compressing module to [$zipPath]" + Compress-Archive -Path (Join-Path $modulePath '*') -DestinationPath $zipPath -Force + + if ($whatIf) { + Write-Host "WhatIf: gh release upload $releaseTag $zipPath --clobber" + } else { + gh release upload $releaseTag $zipPath --clobber + if ($LASTEXITCODE -ne 0) { + Write-Error "Failed to upload module artifact to release [$releaseTag]." + exit $LASTEXITCODE + } + Write-Host "::notice title=📦 Attached module artifact to release::$zipFileName" + } + if ($whatIf) { Write-Host ( "gh pr comment $prNumber -b " + - "'✅ $releaseType`: GitHub - $name $newVersion'" + "'✅ $releaseType`: GitHub - $name $releaseTag'" ) } else { - gh pr comment $prNumber -b "✅ $releaseType`: GitHub - [$name $newVersion]($releaseURL)" + gh pr comment $prNumber -b "✅ $releaseType`: GitHub - [$name $releaseTag]($releaseURL)" if ($LASTEXITCODE -ne 0) { Write-Error 'Failed to comment on the pull request.' exit $LASTEXITCODE } } - Write-Host "::notice title=✅ $releaseType`: GitHub - $name $newVersion::$releaseURL" + Write-Host "::notice title=✅ $releaseType`: GitHub - $name $releaseTag::$releaseURL" } +#endregion Create GitHub release -Write-Host "Publishing complete. Version: [$($newVersion.ToString())]" +Write-Host "Publishing complete. Version: [$releaseTag]"