windows-terminal/tools/ReleaseEngineering/ServicingPipeline.ps1

263 lines
8.8 KiB
PowerShell

# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
#Requires -Version 7
#Requires -Modules PSGitHub
[CmdletBinding()]
Param(
[string]$Version,
[string]$SourceBranch = "origin/main",
[switch]$GpgSign = $False
)
Function Prompt() {
"PSCR {0}> " -f $PWD.Path
}
Function Enter-ConflictResolutionShell() {
$Global:Abort = $False
$Global:Skip = $False
$Global:Reject = $False
Push-Location -StackName:"ServicingStack" -Path:$PWD > $Null
Write-Host @"
`e[31;1;7mCONFLICT RESOLUTION REQUIRED`e[m
"@
& git status --short
Write-Host (@"
`e[1mCommands`e[m
`e[1;96mdone `e[m Complete conflict resolution and commit
`e[1;96mskip `e[m Skip this commit
`e[1;96mabort `e[m Stop everything
`e[1;96mreject`e[m Skip and `e[31mremove this commit from servicing consideration`e[m
"@)
$Host.EnterNestedPrompt()
Pop-Location -StackName:"ServicingStack" > $Null
}
Function Done() {
$Host.ExitNestedPrompt()
}
Function Abort() {
$Global:Abort = $True
$Host.ExitNestedPrompt()
}
Function Skip() {
$Global:Skip = $True
$Host.ExitNestedPrompt()
}
Function Reject() {
$Global:Skip = $True
$Global:Reject = $True
$Host.ExitNestedPrompt()
}
Function Set-GraphQlProjectEntryStatus($Project, $Item, $Field, $Value) {
Invoke-GitHubGraphQlApi -Query 'mutation($project: ID!, $item: ID!, $field: ID!, $value: String!) {
updateProjectV2ItemFieldValue(
input: {
projectId: $project
itemId: $item
fieldId: $field
value: {
singleSelectOptionId: $value
}
}
) { projectV2Item { id } }
}' -Variables @{ project = $Project; item = $Item; field = $Field; value = $Value } | Out-Null
}
Function Get-GraphQlProjectNumberGivenName($Organization, $Name) {
$projectNumber = ""
$cursor = ""
While ([String]::IsNullOrEmpty($projectNumber)) {
$o = Invoke-GitHubGraphQlApi -Query '
query($organization: String!, $after: String) {
organization (login: $organization) {
projectsV2 (first: 20, after: $after) {
nodes { id title number }
pageInfo { hasNextPage endCursor }
}
}
}' -Variables @{ organization = $Organization; after = $cursor }
If($false -eq $o.organization.projectsV2.pageInfo.hasNextPage) {
Break
}
$pl = $o.organization.projectsV2.nodes | Where-Object title -Like $Name
If($null -ne $pl) {
$projectNumber = $pl.number
Break
}
$cursor = $o.organization.projectsV2.pageInfo.endCursor
}
$projectNumber
}
Function Get-GraphQlProjectWithNodes($Organization, $Number) {
# It's terrible, but it pulls *all* of the info we need all at once!
$Project = Invoke-GitHubGraphQlApi -Query '
query($organization: String! $number: Int!) {
organization(login: $organization) {
projectV2(number: $number) {
id
number
title
fields(first:20){
nodes {
... on ProjectV2FieldCommon { id name }
... on ProjectV2SingleSelectField { options { id name } }
}
}
items {
nodes {
id
status: fieldValueByName(name: "Status") {
... on ProjectV2ItemFieldSingleSelectValue { name }
}
content {
... on Issue {
number
closedByPullRequestsReferences(first: 8) {
... on PullRequestConnection {
nodes {
number
mergeCommit { oid }
}
}
}
}
... on PullRequest {
number
mergeCommit { oid }
}
}
}
}
}
}
}
' -Variables @{ organization = $Organization; number = $Number }
$Project
}
If ([String]::IsNullOrEmpty($Version)) {
$BranchVersionRegex = [Regex]"^release-(\d+(\.\d+)+)$"
$Branch = & git rev-parse --abbrev-ref HEAD
$Version = $BranchVersionRegex.Match($Branch).Groups[1].Value
If ([String]::IsNullOrEmpty($Version)) {
Write-Error "No version specified, and we can't infer it from the name of your branch ($Branch)."
Exit 1
}
Write-Host "Inferred servicing version $Version"
}
$Script:TodoStatusName = "To Cherry Pick"
$Script:DoneStatusName = "Cherry Picked"
$Script:RejectStatusName = "Rejected"
# Propagate default values into all PSGitHub cmdlets
$GithubOrg = "microsoft"
$PSDefaultParameterValues['*GitHub*:Owner'] = $GithubOrg
$PSDefaultParameterValues['*GitHub*:RepositoryName'] = "terminal"
& git fetch --all 2>&1 | Out-Null
$Branch = & git rev-parse --abbrev-ref HEAD
$RemoteForCurrentBranch = & git config "branch.$Branch.remote"
$CommitsBehind = [int](& git rev-list --count "$RemoteForCurrentBranch/$Branch" "^$Branch")
If ($LASTEXITCODE -Ne 0) {
Write-Error "Failed to determine branch divergence"
Exit 1
}
If ($CommitsBehind -Gt 0) {
Write-Error "Local branch $Branch is out of date with $RemoteForCurrentBranch; consider git pull"
Exit 1
}
$projectNumber = Get-GraphQlProjectNumberGivenName -Organization $GithubOrg -Name "$Version Servicing Pipeline"
If([String]::IsNullOrEmpty($projectNumber)) {
Throw "Failed to find ProjectV2 for `"$Version Servicing Pipeline`""
}
$Project = Get-GraphQlProjectWithNodes -Organization $GithubOrg -Number $projectNumber
$StatusField = $Project.organization.projectV2.fields.nodes | Where-Object { $_.name -eq "Status" }
$StatusFieldId = $StatusField.id
$StatusRejectOptionId = $StatusField.options | Where-Object name -eq $script:RejectStatusName | Select-Object -Expand id
$StatusDoneOptionId = $StatusField.options | Where-Object name -eq $script:DoneStatusName | Select-Object -Expand id
$ToPickList = $Project.organization.projectV2.items.nodes | Where-Object { $_.status.name -eq $TodoStatusName }
$commits = New-Object System.Collections.ArrayList
$cards = [System.Collections.Generic.Dictionary[String, String[]]]::new()
$ToPickList | ForEach-Object {
If (-Not [String]::IsNullOrEmpty($_.content.mergeCommit.oid)) {
$co = $_.content.mergeCommit.oid
} ElseIf (-Not [String]::IsNullOrEmpty($_.content.closedByPullRequestsReferences.nodes.mergeCommit.oid)) {
$co = $_.content.closedByPullRequestsReferences.nodes.mergeCommit.oid
} Else {
Return
}
$null = $commits.Add($co)
$cards[$co] += $_.id
}
$sortedAllCommits = & git rev-list --no-walk=sorted $commits
[Array]::Reverse($sortedAllCommits)
$PickArgs = @()
If ($GpgSign) {
$PickArgs += , "-S"
}
$sortedAllCommits | ForEach-Object {
Write-Host "`e[96m`e[1;7mPICK`e[22;27m`e[m $(& git show -q --pretty=oneline $_)"
$null = & git cherry-pick -x $_ 2>&1
$Err = ""
While ($True) {
If ($LASTEXITCODE -ne 0) {
$Err
Enter-ConflictResolutionShell
If ($Global:Abort) {
& git cherry-pick --abort
Write-Host -ForegroundColor "Red" "YOU'RE ON YOUR OWN"
Exit
}
If ($Global:Reject) {
ForEach($card In $cards[$_]) {
Set-GraphQlProjectEntryStatus -Project $Project.organization.projectV2.id -Item $card -Field $StatusFieldId -Value $StatusRejectOptionId
}
# Fall through to Skip
}
If ($Global:Skip) {
& git cherry-pick --skip
Break
}
$Err = & git cherry-pick --continue --no-edit
} Else {
& git commit @PickArgs --amend --no-edit --trailer "Service-Card-Id:$($cards[$_])" --trailer "Service-Version:$Version" | Out-Null
Write-Host "`e[92;1;7m OK `e[m"
ForEach($card In $cards[$_]) {
Set-GraphQlProjectEntryStatus -Project $Project.organization.projectV2.id -Item $card -Field $StatusFieldId -Value $StatusDoneOptionId
}
Break
}
}
}