Skip to content
Merged
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
113 changes: 87 additions & 26 deletions tools/releaseTools.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ $Script:powershell_team = @(
"dependabot-preview[bot]"
"dependabot[bot]"
"github-actions[bot]"
"Copilot"
"Anam Navied"
"Andrew Schwartzmeyer"
"Jason Helmick"
Expand All @@ -46,6 +47,27 @@ $Script:powershell_team = @(
"Justin Chung"
)

# The powershell team members GitHub logins. We use them to decide if the original author of a backport PR is from the team.
$script:psteam_logins = @(
'andyleejordan'
'TravisEz13'
'daxian-dbw'
'adityapatwardhan'
'SteveL-MSFT'
'dependabot[bot]'
'pwshBot'
'jshigetomi'
'SeeminglyScience'
'anamnavi'
'sdwheeler'
'Copilot'
'copilot-swe-agent'
'app/copilot-swe-agent'
'StevenBucher98'
'alerickson'
'tgauth'
)

# They are very active contributors, so we keep their email-login mappings here to save a few queries to Github.
$Script:community_login_map = @{
"darpa@yandex.ru" = "iSazonov"
Expand All @@ -54,11 +76,6 @@ $Script:community_login_map = @{
"info@powercode-consulting.se" = "powercode"
}

# Ignore dependency bumping bot (Dependabot):
$Script:attribution_ignore_list = @(
'dependabot[bot]@users.noreply.github.com'
)

##############################
#.SYNOPSIS
#In the release workflow, the release branch will be merged back to master after the release is done,
Expand Down Expand Up @@ -262,25 +279,76 @@ function Get-ChangeLog
$clExperimental = @()

foreach ($commit in $new_commits) {
$commitSubject = $commit.Subject
$prNumber = $commit.PullRequest
Write-Verbose "subject: $commitSubject"
Write-Verbose "authorname: $($commit.AuthorName)"
if ($commit.AuthorEmail.EndsWith("@microsoft.com") -or $powershell_team -contains $commit.AuthorName -or $Script:attribution_ignore_list -contains $commit.AuthorEmail) {
$commit.ChangeLogMessage = "- {0}" -f (Get-ChangeLogMessage $commit.Subject)

try {
$pr = Invoke-RestMethod `
-Uri "https://api.github.com/repos/PowerShell/PowerShell/pulls/$prNumber" `
-Headers $header `
-ErrorAction Stop `
-Verbose:$false ## Always disable verbose to avoid noise when we debug this function.
} catch {
## A commit may not have corresponding GitHub PRs. In that case, we will get status code 404 (Not Found).
## Otherwise, let the error bubble up.
if ($_.Exception.Response.StatusCode -ne 404) {
throw
}
}
Comment on lines 281 to +299
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$pr is not reset per loop iteration, and the REST call is attempted even when $prNumber is empty. If $prNumber is empty, the .../pulls/ endpoint can return a list of PRs; and if the call fails with 404, $pr can retain the previous iteration’s value. Initialize $pr = $null each iteration, only call the PR endpoint when $prNumber is present, and explicitly set $pr = $null when handling 404.

Copilot uses AI. Check for mistakes.

if ($commitSubject -match '^\[release/v\d\.\d\] ') {
## The commit was from a backport PR. We need to get the real author in this case.
if (-not $pr) {
throw "The commit is from a backport PR (#$prNumber), but the PR cannot be found.`nPR Title: $commitSubject"
}

$userPattern = 'Triggered by @.+ on behalf of @(.+)'
if ($pr.body -match $userPattern) {
$commit.AuthorGitHubLogin = ($Matches.1).Trim()
Write-Verbose "backport PR. real author login: $($commit.AuthorGitHubLogin)"
} else {
throw "The commit is from a backport PR (#$prNumber), but the PR description failed to match the pattern '$userPattern'. Was the template for backport PRs changed?`nPR Title: $commitSubject"
}
}

if ($commit.AuthorGitHubLogin) {
if ($script:psteam_logins -contains $commit.AuthorGitHubLogin) {
$commit.ChangeLogMessage = "- {0}" -f (Get-ChangeLogMessage $commitSubject)
} else {
$commit.ChangeLogMessage = ("- {0} (Thanks @{1}!)" -f (Get-ChangeLogMessage $commitSubject), $commit.AuthorGitHubLogin)
$commit.ThankYouMessage = ("@{0}" -f ($commit.AuthorGitHubLogin))
}
} elseif ($commit.AuthorEmail.EndsWith("@microsoft.com") -or $powershell_team -contains $commit.AuthorName) {
$commit.ChangeLogMessage = "- {0}" -f (Get-ChangeLogMessage $commitSubject)
} else {
if ($community_login_map.ContainsKey($commit.AuthorEmail)) {
$commit.AuthorGitHubLogin = $community_login_map[$commit.AuthorEmail]
} else {
$uri = "https://api.github.com/repos/PowerShell/PowerShell/commits/$($commit.Hash)"
try{
$response = Invoke-WebRequest -Uri $uri -Method Get -Headers $header -ErrorAction Ignore
} catch{}
## Always disable verbose to avoid noise when we debug this function.
$response = Invoke-RestMethod `
-Uri "https://api.github.com/repos/PowerShell/PowerShell/commits/$($commit.Hash)" `
-Headers $header `
-ErrorAction Stop `
-Verbose:$false
} catch {
## A commit could be available in ADO only. In that case, we will get status code 422 (UnprocessableEntity).
## Otherwise, let the error bubble up.
if ($_.Exception.Response.StatusCode -ne 422) {
throw
}
}
Comment on lines 329 to +342
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$response is not cleared before the commit lookup, so if the GitHub commit API call fails (e.g., 422) $response may still contain the prior commit’s data and incorrectly set AuthorGitHubLogin. Set $response = $null before the try/catch (and/or in the 422 catch path) so stale responses can’t be reused.

Copilot uses AI. Check for mistakes.

if($response)
{
$content = ConvertFrom-Json -InputObject $response.Content
$commit.AuthorGitHubLogin = $content.author.login
$commit.AuthorGitHubLogin = $response.author.login
$community_login_map[$commit.AuthorEmail] = $commit.AuthorGitHubLogin
}
}
$commit.ChangeLogMessage = ("- {0} (Thanks @{1}!)" -f (Get-ChangeLogMessage $commit.Subject), $commit.AuthorGitHubLogin)

$commit.ChangeLogMessage = ("- {0} (Thanks @{1}!)" -f (Get-ChangeLogMessage $commitSubject), $commit.AuthorGitHubLogin)
$commit.ThankYouMessage = ("@{0}" -f ($commit.AuthorGitHubLogin))
}

Expand All @@ -289,16 +357,6 @@ function Get-ChangeLog
}

## Get the labels for the PR
try {
$pr = Invoke-RestMethod -Uri "https://api.github.com/repos/PowerShell/PowerShell/pulls/$($commit.PullRequest)" -Headers $header -ErrorAction SilentlyContinue
}
catch {
if ($_.Exception.Response.StatusCode -eq '404') {
$pr = $null
#continue
}
}

if($pr)
{
$clLabel = $pr.labels | Where-Object { $_.Name -match "^CL-"}
Expand Down Expand Up @@ -328,7 +386,7 @@ function Get-ChangeLog
"CL-Tools" { $clTools += $commit }
"CL-Untagged" { $clUntagged += $commit }
"CL-NotInBuild" { continue }
Default { throw "unknown tag '$cLabel' for PR: '$($commit.PullRequest)'" }
Default { throw "unknown tag '$cLabel' for PR: '$prNumber'" }
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default branch of this switch throws unknown tag '$cLabel' ..., but $cLabel is not defined anywhere in this scope. Use the actual label value being evaluated (e.g., $clLabel.Name) so the error message is accurate and doesn’t itself error/print an empty label.

Suggested change
Default { throw "unknown tag '$cLabel' for PR: '$prNumber'" }
Default { throw "unknown tag '$($clLabel.Name)' for PR: '$prNumber'" }

Copilot uses AI. Check for mistakes.
}
}
}
Expand Down Expand Up @@ -426,6 +484,9 @@ function Get-ChangeLogMessage
'^Build\(deps\): ' {
return $OriginalMessage.replace($Matches.0,'')
}
'^\[release/v\d\.\d\] ' {
return $OriginalMessage.replace($Matches.0,'')
}
default {
return $OriginalMessage
}
Expand Down Expand Up @@ -867,8 +928,8 @@ function Invoke-PRBackport {
)
$continue = $false
while(!$continue) {
$input= Read-Host -Prompt ($Message + "`nType 'Yes<enter>' to continue 'No<enter>' to exit")
switch($input) {
$value = Read-Host -Prompt ($Message + "`nType 'Yes<enter>' to continue 'No<enter>' to exit")
switch($value) {
'yes' {
$continue= $true
}
Expand Down