From 833a9b85e01ece0ba1b69db51f389788827f5e15 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:05:47 +0200 Subject: [PATCH 01/10] =?UTF-8?q?=F0=9F=9A=A6=20[Feature]:=20Trigger=20rel?= =?UTF-8?q?ease=20paths=20on=20main=20pushes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Release.yml | 11 ++++++++++- .github/workflows/Workflow-Test-Default.yml | 10 +++++++++- .github/workflows/Workflow-Test-WithManifest.yml | 10 +++++++++- 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 1e346454..567a197b 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -1,8 +1,17 @@ name: Release -run-name: "Release - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}" +run-name: "Release - ${{ github.event_name }} [${{ github.event.pull_request.title || github.event.head_commit.message || github.ref_name }}] by @${{ github.actor }}" on: + push: + branches: + - main + paths: + - '.github/actions/**' + - '.github/workflows/**' + - '!.github/workflows/Release.yml' + - '!.github/workflows/Linter.yml' + - '!.github/workflows/Workflow-Test-*' pull_request: branches: - main diff --git a/.github/workflows/Workflow-Test-Default.yml b/.github/workflows/Workflow-Test-Default.yml index 511b4b1c..f73dec5d 100644 --- a/.github/workflows/Workflow-Test-Default.yml +++ b/.github/workflows/Workflow-Test-Default.yml @@ -1,9 +1,17 @@ name: Workflow-Test [Default] -run-name: 'Workflow-Test [Default] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}' +run-name: 'Workflow-Test [Default] - ${{ github.event_name }} [${{ github.event.pull_request.title || github.event.head_commit.message || github.ref_name }}] by @${{ github.actor }}' on: workflow_dispatch: + push: + branches: + - main + paths: + - '.github/actions/**' + - '.github/workflows/**' + - '!.github/workflows/Release.yml' + - '!.github/workflows/Linter.yml' pull_request: paths: - '.github/actions/**' diff --git a/.github/workflows/Workflow-Test-WithManifest.yml b/.github/workflows/Workflow-Test-WithManifest.yml index 82f80f55..3e47e93c 100644 --- a/.github/workflows/Workflow-Test-WithManifest.yml +++ b/.github/workflows/Workflow-Test-WithManifest.yml @@ -1,9 +1,17 @@ name: Workflow-Test [WithManifest] -run-name: 'Workflow-Test [WithManifest] - [${{ github.event.pull_request.title }} #${{ github.event.pull_request.number }}] by @${{ github.actor }}' +run-name: 'Workflow-Test [WithManifest] - ${{ github.event_name }} [${{ github.event.pull_request.title || github.event.head_commit.message || github.ref_name }}] by @${{ github.actor }}' on: workflow_dispatch: + push: + branches: + - main + paths: + - '.github/actions/**' + - '.github/workflows/**' + - '!.github/workflows/Release.yml' + - '!.github/workflows/Linter.yml' pull_request: paths: - '.github/actions/**' From 24854c96cf82ce7337787a182295cdf90e772266 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:08:11 +0200 Subject: [PATCH 02/10] =?UTF-8?q?=F0=9F=A7=AD=20[Feature]:=20Normalize=20r?= =?UTF-8?q?elease=20context=20for=20push=20events?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/Settings.schema.json | 4 + .../actions/Get-PSModuleSettings/src/main.ps1 | 123 ++++++++++++++---- 2 files changed, 102 insertions(+), 25 deletions(-) diff --git a/.github/actions/Get-PSModuleSettings/src/Settings.schema.json b/.github/actions/Get-PSModuleSettings/src/Settings.schema.json index fdbc9013..968e18de 100644 --- a/.github/actions/Get-PSModuleSettings/src/Settings.schema.json +++ b/.github/actions/Get-PSModuleSettings/src/Settings.schema.json @@ -142,6 +142,10 @@ "type": "boolean", "description": "Automatically apply patches" }, + "AllowDirectPushRelease": { + "type": "boolean", + "description": "Allow a stable release from a default-branch push that has no associated pull request. Defaults to false." + }, "IncrementalPrerelease": { "type": "boolean", "description": "Use incremental prerelease versioning" diff --git a/.github/actions/Get-PSModuleSettings/src/main.ps1 b/.github/actions/Get-PSModuleSettings/src/main.ps1 index 36aeba1a..9b2c774b 100644 --- a/.github/actions/Get-PSModuleSettings/src/main.ps1 +++ b/.github/actions/Get-PSModuleSettings/src/main.ps1 @@ -190,6 +190,7 @@ $settings = [pscustomobject]@{ Skip = $settings.Publish.Module.Skip ?? $false AutoCleanup = $settings.Publish.Module.AutoCleanup ?? $true AutoPatching = $settings.Publish.Module.AutoPatching ?? $true + AllowDirectPushRelease = $settings.Publish.Module.AllowDirectPushRelease ?? $false IncrementalPrerelease = $settings.Publish.Module.IncrementalPrerelease ?? $true DatePrereleaseFormat = $settings.Publish.Module.DatePrereleaseFormat ?? '' VersionPrefix = $settings.Publish.Module.VersionPrefix ?? 'v' @@ -226,43 +227,103 @@ LogGroup 'Calculate Job Run Conditions:' { $eventData | ConvertTo-Json -Depth 10 | Out-String } + $defaultBranch = $eventData.Repository.default_branch + $eventName = $env:GITHUB_EVENT_NAME + $isPR = $eventName -eq 'pull_request' + $isPush = $eventName -eq 'push' $pullRequestAction = $eventData.Action + $commitSha = if ($isPush) { $eventData.After ?? $env:GITHUB_SHA } else { $env:GITHUB_SHA } + $pushBranch = if ($isPush) { $eventData.Ref -replace '^refs/heads/', '' } else { '' } + $isPushToDefaultBranch = $isPush -and $pushBranch -eq $defaultBranch $pullRequest = $eventData.PullRequest - $pullRequestIsMerged = $pullRequest.Merged - $targetBranch = $pullRequest.Base.Ref - $defaultBranch = $eventData.Repository.default_branch + + if ($isPush -and $commitSha) { + LogGroup "Resolve pull request for commit [$commitSha]" { + $owner = $env:GITHUB_REPOSITORY_OWNER + $repo = $env:GITHUB_REPOSITORY_NAME + $response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$commitSha/pulls" -Method GET + $associatedPullRequests = @($response.Response) + $pullRequest = $associatedPullRequests | + Where-Object { $_.Base.Ref -eq $defaultBranch } | + Sort-Object -Property @{ Expression = { $_.'merge_commit_sha' -eq $commitSha }; Descending = $true }, + @{ Expression = { $_.'merged_at' }; Descending = $true } | + Select-Object -First 1 + + if ($pullRequest) { + Write-Host "Resolved pull request #$($pullRequest.Number) from commit [$commitSha]." + } else { + Write-Host "::notice::No pull request is associated with commit [$commitSha]." + } + } + } + + $pullRequestIsMerged = if ($null -ne $pullRequest.Merged) { + [bool]$pullRequest.Merged + } else { + -not [string]::IsNullOrWhiteSpace($pullRequest.'merged_at') + } + $targetBranch = if ($pullRequest) { $pullRequest.Base.Ref } elseif ($isPush) { $pushBranch } else { '' } $isTargetDefaultBranch = $targetBranch -eq $defaultBranch + $pullRequestContext = if ($pullRequest) { + [pscustomobject]@{ + Number = $pullRequest.Number + Title = $pullRequest.Title + Body = $pullRequest.Body + HeadRef = $pullRequest.Head.Ref + BaseRef = $pullRequest.Base.Ref + Labels = @($pullRequest.Labels.Name) + Merged = $pullRequestIsMerged + MergeCommitSha = $pullRequest.'merge_commit_sha' + HtmlUrl = $pullRequest.'html_url' + } + } else { + $null + } + + $settings | Add-Member -MemberType NoteProperty -Name Context -Value ([pscustomobject]@{ + EventName = $eventName + EventAction = $pullRequestAction + CommitSha = $commitSha + Ref = if ($isPush) { $eventData.Ref } else { $env:GITHUB_REF } + DefaultBranch = $defaultBranch + IsPushToDefaultBranch = $isPushToDefaultBranch + PullRequest = $pullRequestContext + }) -Force Write-Host 'GitHub event inputs:' [pscustomobject]@{ - GITHUB_EVENT_NAME = $env:GITHUB_EVENT_NAME + GITHUB_EVENT_NAME = $eventName GITHUB_EVENT_ACTION = $pullRequestAction GITHUB_EVENT_PULL_REQUEST_MERGED = $pullRequestIsMerged + CommitSha = $commitSha + PushBranch = $pushBranch TargetBranch = $targetBranch DefaultBranch = $defaultBranch IsTargetDefaultBranch = $isTargetDefaultBranch + IsPushToDefaultBranch = $isPushToDefaultBranch + AssociatedPullRequest = $pullRequestContext.Number } | Format-List | Out-String - $isPR = $env:GITHUB_EVENT_NAME -eq 'pull_request' $isOpenOrUpdatedPR = $isPR -and $pullRequestAction -in @('opened', 'reopened', 'synchronize', 'labeled', 'unlabeled') + $isClosedPR = $isPR -and $pullRequestAction -eq 'closed' $isAbandonedPR = $isPR -and $pullRequestAction -eq 'closed' -and $pullRequestIsMerged -ne $true $isMergedPR = $isPR -and $pullRequestAction -eq 'closed' -and $pullRequestIsMerged -eq $true - $isNotAbandonedPR = -not $isAbandonedPR + $hasPullRequestContext = $null -ne $pullRequestContext # Check if a prerelease label exists on the PR $prereleaseLabels = $settings.Publish.Module.PrereleaseLabels -split ',' | ForEach-Object { $_.Trim() } - $prLabels = @($pullRequest.labels.name) + $prLabels = @($pullRequestContext.Labels) $hasPrereleaseLabel = ($prLabels | Where-Object { $prereleaseLabels -contains $_ }).Count -gt 0 $isOpenOrLabeledPR = $isPR -and $pullRequestAction -in @('opened', 'reopened', 'synchronize', 'labeled') # Check if important files have changed in the PR # Important files are determined by the configured ImportantFilePatterns setting $hasImportantChanges = $false - if ($isPR -and $pullRequest.Number) { + if ($pullRequestContext.Number) { LogGroup 'Check for Important File Changes' { $owner = $env:GITHUB_REPOSITORY_OWNER $repo = $env:GITHUB_REPOSITORY_NAME - $prNumber = $pullRequest.Number + $prNumber = $pullRequestContext.Number Write-Host "Fetching changed files for PR #$prNumber..." $changedFiles = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/pulls/$prNumber/files" -Method GET | @@ -332,10 +393,17 @@ If you believe this is incorrect, please verify that your changes are in the cor } } } + } elseif ($isPush) { + $hasImportantChanges = $isPushToDefaultBranch -and $settings.Publish.Module.AllowDirectPushRelease + if ($hasImportantChanges) { + Write-Host 'Direct push release is explicitly enabled; treating the push as having important changes.' + } else { + Write-Host 'Push has no associated pull request; stable publishing is disabled by default.' + } } else { - # Not a PR event or no PR number - consider as having important changes (e.g., workflow_dispatch, schedule) + # Preserve build/test behavior for workflow_dispatch and schedule events. $hasImportantChanges = $true - Write-Host 'Not a PR event or missing PR number - treating as having important changes' + Write-Host 'Non-PR event - treating as having important changes' } # Prerelease requires both: prerelease label AND important file changes @@ -344,9 +412,12 @@ If you believe this is incorrect, please verify that your changes are in the cor # Determine ReleaseType - what type of release to create # Values: 'Release', 'Prerelease', 'None' - # Release only happens when important files changed (actual module code/docs) - # Merged PRs without important changes should only trigger cleanup, not a new release - $releaseType = if ($isMergedPR -and $isTargetDefaultBranch -and $hasImportantChanges) { + # Stable releases are push-driven. A direct push requires explicit opt-in. + $releaseType = if ( + $isPushToDefaultBranch -and + $hasImportantChanges -and + ($hasPullRequestContext -or $settings.Publish.Module.AllowDirectPushRelease) + ) { 'Release' } elseif ($shouldPrerelease) { 'Prerelease' @@ -358,10 +429,13 @@ If you believe this is incorrect, please verify that your changes are in the cor isPR = $isPR isOpenOrUpdatedPR = $isOpenOrUpdatedPR isOpenOrLabeledPR = $isOpenOrLabeledPR + isClosedPR = $isClosedPR isAbandonedPR = $isAbandonedPR isMergedPR = $isMergedPR - isNotAbandonedPR = $isNotAbandonedPR + isPush = $isPush + isPushToDefaultBranch = $isPushToDefaultBranch isTargetDefaultBranch = $isTargetDefaultBranch + hasPullRequestContext = $hasPullRequestContext hasPrereleaseLabel = $hasPrereleaseLabel shouldPrerelease = $shouldPrerelease ReleaseType = $releaseType @@ -532,23 +606,22 @@ $settings.Test.Module | Add-Member -MemberType NoteProperty -Name Suites -Value # Calculate job-specific conditions and add to settings LogGroup 'Calculate Job Run Conditions:' { # Calculate if prereleases should be cleaned up: - # True if (Release, merged PR to default branch, or Abandoned PR) AND user has AutoCleanup enabled (defaults to true) - # Even if no important files changed, we still want to cleanup prereleases when merging to default branch - $isReleaseOrMergedOrAbandoned = ( + # Closed PRs only clean up prereleases. Push runs also clean up the associated PR channel. + $shouldCleanupEvent = ( ($releaseType -eq 'Release') -or - ($isMergedPR -and $isTargetDefaultBranch) -or - $isAbandonedPR + $isClosedPR -or + ($isPushToDefaultBranch -and $hasPullRequestContext) ) - $shouldAutoCleanup = $isReleaseOrMergedOrAbandoned -and ($settings.Publish.Module.AutoCleanup -eq $true) + $shouldAutoCleanup = $shouldCleanupEvent -and ($settings.Publish.Module.AutoCleanup -eq $true) # Update Publish.Module with computed release values $settings.Publish.Module | Add-Member -MemberType NoteProperty -Name ReleaseType -Value $releaseType -Force $settings.Publish.Module.AutoCleanup = $shouldAutoCleanup # For open PRs, we only want to run build/test stages if important files changed. - # For merged PRs, workflow_dispatch, schedule - $hasImportantChanges is already true. + # Closed PR events are cleanup-only. workflow_dispatch and schedule retain build/test behavior. # Note: $shouldPrerelease already requires $hasImportantChanges, so no separate check needed. - $shouldRunBuildTest = $isNotAbandonedPR -and $hasImportantChanges + $shouldRunBuildTest = (-not $isClosedPR) -and $hasImportantChanges # Check if setup/teardown scripts exist in the repository $hasBeforeAllScript = Test-Path -Path 'tests/BeforeAll.ps1' @@ -607,8 +680,8 @@ LogGroup 'Calculate Job Run Conditions:' { $settings.Publish.Module | Add-Member -MemberType NoteProperty -Name Desired -Value (($releaseType -ne 'None') -or $shouldAutoCleanup) -Force $settings.Publish.Module | Add-Member -MemberType NoteProperty -Name Enabled -Value (($releaseType -ne 'None') -or $shouldAutoCleanup) -Force $settings.Publish | Add-Member -MemberType NoteProperty -Name Site -Value ([pscustomobject]@{ - Desired = $isMergedPR -and $isTargetDefaultBranch -and $hasImportantChanges - Enabled = $isMergedPR -and $isTargetDefaultBranch -and $hasImportantChanges + Desired = $releaseType -eq 'Release' + Enabled = $releaseType -eq 'Release' }) -Force $settings | Add-Member -MemberType NoteProperty -Name HasImportantChanges -Value $hasImportantChanges From ae75caf870706be406e8d30d9d60485d7813b847 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:08:41 +0200 Subject: [PATCH 03/10] =?UTF-8?q?=F0=9F=94=96=20[Feature]:=20Resolve=20ver?= =?UTF-8?q?sions=20from=20normalized=20PR=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/Resolve-PSModuleVersion.Helpers.psm1 | 58 +++++++++++++++---- .../Resolve-PSModuleVersion/src/main.ps1 | 2 +- 2 files changed, 49 insertions(+), 11 deletions(-) diff --git a/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 b/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 index d4f2f450..5899b569 100644 --- a/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 +++ b/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 @@ -123,26 +123,63 @@ function Get-PublishConfiguration { function Get-GitHubPullRequest { <# .SYNOPSIS - Reads and validates the GitHub pull request from the event payload. + Reads normalized pull request context from settings, with event payload fallback. .DESCRIPTION - Loads the GitHub event from the input override or from the event path file. On a - pull_request event it returns the pull request head ref and labels. On any other - event (for example workflow_dispatch or schedule) there is no pull request, so it - returns $null and the caller resolves the current version without a version bump. + Uses the normalized context produced by Get-PSModuleSettings so push events can use + pull request labels and head ref resolved from the commit SHA. Raw pull_request event + payloads remain supported for callers that invoke this action directly. .OUTPUTS - PSCustomObject with HeadRef and Labels properties for a pull_request event, or - $null when the event has no pull request (non-PR events). + PSCustomObject with HeadRef and Labels properties, or $null when no release context + is available. .EXAMPLE $pullRequest = Get-GitHubPullRequest #> [CmdletBinding()] [OutputType([PSCustomObject])] - param() + param( + # The JSON string containing normalized workflow settings. + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] $SettingsJson + ) LogGroup 'Event information' { + $settings = $SettingsJson | ConvertFrom-Json + $pr = $settings.Context.PullRequest + if ($pr) { + $labels = @($pr.Labels) + + Write-Host 'Using normalized pull request context from settings.' + Write-Host '-------------------------------------------------' + Write-Host ([PSCustomObject]@{ + PRNumber = $pr.Number + PRHeadRef = $pr.HeadRef + Labels = $labels -join ', ' + } | Format-List | Out-String) + Write-Host '-------------------------------------------------' + + return [PSCustomObject]@{ + Number = $pr.Number + HeadRef = $pr.HeadRef + Labels = $labels + } + } + + if ( + $settings.Context.IsPushToDefaultBranch -and + $settings.Publish.Module.ReleaseType -eq 'Release' + ) { + Write-Host 'Using explicitly enabled direct-push release context from settings.' + return [PSCustomObject]@{ + Number = $null + HeadRef = $settings.Context.DefaultBranch + Labels = @() + } + } + $eventJsonInput = $env:PSMODULE_RESOLVE_PSMODULEVERSION_INPUT_EventJson $githubEvent = if (-not [string]::IsNullOrWhiteSpace($eventJsonInput)) { $eventJsonInput | ConvertFrom-Json @@ -152,8 +189,8 @@ function Get-GitHubPullRequest { $pr = $githubEvent.pull_request if (-not $pr) { - Write-Host 'GitHub event does not contain pull_request data (non-PR event, e.g. workflow_dispatch or schedule).' - Write-Host 'No pull request context is available; the caller keeps the current version without a bump.' + Write-Host 'No normalized or event pull request context is available.' + Write-Host 'The caller keeps the current version without a bump.' return $null } @@ -168,6 +205,7 @@ function Get-GitHubPullRequest { Write-Host '-------------------------------------------------' [PSCustomObject]@{ + Number = $pr.number HeadRef = $pr.head.ref Labels = $labels } diff --git a/.github/actions/Resolve-PSModuleVersion/src/main.ps1 b/.github/actions/Resolve-PSModuleVersion/src/main.ps1 index da6d70e6..c950eda6 100644 --- a/.github/actions/Resolve-PSModuleVersion/src/main.ps1 +++ b/.github/actions/Resolve-PSModuleVersion/src/main.ps1 @@ -8,7 +8,7 @@ Import-Module -Name "$PSScriptRoot/Resolve-PSModuleVersion.Helpers.psm1" -Force $actionInput = Read-ActionInput $config = Get-PublishConfiguration -SettingsJson $actionInput.SettingsJson -$pullRequest = Get-GitHubPullRequest +$pullRequest = Get-GitHubPullRequest -SettingsJson $actionInput.SettingsJson $decision = if ($null -eq $pullRequest) { # Non-PR event (for example workflow_dispatch or schedule): there are no pull request From b3efa0449aff22e6715d2e4ea8053ffc44d3473d Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:09:43 +0200 Subject: [PATCH 04/10] =?UTF-8?q?=F0=9F=93=A6=20[Feature]:=20Publish=20fro?= =?UTF-8?q?m=20normalized=20release=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Cleanup-PSModulePrereleases/action.yml | 5 ++ .../src/cleanup.ps1 | 18 +++-- .github/actions/Publish-PSModule/action.yml | 5 ++ .../actions/Publish-PSModule/src/publish.ps1 | 68 +++++++++++++------ .github/workflows/Publish-Module.yml | 2 + 5 files changed, 71 insertions(+), 27 deletions(-) diff --git a/.github/actions/Cleanup-PSModulePrereleases/action.yml b/.github/actions/Cleanup-PSModulePrereleases/action.yml index 3c076b45..0194d117 100644 --- a/.github/actions/Cleanup-PSModulePrereleases/action.yml +++ b/.github/actions/Cleanup-PSModulePrereleases/action.yml @@ -7,6 +7,10 @@ inputs: description: Control whether to automatically delete prerelease tags. required: false default: 'true' + PullRequest: + description: Normalized pull request context JSON from Get-PSModuleSettings. + required: false + default: '' WhatIf: description: If specified, the action will only log the changes it would make. required: false @@ -28,5 +32,6 @@ runs: working-directory: ${{ inputs.WorkingDirectory }} env: PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_WhatIf: ${{ inputs.WhatIf }} + PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_PullRequest: ${{ inputs.PullRequest }} PSMODULE_CLEANUP_PSMODULEPRERELEASES_CONTEXT_ReleaseTag: ${{ env.PSMODULE_PUBLISH_PSMODULE_CONTEXT_ReleaseTag }} run: ${{ github.action_path }}/src/cleanup.ps1 diff --git a/.github/actions/Cleanup-PSModulePrereleases/src/cleanup.ps1 b/.github/actions/Cleanup-PSModulePrereleases/src/cleanup.ps1 index a10bdf1d..d61da5b2 100644 --- a/.github/actions/Cleanup-PSModulePrereleases/src/cleanup.ps1 +++ b/.github/actions/Cleanup-PSModulePrereleases/src/cleanup.ps1 @@ -9,13 +9,19 @@ Import-Module -Name 'PSModule' -Force LogGroup 'Load inputs' { $whatIf = $env:PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_WhatIf -eq 'true' - $githubEventJson = Get-Content -Raw $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.' + $pullRequestJson = $env:PSMODULE_CLEANUP_PSMODULEPRERELEASES_INPUT_PullRequest + $prHeadRef = if (-not [string]::IsNullOrWhiteSpace($pullRequestJson) -and $pullRequestJson -ne 'null') { + ($pullRequestJson | ConvertFrom-Json).HeadRef + } else { + $githubEventJson = Get-Content -Raw $env:GITHUB_EVENT_PATH + $githubEvent = $githubEventJson | ConvertFrom-Json + $githubEvent.pull_request.head.ref + } + + if ([string]::IsNullOrWhiteSpace($prHeadRef)) { + Write-Host '::notice::No pull request head ref is available. Nothing to cleanup.' + exit 0 } - $prHeadRef = $pull_request.head.ref $prereleaseName = $prHeadRef -replace '[^a-zA-Z0-9]' if ([string]::IsNullOrWhiteSpace($prereleaseName)) { diff --git a/.github/actions/Publish-PSModule/action.yml b/.github/actions/Publish-PSModule/action.yml index 0ff0a534..3160dcc0 100644 --- a/.github/actions/Publish-PSModule/action.yml +++ b/.github/actions/Publish-PSModule/action.yml @@ -13,6 +13,10 @@ inputs: APIKey: description: PowerShell Gallery API Key. required: true + PullRequest: + description: Normalized pull request context JSON from Get-PSModuleSettings. + required: false + default: '' 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 @@ -61,6 +65,7 @@ runs: PSMODULE_PUBLISH_PSMODULE_INPUT_Name: ${{ inputs.Name }} PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath: ${{ inputs.ModulePath }} PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey: ${{ inputs.APIKey }} + PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest: ${{ inputs.PullRequest }} PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }} PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes: ${{ inputs.UsePRBodyAsReleaseNotes }} PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName: ${{ inputs.UsePRTitleAsReleaseName }} diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index b9f76254..0f8417e3 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -65,18 +65,37 @@ LogGroup 'Load inputs' { } #endregion Load inputs -#region Load PR information -LogGroup 'Load PR information' { - $githubEventJson = Get-Content -Raw $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.' +#region Load release context +LogGroup 'Load release context' { + $pullRequestJson = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest + $pullRequest = if (-not [string]::IsNullOrWhiteSpace($pullRequestJson) -and $pullRequestJson -ne 'null') { + $pullRequestJson | ConvertFrom-Json + } else { + $githubEventJson = Get-Content -Raw $env:GITHUB_EVENT_PATH + $githubEvent = $githubEventJson | ConvertFrom-Json + if ($githubEvent.pull_request) { + [pscustomobject]@{ + Number = $githubEvent.pull_request.number + Title = $githubEvent.pull_request.title + Body = $githubEvent.pull_request.body + HeadRef = $githubEvent.pull_request.head.ref + } + } + } + + if ($pullRequest) { + $prNumber = $pullRequest.Number + $prHeadRef = $pullRequest.HeadRef + Write-Host "Pull request: [#$prNumber]" + Write-Host "PR head ref: [$prHeadRef]" + } else { + $prNumber = $null + $prHeadRef = $env:GITHUB_REF_NAME + Write-Host 'No pull request context is available; PR-derived release metadata and comments are disabled.' + Write-Host "Release ref: [$prHeadRef]" } - $prNumber = $pull_request.number - $prHeadRef = $pull_request.head.ref } -#endregion Load PR information +#endregion Load release context #region Resolve version from manifest # The manifest was stamped with the final version during Build-PSModule. This step is read-only @@ -129,6 +148,11 @@ LogGroup 'Resolve version from manifest' { $createPrerelease = $true } + if ($createPrerelease -and [string]::IsNullOrWhiteSpace($prHeadRef)) { + Write-Error 'A prerelease requires pull request context with a head ref.' + exit 1 + } + $releaseTag = if ($createPrerelease) { "$moduleVersion-$prerelease" } else { $moduleVersion } [PSCustomObject]@{ @@ -169,18 +193,20 @@ LogGroup 'Publish to PSGallery' { } } - if ($whatIf) { + if ($whatIf -and $prNumber) { Write-Host ( "gh pr comment $prNumber -b " + "'✅ $releaseType`: PowerShell Gallery - [$name $publishPSVersion]($psGalleryReleaseLink)'" ) - } else { + } elseif (-not $whatIf -and $prNumber) { Write-Host "::notice title=✅ $releaseType`: PowerShell Gallery - $name $publishPSVersion::$psGalleryReleaseLink" gh pr comment $prNumber -b "✅ $releaseType`: PowerShell Gallery - [$name $publishPSVersion]($psGalleryReleaseLink)" if ($LASTEXITCODE -ne 0) { Write-Error 'Failed to comment on the pull request.' exit $LASTEXITCODE } + } else { + Write-Host "::notice title=✅ $releaseType`: PowerShell Gallery - $name $publishPSVersion::$psGalleryReleaseLink" } } #endregion Publish to PSGallery @@ -192,9 +218,9 @@ LogGroup 'Create GitHub release' { $releaseCreateCommand = @('release', 'create', $releaseTag) $notesFilePath = $null - if ($usePRTitleAsReleaseName -and $pull_request.title) { - $releaseCreateCommand += @('--title', $pull_request.title) - Write-Host "Using PR title as release name: [$($pull_request.title)]" + if ($usePRTitleAsReleaseName -and $pullRequest.Title) { + $releaseCreateCommand += @('--title', $pullRequest.Title) + Write-Host "Using PR title as release name: [$($pullRequest.Title)]" } else { $releaseCreateCommand += @('--title', $releaseTag) } @@ -204,15 +230,15 @@ LogGroup 'Create GitHub release' { # 1. UsePRTitleAsNotesHeading + UsePRBodyAsReleaseNotes: Creates "# Title (#PR)\n\nBody" format. # 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) { - $notes = "# $($pull_request.title) (#$prNumber)`n`n$($pull_request.body)" + if ($usePRTitleAsNotesHeading -and $usePRBodyAsReleaseNotes -and $pullRequest.Title -and $pullRequest.Body) { + $notes = "# $($pullRequest.Title) (#$prNumber)`n`n$($pullRequest.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) { + } elseif ($usePRBodyAsReleaseNotes -and $pullRequest.Body) { $notesFilePath = [System.IO.Path]::GetTempFileName() - Set-Content -Path $notesFilePath -Value $pull_request.body -Encoding utf8 + Set-Content -Path $notesFilePath -Value $pullRequest.Body -Encoding utf8 $releaseCreateCommand += @('--notes-file', $notesFilePath) Write-Host 'Using PR body as release notes' } else { @@ -267,9 +293,9 @@ LogGroup 'Create GitHub release' { } } - if ($whatIf) { + if ($whatIf -and $prNumber) { Write-Host "gh pr comment $prNumber -b '✅ $($releaseType): GitHub - $name $releaseTag'" - } else { + } elseif (-not $whatIf -and $prNumber) { gh pr comment $prNumber -b "✅ $releaseType`: GitHub - [$name $releaseTag]($releaseURL)" if ($LASTEXITCODE -ne 0) { Write-Error 'Failed to comment on the pull request.' diff --git a/.github/workflows/Publish-Module.yml b/.github/workflows/Publish-Module.yml index ee1bbcb6..c4a9856c 100644 --- a/.github/workflows/Publish-Module.yml +++ b/.github/workflows/Publish-Module.yml @@ -46,6 +46,7 @@ jobs: Name: ${{ fromJson(inputs.Settings).Name }} ModulePath: outputs/module APIKey: ${{ secrets.APIKey }} + PullRequest: ${{ toJson(fromJson(inputs.Settings).Context.PullRequest) }} WhatIf: ${{ github.repository == 'PSModule/Process-PSModule' }} UsePRTitleAsReleaseName: ${{ fromJson(inputs.Settings).Publish.Module.UsePRTitleAsReleaseName }} UsePRBodyAsReleaseNotes: ${{ fromJson(inputs.Settings).Publish.Module.UsePRBodyAsReleaseNotes }} @@ -60,4 +61,5 @@ jobs: with: WhatIf: ${{ github.repository == 'PSModule/Process-PSModule' }} AutoCleanup: ${{ fromJson(inputs.Settings).Publish.Module.AutoCleanup }} + PullRequest: ${{ toJson(fromJson(inputs.Settings).Context.PullRequest) }} WorkingDirectory: ${{ fromJson(inputs.Settings).WorkingDirectory }} From b5ebe124d783887d80c13a5e66f8011478698146 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:13:31 +0200 Subject: [PATCH 05/10] =?UTF-8?q?=F0=9F=A7=AA=20[Test]:=20Cover=20push=20a?= =?UTF-8?q?nd=20PR=20release=20routing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/Get-PSModuleSettings.Helpers.psm1 | 77 +++++++++++++++++++ .../actions/Get-PSModuleSettings/src/main.ps1 | 64 ++++++--------- .../Get-PSModuleSettings.Helpers.Tests.ps1 | 75 ++++++++++++++++++ .../tests/ReleaseContext.Tests.ps1 | 64 +++++++++++++++ 4 files changed, 241 insertions(+), 39 deletions(-) create mode 100644 .github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 create mode 100644 .github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 create mode 100644 .github/actions/Resolve-PSModuleVersion/tests/ReleaseContext.Tests.ps1 diff --git a/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 new file mode 100644 index 00000000..a09ba65f --- /dev/null +++ b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 @@ -0,0 +1,77 @@ +function Resolve-WorkflowEventRouting { + <# + .SYNOPSIS + Resolves release and execution routing from normalized GitHub event state. + #> + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter(Mandatory)] + [string] $EventName, + + [Parameter()] + [string] $EventAction, + + [Parameter()] + [bool] $PullRequestIsMerged, + + [Parameter()] + [bool] $IsTargetDefaultBranch, + + [Parameter()] + [bool] $IsPushToDefaultBranch, + + [Parameter()] + [bool] $HasPullRequestContext, + + [Parameter()] + [bool] $HasImportantChanges, + + [Parameter()] + [bool] $HasPrereleaseLabel, + + [Parameter()] + [bool] $AllowDirectPushRelease + ) + + $isPR = $EventName -eq 'pull_request' + $isPush = $EventName -eq 'push' + $isOpenOrUpdatedPR = $isPR -and $EventAction -in @('opened', 'reopened', 'synchronize', 'labeled', 'unlabeled') + $isOpenOrLabeledPR = $isPR -and $EventAction -in @('opened', 'reopened', 'synchronize', 'labeled') + $isClosedPR = $isPR -and $EventAction -eq 'closed' + $isAbandonedPR = $isClosedPR -and -not $PullRequestIsMerged + $isMergedPR = $isClosedPR -and $PullRequestIsMerged + $shouldPrerelease = $isOpenOrLabeledPR -and $HasPrereleaseLabel -and $HasImportantChanges + + $releaseType = if ( + $IsPushToDefaultBranch -and + $HasImportantChanges -and + ($HasPullRequestContext -or $AllowDirectPushRelease) + ) { + 'Release' + } elseif ($shouldPrerelease) { + 'Prerelease' + } else { + 'None' + } + + [pscustomobject]@{ + IsPR = $isPR + IsPush = $isPush + IsOpenOrUpdatedPR = $isOpenOrUpdatedPR + IsOpenOrLabeledPR = $isOpenOrLabeledPR + IsClosedPR = $isClosedPR + IsAbandonedPR = $isAbandonedPR + IsMergedPR = $isMergedPR + IsTargetDefaultBranch = $IsTargetDefaultBranch + IsPushToDefaultBranch = $IsPushToDefaultBranch + ShouldPrerelease = $shouldPrerelease + ReleaseType = $releaseType + ShouldRunBuildTest = (-not $isClosedPR) -and $HasImportantChanges + ShouldCleanupEvent = ( + ($releaseType -eq 'Release') -or + $isClosedPR -or + ($IsPushToDefaultBranch -and $HasPullRequestContext) + ) + } +} diff --git a/.github/actions/Get-PSModuleSettings/src/main.ps1 b/.github/actions/Get-PSModuleSettings/src/main.ps1 index 9b2c774b..393ab1ae 100644 --- a/.github/actions/Get-PSModuleSettings/src/main.ps1 +++ b/.github/actions/Get-PSModuleSettings/src/main.ps1 @@ -1,4 +1,5 @@ 'powershell-yaml', 'Hashtable' | Install-PSResource -Repository PSGallery -TrustRepository +Import-Module -Name "$PSScriptRoot/Get-PSModuleSettings.Helpers.psm1" -Force $name = $env:PSMODULE_GET_SETTINGS_INPUT_Name $settingsPath = $env:PSMODULE_GET_SETTINGS_INPUT_SettingsPath @@ -300,21 +301,17 @@ LogGroup 'Calculate Job Run Conditions:' { TargetBranch = $targetBranch DefaultBranch = $defaultBranch IsTargetDefaultBranch = $isTargetDefaultBranch - IsPushToDefaultBranch = $isPushToDefaultBranch - AssociatedPullRequest = $pullRequestContext.Number + IsPushToDefaultBranch = $isPushToDefaultBranch + AssociatedPullRequest = $pullRequestContext.Number } | Format-List | Out-String $isOpenOrUpdatedPR = $isPR -and $pullRequestAction -in @('opened', 'reopened', 'synchronize', 'labeled', 'unlabeled') - $isClosedPR = $isPR -and $pullRequestAction -eq 'closed' - $isAbandonedPR = $isPR -and $pullRequestAction -eq 'closed' -and $pullRequestIsMerged -ne $true - $isMergedPR = $isPR -and $pullRequestAction -eq 'closed' -and $pullRequestIsMerged -eq $true $hasPullRequestContext = $null -ne $pullRequestContext # Check if a prerelease label exists on the PR $prereleaseLabels = $settings.Publish.Module.PrereleaseLabels -split ',' | ForEach-Object { $_.Trim() } $prLabels = @($pullRequestContext.Labels) $hasPrereleaseLabel = ($prLabels | Where-Object { $prereleaseLabels -contains $_ }).Count -gt 0 - $isOpenOrLabeledPR = $isPR -and $pullRequestAction -in @('opened', 'reopened', 'synchronize', 'labeled') # Check if important files have changed in the PR # Important files are determined by the configured ImportantFilePatterns setting @@ -406,35 +403,28 @@ If you believe this is incorrect, please verify that your changes are in the cor Write-Host 'Non-PR event - treating as having important changes' } - # Prerelease requires both: prerelease label AND important file changes - # No point creating a prerelease if only non-module files changed - $shouldPrerelease = $isOpenOrLabeledPR -and $hasPrereleaseLabel -and $hasImportantChanges - - # Determine ReleaseType - what type of release to create - # Values: 'Release', 'Prerelease', 'None' - # Stable releases are push-driven. A direct push requires explicit opt-in. - $releaseType = if ( - $isPushToDefaultBranch -and - $hasImportantChanges -and - ($hasPullRequestContext -or $settings.Publish.Module.AllowDirectPushRelease) - ) { - 'Release' - } elseif ($shouldPrerelease) { - 'Prerelease' - } else { - 'None' - } + $routing = Resolve-WorkflowEventRouting -EventName $eventName ` + -EventAction $pullRequestAction ` + -PullRequestIsMerged $pullRequestIsMerged ` + -IsTargetDefaultBranch $isTargetDefaultBranch ` + -IsPushToDefaultBranch $isPushToDefaultBranch ` + -HasPullRequestContext $hasPullRequestContext ` + -HasImportantChanges $hasImportantChanges ` + -HasPrereleaseLabel $hasPrereleaseLabel ` + -AllowDirectPushRelease $settings.Publish.Module.AllowDirectPushRelease + $shouldPrerelease = $routing.ShouldPrerelease + $releaseType = $routing.ReleaseType [pscustomobject]@{ - isPR = $isPR - isOpenOrUpdatedPR = $isOpenOrUpdatedPR - isOpenOrLabeledPR = $isOpenOrLabeledPR - isClosedPR = $isClosedPR - isAbandonedPR = $isAbandonedPR - isMergedPR = $isMergedPR - isPush = $isPush - isPushToDefaultBranch = $isPushToDefaultBranch - isTargetDefaultBranch = $isTargetDefaultBranch + isPR = $routing.IsPR + isOpenOrUpdatedPR = $routing.IsOpenOrUpdatedPR + isOpenOrLabeledPR = $routing.IsOpenOrLabeledPR + isClosedPR = $routing.IsClosedPR + isAbandonedPR = $routing.IsAbandonedPR + isMergedPR = $routing.IsMergedPR + isPush = $routing.IsPush + isPushToDefaultBranch = $routing.IsPushToDefaultBranch + isTargetDefaultBranch = $routing.IsTargetDefaultBranch hasPullRequestContext = $hasPullRequestContext hasPrereleaseLabel = $hasPrereleaseLabel shouldPrerelease = $shouldPrerelease @@ -607,11 +597,7 @@ $settings.Test.Module | Add-Member -MemberType NoteProperty -Name Suites -Value LogGroup 'Calculate Job Run Conditions:' { # Calculate if prereleases should be cleaned up: # Closed PRs only clean up prereleases. Push runs also clean up the associated PR channel. - $shouldCleanupEvent = ( - ($releaseType -eq 'Release') -or - $isClosedPR -or - ($isPushToDefaultBranch -and $hasPullRequestContext) - ) + $shouldCleanupEvent = $routing.ShouldCleanupEvent $shouldAutoCleanup = $shouldCleanupEvent -and ($settings.Publish.Module.AutoCleanup -eq $true) # Update Publish.Module with computed release values @@ -621,7 +607,7 @@ LogGroup 'Calculate Job Run Conditions:' { # For open PRs, we only want to run build/test stages if important files changed. # Closed PR events are cleanup-only. workflow_dispatch and schedule retain build/test behavior. # Note: $shouldPrerelease already requires $hasImportantChanges, so no separate check needed. - $shouldRunBuildTest = (-not $isClosedPR) -and $hasImportantChanges + $shouldRunBuildTest = $routing.ShouldRunBuildTest # Check if setup/teardown scripts exist in the repository $hasBeforeAllScript = Test-Path -Path 'tests/BeforeAll.ps1' diff --git a/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 new file mode 100644 index 00000000..47d6690c --- /dev/null +++ b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 @@ -0,0 +1,75 @@ +BeforeAll { + Import-Module "$PSScriptRoot/../src/Get-PSModuleSettings.Helpers.psm1" -Force +} + +Describe 'Resolve-WorkflowEventRouting' { + It 'routes an associated push to the default branch to a stable release' { + $result = Resolve-WorkflowEventRouting -EventName push ` + -IsTargetDefaultBranch $true ` + -IsPushToDefaultBranch $true ` + -HasPullRequestContext $true ` + -HasImportantChanges $true + + $result.ReleaseType | Should -Be 'Release' + $result.ShouldRunBuildTest | Should -BeTrue + $result.ShouldCleanupEvent | Should -BeTrue + } + + It 'does not publish an unassociated direct push by default' { + $result = Resolve-WorkflowEventRouting -EventName push ` + -IsTargetDefaultBranch $true ` + -IsPushToDefaultBranch $true ` + -HasImportantChanges $true + + $result.ReleaseType | Should -Be 'None' + } + + It 'allows an explicitly enabled direct push release' { + $result = Resolve-WorkflowEventRouting -EventName push ` + -IsTargetDefaultBranch $true ` + -IsPushToDefaultBranch $true ` + -HasImportantChanges $true ` + -AllowDirectPushRelease $true + + $result.ReleaseType | Should -Be 'Release' + } + + It 'routes a labeled open PR with important changes to a prerelease' { + $result = Resolve-WorkflowEventRouting -EventName pull_request ` + -EventAction labeled ` + -IsTargetDefaultBranch $true ` + -HasPullRequestContext $true ` + -HasImportantChanges $true ` + -HasPrereleaseLabel $true + + $result.ReleaseType | Should -Be 'Prerelease' + $result.ShouldRunBuildTest | Should -BeTrue + } + + It 'routes an abandoned PR to cleanup only' { + $result = Resolve-WorkflowEventRouting -EventName pull_request ` + -EventAction closed ` + -IsTargetDefaultBranch $true ` + -HasPullRequestContext $true ` + -HasImportantChanges $true + + $result.ReleaseType | Should -Be 'None' + $result.IsAbandonedPR | Should -BeTrue + $result.ShouldRunBuildTest | Should -BeFalse + $result.ShouldCleanupEvent | Should -BeTrue + } + + It 'routes a merged PR event to cleanup only' { + $result = Resolve-WorkflowEventRouting -EventName pull_request ` + -EventAction closed ` + -PullRequestIsMerged $true ` + -IsTargetDefaultBranch $true ` + -HasPullRequestContext $true ` + -HasImportantChanges $true + + $result.ReleaseType | Should -Be 'None' + $result.IsMergedPR | Should -BeTrue + $result.ShouldRunBuildTest | Should -BeFalse + $result.ShouldCleanupEvent | Should -BeTrue + } +} diff --git a/.github/actions/Resolve-PSModuleVersion/tests/ReleaseContext.Tests.ps1 b/.github/actions/Resolve-PSModuleVersion/tests/ReleaseContext.Tests.ps1 new file mode 100644 index 00000000..55075a61 --- /dev/null +++ b/.github/actions/Resolve-PSModuleVersion/tests/ReleaseContext.Tests.ps1 @@ -0,0 +1,64 @@ +BeforeAll { + function global:LogGroup { + <# + .SYNOPSIS + Executes a test script block without GitHub Actions log grouping. + #> + param( + [Parameter(Position = 0)] + [string] $Name, + + [Parameter(Position = 1)] + [scriptblock] $ScriptBlock + ) + + $null = $Name + & $ScriptBlock + } + + Import-Module "$PSScriptRoot/../src/Resolve-PSModuleVersion.Helpers.psm1" -Force +} + +AfterAll { + Remove-Item Function:/LogGroup -ErrorAction SilentlyContinue +} + +Describe 'Get-GitHubPullRequest' { + It 'reads push-derived pull request context from settings' { + $settings = @{ + Context = @{ + IsPushToDefaultBranch = $true + DefaultBranch = 'main' + PullRequest = @{ + Number = 390 + HeadRef = 'feature/push-release' + Labels = @('minor', 'prerelease') + } + } + Publish = @{ Module = @{ ReleaseType = 'Release' } } + } | ConvertTo-Json -Depth 5 + + $result = Get-GitHubPullRequest -SettingsJson $settings + + $result.Number | Should -Be 390 + $result.HeadRef | Should -Be 'feature/push-release' + $result.Labels | Should -Be @('minor', 'prerelease') + } + + It 'creates release context for an explicitly enabled direct push' { + $settings = @{ + Context = @{ + IsPushToDefaultBranch = $true + DefaultBranch = 'main' + PullRequest = $null + } + Publish = @{ Module = @{ ReleaseType = 'Release' } } + } | ConvertTo-Json -Depth 5 + + $result = Get-GitHubPullRequest -SettingsJson $settings + + $result.Number | Should -BeNullOrEmpty + $result.HeadRef | Should -Be 'main' + $result.Labels | Should -BeNullOrEmpty + } +} From 25d9225517a4454f39115db2e2764e8b67dc6037 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:14:02 +0200 Subject: [PATCH 06/10] =?UTF-8?q?=F0=9F=93=9D=20[Docs]:=20Describe=20push-?= =?UTF-8?q?driven=20workflow=20phases?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/Plan.yml | 2 +- .github/workflows/Release.yml | 1 + .github/workflows/workflow.yml | 64 +++++++++++++++++----------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/.github/workflows/Plan.yml b/.github/workflows/Plan.yml index 3fad50eb..fb1fa9e5 100644 --- a/.github/workflows/Plan.yml +++ b/.github/workflows/Plan.yml @@ -3,7 +3,7 @@ name: Plan # The Plan job is the single decision point for the workflow. # It runs two steps: # 1. Get-PSModuleSettings - loads and resolves configuration -# 2. Resolve-PSModuleVersion - calculates the next version from settings + PR labels +# 2. Resolve-PSModuleVersion - calculates the next version from settings + normalized PR context # The resolved version is merged into the publish phase object (Settings.Publish.Module.Resolution.*) by the Enrich-Settings step. # All downstream jobs receive one self-contained Settings JSON with no separate version outputs. diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 567a197b..7b28f097 100644 --- a/.github/workflows/Release.yml +++ b/.github/workflows/Release.yml @@ -38,6 +38,7 @@ permissions: jobs: Release: + if: github.event_name != 'pull_request' || github.event.action != 'closed' || github.event.pull_request.merged == false runs-on: ubuntu-latest steps: - name: Checkout repo diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 099afc34..34196464 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -72,8 +72,8 @@ permissions: jobs: # Runs on: # - ✅ Open/Updated PR - Always runs to load configuration - # - ✅ Merged PR - Always runs to load configuration - # - ✅ Abandoned PR - Always runs to load configuration + # - ✅ Push to default - Always runs to resolve stable-release context + # - ✅ Closed PR - Always runs to resolve prerelease cleanup # - ✅ Manual run - Always runs to load configuration Plan: uses: ./.github/workflows/Plan.yml @@ -88,8 +88,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Lints code changes in active PRs - # - ❌ Merged PR - No need to lint after merge + its a merge commit that causes issues with super-linter - # - ❌ Abandoned PR - No need to lint abandoned changes + # - ❌ Push to default - No need to lint after merge + its a merge commit that causes issues with super-linter + # - ❌ Closed PR - No need to lint closed changes # - ❌ Manual run - Only runs for PR events Lint-Repository: if: fromJson(needs.Plan.outputs.Settings).Linter.Repository.Enabled @@ -101,8 +101,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Builds module for testing - # - ✅ Merged PR - Builds module for publishing - # - ❌ Abandoned PR - Skips building abandoned changes + # - ✅ Push to default - Builds module for publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Builds module when manually triggered Build-Module: if: fromJson(needs.Plan.outputs.Settings).Build.Module.Enabled @@ -114,8 +114,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Tests source code changes - # - ✅ Merged PR - Tests source code before publishing - # - ❌ Abandoned PR - Skips testing abandoned changes + # - ✅ Push to default - Tests source code before publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Tests source code when manually triggered Test-SourceCode: if: fromJson(needs.Plan.outputs.Settings).Test.SourceCode.Enabled @@ -127,8 +127,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Lints source code changes - # - ✅ Merged PR - Lints source code before publishing - # - ❌ Abandoned PR - Skips linting abandoned changes + # - ✅ Push to default - Lints source code before publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Lints source code when manually triggered Lint-SourceCode: if: fromJson(needs.Plan.outputs.Settings).Linter.SourceCode.Enabled @@ -140,8 +140,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Tests built module - # - ✅ Merged PR - Tests built module before publishing - # - ❌ Abandoned PR - Skips testing abandoned changes + # - ✅ Push to default - Tests built module before publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Tests built module when manually triggered Test-Module: if: fromJson(needs.Plan.outputs.Settings).Test.PSModule.Enabled && needs.Build-Module.result == 'success' && !cancelled() @@ -154,8 +154,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Runs setup scripts before local module tests - # - ✅ Merged PR - Runs setup scripts before local module tests - # - ❌ Abandoned PR - Skips setup for abandoned changes + # - ✅ Push to default - Runs setup scripts before local module tests + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Runs setup scripts when manually triggered BeforeAll-ModuleLocal: if: fromJson(needs.Plan.outputs.Settings).Test.Module.BeforeAllEnabled && needs.Build-Module.result == 'success' && !cancelled() @@ -170,8 +170,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Tests module in local environment - # - ✅ Merged PR - Tests module in local environment before publishing - # - ❌ Abandoned PR - Skips testing abandoned changes + # - ✅ Push to default - Tests module in local environment before publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Tests module in local environment when manually triggered Test-ModuleLocal: if: fromJson(needs.Plan.outputs.Settings).Test.Module.MainEnabled && needs.Build-Module.result == 'success' && !cancelled() @@ -187,8 +187,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Runs teardown scripts after local module setup/tests - # - ✅ Merged PR - Runs teardown scripts after local module setup/tests - # - ✅ Abandoned PR - Runs teardown if local module setup/tests were started (cleanup) + # - ✅ Push to default - Runs teardown scripts after local module setup/tests + # - ❌ Closed PR - Workflow cleanup does not start module-local tests # - ✅ Manual run - Runs teardown scripts after local module setup/tests AfterAll-ModuleLocal: if: fromJson(needs.Plan.outputs.Settings).Test.Module.AfterAllEnabled && needs.BeforeAll-ModuleLocal.result != 'skipped' && always() @@ -204,8 +204,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Collects and reports test results - # - ✅ Merged PR - Collects and reports test results before publishing - # - ❌ Abandoned PR - Skips collecting results for abandoned changes + # - ✅ Push to default - Collects and reports test results before publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Collects and reports test results when manually triggered Get-TestResults: if: fromJson(needs.Plan.outputs.Settings).Test.TestResults.Enabled && needs.Plan.result == 'success' && always() && !cancelled() @@ -223,8 +223,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Calculates and reports code coverage - # - ✅ Merged PR - Calculates and reports code coverage before publishing - # - ❌ Abandoned PR - Skips coverage for abandoned changes + # - ✅ Push to default - Calculates and reports code coverage before publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Calculates and reports code coverage when manually triggered Get-CodeCoverage: if: fromJson(needs.Plan.outputs.Settings).Test.CodeCoverage.Enabled && needs.Plan.result == 'success' && always() && !cancelled() @@ -238,9 +238,9 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Only with prerelease label: publishes prerelease version - # - ✅ Merged PR - To default branch only: publishes release when all tests/coverage/build succeed - # - ✅ Abandoned PR - Cleans up prereleases for the abandoned branch (no version published) - # - ❌ Manual run - Only runs for PR events + # - ✅ Push to default - Publishes stable release when associated PR has important changes + # - ✅ Closed PR - Cleans up prereleases only (no stable version published) + # - ❌ Manual run - Does not publish without resolved release context Publish-Module: if: fromJson(needs.Plan.outputs.Settings).Publish.Module.Enabled && 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 @@ -256,8 +256,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Builds documentation for review - # - ✅ Merged PR - Builds documentation for publishing - # - ❌ Abandoned PR - Skips building docs for abandoned changes + # - ✅ Push to default - Builds documentation for publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Builds documentation when manually triggered Build-Docs: if: fromJson(needs.Plan.outputs.Settings).Build.Docs.Enabled @@ -270,8 +270,8 @@ jobs: # Runs on: # - ✅ Open/Updated PR - Builds site for preview - # - ✅ Merged PR - Builds site for publishing - # - ❌ Abandoned PR - Skips building site for abandoned changes + # - ✅ Push to default - Builds site for publishing + # - ❌ Closed PR - Cleanup only # - ✅ Manual run - Builds site when manually triggered Build-Site: if: fromJson(needs.Plan.outputs.Settings).Build.Site.Enabled @@ -284,9 +284,9 @@ jobs: # Runs on: # - ❌ Open/Updated PR - Site not published for PRs in progress - # - ✅ Merged PR - To default branch only: deploys site to GitHub Pages - # - ❌ Abandoned PR - Site not published for abandoned changes - # - ❌ Manual run - Only publishes on merged PRs to default branch + # - ✅ Push to default - Deploys site to GitHub Pages with stable releases + # - ❌ Closed PR - Site not published for closed changes + # - ❌ Manual run - Requires stable push release context Publish-Site: if: fromJson(needs.Plan.outputs.Settings).Publish.Site.Enabled && needs.Get-TestResults.result == 'success' && needs.Get-CodeCoverage.result == 'success' && needs.Build-Site.result == 'success' && !cancelled() uses: ./.github/workflows/Publish-Site.yml From db6af954ca2413cce5438dacfbf42afed2bfec22 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:14:50 +0200 Subject: [PATCH 07/10] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20[Fix]:=20Preserve?= =?UTF-8?q?=20CI=20for=20safe=20direct=20pushes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/Get-PSModuleSettings/src/main.ps1 | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/actions/Get-PSModuleSettings/src/main.ps1 b/.github/actions/Get-PSModuleSettings/src/main.ps1 index 393ab1ae..459a9f8c 100644 --- a/.github/actions/Get-PSModuleSettings/src/main.ps1 +++ b/.github/actions/Get-PSModuleSettings/src/main.ps1 @@ -391,11 +391,13 @@ If you believe this is incorrect, please verify that your changes are in the cor } } } elseif ($isPush) { - $hasImportantChanges = $isPushToDefaultBranch -and $settings.Publish.Module.AllowDirectPushRelease - if ($hasImportantChanges) { - Write-Host 'Direct push release is explicitly enabled; treating the push as having important changes.' + # Without PR files to compare, preserve push CI by running build/test. Publication is + # independently gated by AllowDirectPushRelease. + $hasImportantChanges = $true + if ($settings.Publish.Module.AllowDirectPushRelease) { + Write-Host 'Direct push release is explicitly enabled.' } else { - Write-Host 'Push has no associated pull request; stable publishing is disabled by default.' + Write-Host 'Push has no associated pull request; build/test will run but stable publishing is disabled by default.' } } else { # Preserve build/test behavior for workflow_dispatch and schedule events. From 5d2c55891d04ee3dc33c0d725b5c0a7979ea6660 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:15:41 +0200 Subject: [PATCH 08/10] =?UTF-8?q?=F0=9F=A7=B9=20[Maintenance]:=20Align=20r?= =?UTF-8?q?elease=20scripts=20with=20analyzer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/actions/Publish-PSModule/src/publish.ps1 | 8 -------- .../src/Resolve-PSModuleVersion.Helpers.psm1 | 2 ++ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index 0f8417e3..6406a130 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -14,14 +14,6 @@ 'PSUseDeclaredVarsMoreThanAssignments', 'usePRTitleAsNotesHeading', Justification = 'Variable is used in script blocks.' )] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'prNumber', - Justification = 'Variable is used in script blocks.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'prHeadRef', - Justification = 'Variable is used in script blocks.' -)] [CmdletBinding()] param() diff --git a/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 b/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 index 5899b569..2ff3a452 100644 --- a/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 +++ b/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 @@ -137,6 +137,8 @@ function Get-GitHubPullRequest { .EXAMPLE $pullRequest = Get-GitHubPullRequest #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'Parameter is used inside LogGroup script block.')] [CmdletBinding()] [OutputType([PSCustomObject])] param( From 0dcb7ca27c1949df4526f9697280396dd97de884 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:23:19 +0200 Subject: [PATCH 09/10] =?UTF-8?q?=F0=9F=94=92=20[Fix]:=20Bind=20stable=20r?= =?UTF-8?q?eleases=20to=20merged=20push=20context?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/Get-PSModuleSettings.Helpers.psm1 | 30 +++++++++++++++ .../actions/Get-PSModuleSettings/src/main.ps1 | 8 ++-- .../Get-PSModuleSettings.Helpers.Tests.ps1 | 38 +++++++++++++++++++ .github/actions/Publish-PSModule/action.yml | 5 +++ .../actions/Publish-PSModule/src/publish.ps1 | 7 ++++ .github/workflows/Publish-Module.yml | 1 + 6 files changed, 84 insertions(+), 5 deletions(-) diff --git a/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 index a09ba65f..c753bb62 100644 --- a/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 +++ b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 @@ -75,3 +75,33 @@ function Resolve-WorkflowEventRouting { ) } } + +function Select-PullRequestForPush { + <# + .SYNOPSIS + Selects the merged default-branch pull request associated with a pushed commit. + #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'Parameter is used inside a Sort-Object script block.')] + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter()] + [object[]] $PullRequest, + + [Parameter(Mandatory)] + [string] $DefaultBranch, + + [Parameter(Mandatory)] + [string] $CommitSha + ) + + $PullRequest | + Where-Object { + $_.Base.Ref -eq $DefaultBranch -and + -not [string]::IsNullOrWhiteSpace($_.'merged_at') + } | + Sort-Object -Property @{ Expression = { $_.'merge_commit_sha' -eq $CommitSha }; Descending = $true }, + @{ Expression = { $_.'merged_at' }; Descending = $true } | + Select-Object -First 1 +} diff --git a/.github/actions/Get-PSModuleSettings/src/main.ps1 b/.github/actions/Get-PSModuleSettings/src/main.ps1 index 459a9f8c..d9bc1b81 100644 --- a/.github/actions/Get-PSModuleSettings/src/main.ps1 +++ b/.github/actions/Get-PSModuleSettings/src/main.ps1 @@ -244,11 +244,9 @@ LogGroup 'Calculate Job Run Conditions:' { $repo = $env:GITHUB_REPOSITORY_NAME $response = Invoke-GitHubAPI -ApiEndpoint "/repos/$owner/$repo/commits/$commitSha/pulls" -Method GET $associatedPullRequests = @($response.Response) - $pullRequest = $associatedPullRequests | - Where-Object { $_.Base.Ref -eq $defaultBranch } | - Sort-Object -Property @{ Expression = { $_.'merge_commit_sha' -eq $commitSha }; Descending = $true }, - @{ Expression = { $_.'merged_at' }; Descending = $true } | - Select-Object -First 1 + $pullRequest = Select-PullRequestForPush -PullRequest $associatedPullRequests ` + -DefaultBranch $defaultBranch ` + -CommitSha $commitSha if ($pullRequest) { Write-Host "Resolved pull request #$($pullRequest.Number) from commit [$commitSha]." diff --git a/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 index 47d6690c..3c46f6f1 100644 --- a/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 +++ b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 @@ -73,3 +73,41 @@ Describe 'Resolve-WorkflowEventRouting' { $result.ShouldCleanupEvent | Should -BeTrue } } + +Describe 'Select-PullRequestForPush' { + It 'selects the merged PR whose merge commit matches the push SHA' { + $pullRequests = @( + [pscustomobject]@{ + Number = 411 + Base = [pscustomobject]@{ Ref = 'main' } + merged_at = $null + merge_commit_sha = 'open-pr-sha' + }, + [pscustomobject]@{ + Number = 390 + Base = [pscustomobject]@{ Ref = 'main' } + merged_at = '2026-07-24T00:00:00Z' + merge_commit_sha = 'pushed-sha' + } + ) + + $result = Select-PullRequestForPush -PullRequest $pullRequests -DefaultBranch main -CommitSha pushed-sha + + $result.Number | Should -Be 390 + } + + It 'does not use an open PR association as stable release context' { + $pullRequests = @( + [pscustomobject]@{ + Number = 411 + Base = [pscustomobject]@{ Ref = 'main' } + merged_at = $null + merge_commit_sha = 'pushed-sha' + } + ) + + $result = Select-PullRequestForPush -PullRequest $pullRequests -DefaultBranch main -CommitSha pushed-sha + + $result | Should -BeNullOrEmpty + } +} diff --git a/.github/actions/Publish-PSModule/action.yml b/.github/actions/Publish-PSModule/action.yml index 3160dcc0..4bad39ae 100644 --- a/.github/actions/Publish-PSModule/action.yml +++ b/.github/actions/Publish-PSModule/action.yml @@ -17,6 +17,10 @@ inputs: description: Normalized pull request context JSON from Get-PSModuleSettings. required: false default: '' + CommitSha: + description: Commit SHA that produced the module artifact. Stable release tags target this exact commit. + required: false + default: '' 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 @@ -66,6 +70,7 @@ runs: PSMODULE_PUBLISH_PSMODULE_INPUT_ModulePath: ${{ inputs.ModulePath }} PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey: ${{ inputs.APIKey }} PSMODULE_PUBLISH_PSMODULE_INPUT_PullRequest: ${{ inputs.PullRequest }} + PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha: ${{ inputs.CommitSha || github.sha }} PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf: ${{ inputs.WhatIf }} PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRBodyAsReleaseNotes: ${{ inputs.UsePRBodyAsReleaseNotes }} PSMODULE_PUBLISH_PSMODULE_INPUT_UsePRTitleAsReleaseName: ${{ inputs.UsePRTitleAsReleaseName }} diff --git a/.github/actions/Publish-PSModule/src/publish.ps1 b/.github/actions/Publish-PSModule/src/publish.ps1 index 6406a130..f660ea34 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -14,6 +14,10 @@ 'PSUseDeclaredVarsMoreThanAssignments', 'usePRTitleAsNotesHeading', Justification = 'Variable is used in script blocks.' )] +[Diagnostics.CodeAnalysis.SuppressMessageAttribute( + 'PSUseDeclaredVarsMoreThanAssignments', 'commitSha', + Justification = 'Variable is used in script blocks.' +)] [CmdletBinding()] param() @@ -47,6 +51,7 @@ LogGroup 'Load inputs' { $modulePath = Resolve-Path -Path $modulePathCandidate | Select-Object -ExpandProperty Path $apiKey = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_APIKey $whatIf = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_WhatIf -eq 'true' + $commitSha = $env:PSMODULE_PUBLISH_PSMODULE_INPUT_CommitSha $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' @@ -239,6 +244,8 @@ LogGroup 'Create GitHub release' { if ($createPrerelease) { $releaseCreateCommand += @('--target', $prHeadRef, '--prerelease') + } elseif (-not [string]::IsNullOrWhiteSpace($commitSha)) { + $releaseCreateCommand += @('--target', $commitSha) } if ($whatIf) { diff --git a/.github/workflows/Publish-Module.yml b/.github/workflows/Publish-Module.yml index c4a9856c..6caa39cd 100644 --- a/.github/workflows/Publish-Module.yml +++ b/.github/workflows/Publish-Module.yml @@ -47,6 +47,7 @@ jobs: ModulePath: outputs/module APIKey: ${{ secrets.APIKey }} PullRequest: ${{ toJson(fromJson(inputs.Settings).Context.PullRequest) }} + CommitSha: ${{ fromJson(inputs.Settings).Context.CommitSha }} WhatIf: ${{ github.repository == 'PSModule/Process-PSModule' }} UsePRTitleAsReleaseName: ${{ fromJson(inputs.Settings).Publish.Module.UsePRTitleAsReleaseName }} UsePRBodyAsReleaseNotes: ${{ fromJson(inputs.Settings).Publish.Module.UsePRBodyAsReleaseNotes }} From 0bda5684566bd1ab94e3a636675129fe02ff3648 Mon Sep 17 00:00:00 2001 From: Marius Storhaug Date: Fri, 24 Jul 2026 03:27:52 +0200 Subject: [PATCH 10/10] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20[Fix]:=20Require?= =?UTF-8?q?=20exact=20merged=20PR=20commit=20match?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../src/Get-PSModuleSettings.Helpers.psm1 | 6 +++--- .../tests/Get-PSModuleSettings.Helpers.Tests.ps1 | 15 +++++++++++++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 index c753bb62..46104dca 100644 --- a/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 +++ b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 @@ -99,9 +99,9 @@ function Select-PullRequestForPush { $PullRequest | Where-Object { $_.Base.Ref -eq $DefaultBranch -and - -not [string]::IsNullOrWhiteSpace($_.'merged_at') + -not [string]::IsNullOrWhiteSpace($_.'merged_at') -and + $_.'merge_commit_sha' -eq $CommitSha } | - Sort-Object -Property @{ Expression = { $_.'merge_commit_sha' -eq $CommitSha }; Descending = $true }, - @{ Expression = { $_.'merged_at' }; Descending = $true } | + Sort-Object -Property @{ Expression = { $_.'merged_at' }; Descending = $true } | Select-Object -First 1 } diff --git a/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 index 3c46f6f1..db074c40 100644 --- a/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 +++ b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 @@ -110,4 +110,19 @@ Describe 'Select-PullRequestForPush' { $result | Should -BeNullOrEmpty } + + It 'does not use a merged PR whose merge commit differs from the push SHA' { + $pullRequests = @( + [pscustomobject]@{ + Number = 390 + Base = [pscustomobject]@{ Ref = 'main' } + merged_at = '2026-07-24T00:00:00Z' + merge_commit_sha = 'different-sha' + } + ) + + $result = Select-PullRequestForPush -PullRequest $pullRequests -DefaultBranch main -CommitSha direct-push-sha + + $result | Should -BeNullOrEmpty + } }