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/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 new file mode 100644 index 00000000..46104dca --- /dev/null +++ b/.github/actions/Get-PSModuleSettings/src/Get-PSModuleSettings.Helpers.psm1 @@ -0,0 +1,107 @@ +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) + ) + } +} + +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') -and + $_.'merge_commit_sha' -eq $CommitSha + } | + Sort-Object -Property @{ Expression = { $_.'merged_at' }; Descending = $true } | + Select-Object -First 1 +} 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..d9bc1b81 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 @@ -190,6 +191,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 +228,97 @@ 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 = Select-PullRequestForPush -PullRequest $associatedPullRequests ` + -DefaultBranch $defaultBranch ` + -CommitSha $commitSha + + 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') - $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,36 +388,44 @@ If you believe this is incorrect, please verify that your changes are in the cor } } } + } elseif ($isPush) { + # 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; build/test will run but 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 - # 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' - # 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) { - '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 - isAbandonedPR = $isAbandonedPR - isMergedPR = $isMergedPR - isNotAbandonedPR = $isNotAbandonedPR - 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 ReleaseType = $releaseType @@ -532,23 +596,18 @@ $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 = ( - ($releaseType -eq 'Release') -or - ($isMergedPR -and $isTargetDefaultBranch) -or - $isAbandonedPR - ) - $shouldAutoCleanup = $isReleaseOrMergedOrAbandoned -and ($settings.Publish.Module.AutoCleanup -eq $true) + # Closed PRs only clean up prereleases. Push runs also clean up the associated PR channel. + $shouldCleanupEvent = $routing.ShouldCleanupEvent + $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 = $routing.ShouldRunBuildTest # Check if setup/teardown scripts exist in the repository $hasBeforeAllScript = Test-Path -Path 'tests/BeforeAll.ps1' @@ -607,8 +666,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 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..db074c40 --- /dev/null +++ b/.github/actions/Get-PSModuleSettings/tests/Get-PSModuleSettings.Helpers.Tests.ps1 @@ -0,0 +1,128 @@ +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 + } +} + +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 + } + + 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 + } +} diff --git a/.github/actions/Publish-PSModule/action.yml b/.github/actions/Publish-PSModule/action.yml index 0ff0a534..4bad39ae 100644 --- a/.github/actions/Publish-PSModule/action.yml +++ b/.github/actions/Publish-PSModule/action.yml @@ -13,6 +13,14 @@ inputs: APIKey: description: PowerShell Gallery API Key. required: true + PullRequest: + 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 @@ -61,6 +69,8 @@ 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_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 b9f76254..f660ea34 100644 --- a/.github/actions/Publish-PSModule/src/publish.ps1 +++ b/.github/actions/Publish-PSModule/src/publish.ps1 @@ -15,11 +15,7 @@ Justification = 'Variable is used in script blocks.' )] [Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'prNumber', - Justification = 'Variable is used in script blocks.' -)] -[Diagnostics.CodeAnalysis.SuppressMessageAttribute( - 'PSUseDeclaredVarsMoreThanAssignments', 'prHeadRef', + 'PSUseDeclaredVarsMoreThanAssignments', 'commitSha', Justification = 'Variable is used in script blocks.' )] [CmdletBinding()] @@ -55,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' @@ -65,18 +62,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 +145,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 +190,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 +215,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 +227,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 { @@ -221,6 +244,8 @@ LogGroup 'Create GitHub release' { if ($createPrerelease) { $releaseCreateCommand += @('--target', $prHeadRef, '--prerelease') + } elseif (-not [string]::IsNullOrWhiteSpace($commitSha)) { + $releaseCreateCommand += @('--target', $commitSha) } if ($whatIf) { @@ -267,9 +292,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/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 b/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 index d4f2f450..2ff3a452 100644 --- a/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 +++ b/.github/actions/Resolve-PSModuleVersion/src/Resolve-PSModuleVersion.Helpers.psm1 @@ -123,26 +123,65 @@ 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 #> + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', + Justification = 'Parameter is used inside LogGroup script block.')] [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 +191,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 +207,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 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 + } +} 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/Publish-Module.yml b/.github/workflows/Publish-Module.yml index ee1bbcb6..6caa39cd 100644 --- a/.github/workflows/Publish-Module.yml +++ b/.github/workflows/Publish-Module.yml @@ -46,6 +46,8 @@ jobs: Name: ${{ fromJson(inputs.Settings).Name }} 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 }} @@ -60,4 +62,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 }} diff --git a/.github/workflows/Release.yml b/.github/workflows/Release.yml index 1e346454..7b28f097 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 @@ -29,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-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/**' 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