Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/actions/Cleanup-PSModulePrereleases/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
18 changes: 12 additions & 6 deletions .github/actions/Cleanup-PSModulePrereleases/src/cleanup.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
155 changes: 107 additions & 48 deletions .github/actions/Get-PSModuleSettings/src/main.ps1
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading