diff --git a/tools/ReleaseEngineering/ServicingPipeline.ps1 b/tools/ReleaseEngineering/ServicingPipeline.ps1 index b9d2ba4c86..92b5380363 100644 --- a/tools/ReleaseEngineering/ServicingPipeline.ps1 +++ b/tools/ReleaseEngineering/ServicingPipeline.ps1 @@ -15,17 +15,15 @@ Function Prompt() { "PSCR {0}> " -f $PWD.Path } -Function Enter-ConflictResolutionShell($entry) { +Function Enter-ConflictResolutionShell() { $Global:Abort = $False $Global:Skip = $False $Global:Reject = $False Push-Location -StackName:"ServicingStack" -Path:$PWD > $Null - Write-Host (@" + Write-Host @" `e[31;1;7mCONFLICT RESOLUTION REQUIRED`e[m -`e[1mCommit `e[m: `e[1;93m{0}`e[m -`e[1mSubject`e[m: `e[1;93m{1}`e[m -"@ -f ($_.CommitID, $_.Subject)) +"@ & git status --short @@ -61,6 +59,97 @@ Function Reject() { $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 @@ -72,21 +161,15 @@ If ([String]::IsNullOrEmpty($Version)) { Write-Host "Inferred servicing version $Version" } -$Script:TodoColumnName = "To Cherry Pick" -$Script:DoneColumnName = "Cherry Picked" -$Script:RejectColumnName = "Rejected" +$Script:TodoStatusName = "To Cherry Pick" +$Script:DoneStatusName = "Cherry Picked" +$Script:RejectStatusName = "Rejected" # Propagate default values into all PSGitHub cmdlets -$PSDefaultParameterValues['*GitHub*:Owner'] = "microsoft" +$GithubOrg = "microsoft" +$PSDefaultParameterValues['*GitHub*:Owner'] = $GithubOrg $PSDefaultParameterValues['*GitHub*:RepositoryName'] = "terminal" -$Project = Get-GithubProject -Name "$Version Servicing Pipeline" -$AllColumns = Get-GithubProjectColumn -ProjectId $Project.id -$ToPickColumn = $AllColumns | ? Name -Eq $script:TodoColumnName -$DoneColumn = $AllColumns | ? Name -Eq $script:DoneColumnName -$RejectColumn = $AllColumns | ? Name -Eq $script:RejectColumnName -$Cards = Get-GithubProjectCard -ColumnId $ToPickColumn.id - & git fetch --all 2>&1 | Out-Null $Branch = & git rev-parse --abbrev-ref HEAD @@ -103,27 +186,50 @@ If ($CommitsBehind -Gt 0) { Exit 1 } -$Entries = @(& git log $SourceBranch --first-parent --grep "(#\($($Cards.Number -Join "\|")\))" "--pretty=format:%H%x1C%s" | - ConvertFrom-CSV -Delimiter "`u{001C}" -Header CommitID,Subject) +$projectNumber = Get-GraphQlProjectNumberGivenName -Organization $GithubOrg -Name "$Version Servicing Pipeline" +If([String]::IsNullOrEmpty($projectNumber)) { + Throw "Failed to find ProjectV2 for `"$Version Servicing Pipeline`"" +} -[Array]::Reverse($Entries) +$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" } -$CommitPRRegex = [Regex]"\(#(\d+)\)$" -$Entries | ForEach-Object { - Write-Host "`e[96m`e[1;7mPICK`e[22;27m $($_.CommitID): $($_.Subject)`e[m" - $PR = $CommitPRRegex.Match($_.Subject).Groups[1].Value - $Card = $Cards | ? Number -Eq $PR - $null = & git cherry-pick -x $_.CommitID 2>&1 +$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 $_ + Enter-ConflictResolutionShell If ($Global:Abort) { & git cherry-pick --abort @@ -132,7 +238,9 @@ $Entries | ForEach-Object { } If ($Global:Reject) { - Move-GithubProjectCard -CardId $Card.id -ColumnId $RejectColumn.id + ForEach($card In $cards[$_]) { + Set-GraphQlProjectEntryStatus -Project $Project.organization.projectV2.id -Item $card -Field $StatusFieldId -Value $StatusRejectOptionId + } # Fall through to Skip } @@ -143,9 +251,11 @@ $Entries | ForEach-Object { $Err = & git cherry-pick --continue --no-edit } Else { - & git commit @PickArgs --amend --no-edit --trailer "Service-Card-Id:$($Card.Id)" --trailer "Service-Version:$Version" | Out-Null + & 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" - Move-GithubProjectCard -CardId $Card.id -ColumnId $DoneColumn.id + ForEach($card In $cards[$_]) { + Set-GraphQlProjectEntryStatus -Project $Project.organization.projectV2.id -Item $card -Field $StatusFieldId -Value $StatusDoneOptionId + } Break } }