From 107e2c3df14d8b013c0fe75eb936e9664ead8e40 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 1 Oct 2025 23:08:31 -0700 Subject: [PATCH 1/7] Add assign reviewers functionality --- .github/workflows/Shared-AutoLabelAssign.yml | 327 ++++++++++++++++++- 1 file changed, 318 insertions(+), 9 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index a3d41429252..9e7fb6b8859 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -19,12 +19,19 @@ on: AutoAssignUsers: required: true type: string + AutoAssignReviewers: + required: true + type: string AutoLabel: required: true type: string secrets: AccessToken: required: true + PrivateKey: + required: true + ClientId: + required: true jobs: build: @@ -32,12 +39,23 @@ jobs: if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: + + - name: Create App Token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.ClientId }} + private-key: ${{ secrets.PrivateKey }} + owner: ${{ github.repository_owner }} + - name: Script shell: pwsh env: PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} + AppGitHubAccessToken: ${{ steps.app-token.outputs.token }} AutoAssignUsers: ${{ inputs.AutoAssignUsers }} + AutoAssignReviewers: ${{ inputs.AutoAssignReviewers }} AutoLabel: ${{ inputs.AutoLabel }} ExcludedUserList: ${{ inputs.ExcludedUserList }} ExcludedBranchList: ${{ inputs.ExcludedBranchList }} @@ -46,6 +64,7 @@ jobs: $RepoRoot = $env:RUNNER_WORKSPACE $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $AccessToken = $env:AccessToken + $GitHubApiUrl = "https://api.github.com/repos/MicrosoftDocs/" $GitRequestEvent = $GitHubData.event_name $DefaultBranch = $GitHubData.event.repository.default_branch @@ -57,17 +76,25 @@ jobs: $RepoLabelUrl = $GitHubData.event.repository.labels_url $PrFileListUrl = "$($GitHubData.event.pull_request.url)/files" $IssueUrl = $GitHubData.event.pull_request.issue_url + $PrUrl = $GitHubData.event.pull_request.url + $PrCreator = $GitHubData.event.pull_request.user.login $AutoAssignUsers = [bool][int]$env:AutoAssignUsers # If it works, it works! - $AutoLabel = [bool][int]$env:AutoLabel # Ditto + $AutoAssignReviewers = [bool][int]$env:AutoAssignReviewers # Ditto + $AutoLabel = [bool][int]$env:AutoLabel # Ditto ditto $ExcludedUserList = $env:ExcludedUserList | ConvertFrom-Json $ExcludedBranchList = $env:ExcludedBranchList | ConvertFrom-Json - + $AppGitHubAccessToken = $env:AppGitHubAccessToken # Set GitHub REST API headers $GitHubHeaders = @{} $GitHubHeaders.Add("Authorization","token $AccessToken") - $GitHubHeaders.Add("User-Agent", "dstrome") + $GitHubHeaders.Add("User-Agent", "OfficeDocs") + + # Create github HTTP authentication header using GitHub app installation token + $AppGitHubAccessHeaders = @{} + $AppGitHubAccessHeaders.Add("Authorization","token $($AppGitHubAccessToken)") + $AppGitHubAccessHeaders.Add("User-Agent", "OfficeDocs") # Regex for string matches $AuthorRegex = "(?m)^(author:\s{0,3})([\w|\-]{1,39})" @@ -80,15 +107,16 @@ jobs: $LabelColor = "BFDADC" $LabelDescription = "" + $ServiceToGitHubUserMapRepo = "officedocs-pr" + $ServiceToGitHubUserMapFilePath = "/contents/.github/workflows/resources/service-subservice-to-github-user-map.csv" + $ServiceToGitHubUserMapRef = $DefaultBranch + $ServiceToGitHubUserMapUrl = $GitHubApiUrl + $ServiceToGitHubUserMapRepo + $ServiceToGitHubUserMapFilePath + "?ref=$ServiceToGitHubUserMapRef" + # Set repo variables $RepoUrl = $GitHubData.event.repository.url Write-Host "Repository URL: $RepoUrl" - #$data = Invoke-RestMethod -Uri $RepoUrl -Headers $GitHubHeaders - - - ##################### ##################### # Get-FileMetadata @@ -172,7 +200,7 @@ jobs: $Tier = $Null } - # Add results of above tests to an utput object. Each property contains the results of the associated tests from above. + # Add results of above tests to an output object. Each property contains the results of the associated tests from above. $FileData = @{ Service = $Service SubService = $SubService @@ -476,6 +504,260 @@ jobs: } + ##################### + ##################### + # Set-PrReviewer + + Function Set-PrReviewer { + + param( + + $PrUrl, + $GitHubReviewers + + ) + + # Ensure ExcludedUserList is always an array (handles $null and single string) + $ExcludedUserList = @($ExcludedUserList) + + $UsersToReview = @() + + ForEach ($User in $GitHubReviewers) { + If ($User -eq $PrCreator) { + Write-Host "Excluding $User — PR creator." + Continue + } + + If ($ExcludedUserList -and ($ExcludedUserList -contains $User)) { + Write-Host "Excluding $User — in exclusion list." + Continue + } + + If (-not ($UsersToReview -contains $User)) { + Write-Host "Adding $User to reviewer list." + $UsersToReview += $User + } + } + + # Construct JSON statement that will be sent to GitHub as the body of the web request. GitHub expects an array regardless of whether it's a single value or multiple. + # Convert array to JSON + $Body = @{} + $Body.Add("reviewers", @($UsersToReview)) + $Body = $Body | ConvertTo-Json + + $ReviewerUrl = "$PrUrl/requested_reviewers" + + # Try to submit the request to GitHub API to apply they label to the issue or pull request + Try { + + Write-Host "Setting accounts $UsersToReview on URL $ReviewerUrl." + + $Result = Invoke-RestMethod -Uri $ReviewerUrl -Body $Body -Headers $GitHubHeaders -Method POST + + } Catch { + + Write-Host "ERROR: Failed assign GitHub accounts on URL $ReviewerUrl. Error: $_." + + + } + + } + + ##################### + ##################### + # Get-ServiceGitHubAccountMappingTable + + Function Get-ServiceGitHubAccountMappingTable { + + Try { + + Write-Host "Getting service/subservice to GitHub user mapping from $ServiceToGitHubUserMapUrl" + + $GitHubMappingTableData = Invoke-RestMethod -Uri $ServiceToGitHubUserMapUrl -Headers $AppGitHubAccessHeaders + $GitHubMappingTable = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($GitHubMappingTableData.content)); + + } Catch { + + Write-Host "Failed to get service/subservice to GitHub user mapping from $ServiceToGitHubUserMapUrl" + + $GitHubMappingTable = $Null + + } + + Return $GitHubMappingTable + + + } + + ##################### + ##################### + # Expand-ServiceSubServiceRows + + Function Expand-ServiceSubServiceRows { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [String]$CsvContent + ) + + # Parse the CSV content + $CsvData = $CsvContent | ConvertFrom-Csv + + # Initialize array to hold expanded rows + $ExpandedRows = @() + + ForEach ($Row in $CsvData) { + # Get the services and subservices, handling null/empty values + $Services = If ([String]::IsNullOrWhiteSpace($Row.Service)) { @('') } + Else { $Row.Service -split ';' | ForEach-Object { $_.Trim() } } + + $SubServices = If ([String]::IsNullOrWhiteSpace($Row.SubService)) { @('') } + Else { $Row.SubService -split ';' | ForEach-Object { $_.Trim() } } + + # Create a new row for each combination of service and subservice + ForEach ($Service in $Services) { + ForEach ($SubService in $SubServices) { + $NewRow = [PSCustomObject]@{ + Product = $Row.Product + Service = $Service + SubService = $SubService + ContentLeadGitHubAccounts = $Row.ContentLeadGitHubAccounts + } + $ExpandedRows += $NewRow + } + } + } + + Return $ExpandedRows + } + + ##################### + ##################### + # Get-ContentLeadAccounts + + Function Get-ContentLeadAccounts { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string]$Service, + + [Parameter(Mandatory = $false)] + [string]$SubService, + + [Parameter(Mandatory = $true)] + [array]$DataArray + ) + + + $AccountsList = @() + + Write-Host "Article service: $Service" + Write-Host "Article subservice: $SubService" + + # Filter data based on Service match + $ServiceMatches = $DataArray | Where-Object { $_.service -eq $Service } + + Write-Host "Service matches:" + $ServiceMatches | ForEach-Object { Write-Host $_ } + + If ($ServiceMatches) { + + Write-Host "Service found" + + # Check for exact match scenarios + If ($SubService) { + + Write-Host "Subservice provided" + + # SubService provided - look for exact match + $ExactMatch = $ServiceMatches | Where-Object { $_.subservice -eq $SubService } + + If ($ExactMatch) { + + Write-Host "SubService found" + + # Found exact match with SubService + $AccountsList += $ExactMatch.contentleadgithubaccounts + } + Else { + + Write-Host "Subservice not found" + + # No exact SubService match found + # Check if there are any subservices defined for this service + $HasSubServices = $ServiceMatches | Where-Object { + ![string]::IsNullOrWhiteSpace($_.subservice) + } + + If ($HasSubServices) { + + Write-Host "Service has subservices. Returning all accounts for all subservices." + + # Multiple subservices exist, return all accounts for this service's subservices + $AccountsList += $HasSubServices.contentleadgithubaccounts + } + } + } + Else { + # No SubService provided - look for exact match with empty/null SubService + $ExactMatch = $ServiceMatches | Where-Object { + [string]::IsNullOrWhiteSpace($_.subservice) + } + + If ($ExactMatch) { + + Write-Host "Service with no subservice row found" + + # Found exact match without SubService + $AccountsList += $ExactMatch.contentleadgithubaccounts + } + Else { + + Write-Host "Service with no subservice row not found" + + # No exact match without SubService + # Check if there are multiple subservices for this service + $HasSubServices = $ServiceMatches | Where-Object { + ![string]::IsNullOrWhiteSpace($_.subservice) + } + + If ($HasSubServices.Count -gt 1) { + + Write-Host "Service has subservices. Returning all accounts for all subservices." + + # Multiple subservices exist, return all accounts + $AccountsList += $HasSubServices.contentleadgithubaccounts + } + } + } + } + + Write-Host "Found accounts" + $AccountsList | ForEach-Object { Write-Host $_ } + + # Process the accounts list + $AllAccounts = @() + + ForEach ($AccountString In $AccountsList) { + If (![string]::IsNullOrWhiteSpace($AccountString)) { + # Remove leading semicolon if present + $CleanedString = $AccountString.TrimStart(';') + + # Split by semicolon and add to array + $Accounts = $CleanedString -split ';' | Where-Object { + ![string]::IsNullOrWhiteSpace($_) + } + + $AllAccounts += $Accounts + } + } + + # Return deduplicated array + Return $AllAccounts | Select-Object -Unique + + } + + ##################### ##################### # Main @@ -572,6 +854,31 @@ jobs: # Auto user assignment can be disabled on repos using the $DataTableName table. If ($AutoAssignUsers -eq $True) { + # Get the service/subservice to GitHub account map + $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable + $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap + + $ReviewerArray = @() + + ForEach ($File in $FileMetadataArray) { + + $FileReviewers = $Null + + $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + + If ($FileReviewers -ne $Null) { + + $ReviewerArray += $FileReviewers + + } + + } + + $ReviewerArray = $ReviewerArray | Select-Object -Unique + + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ReviewerArray + + # Don't add assignments to PRs in excluded branches listed in $ExcludedBranchList If (!$ExcludedBranchList.Contains($TargetBranch)) { @@ -585,6 +892,8 @@ jobs: } + + } Else { Write-Host "Target branch $TargetBranch is an excluded branch. Not adding author assignments." @@ -609,4 +918,4 @@ jobs: } # Number of files in PR check - } # PR event and action check + } # PR event and action check \ No newline at end of file From 86c10834a33c43a76471ebb7edb1f92a881abf17 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 2 Oct 2025 12:02:58 -0700 Subject: [PATCH 2/7] Add AutoLabelAssign-ContentLeadReviewNotice.md --- ...AutoLabelAssign-ContentLeadReviewNotice.md | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md diff --git a/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md b/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md new file mode 100644 index 00000000000..4b9a8b1fe57 --- /dev/null +++ b/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md @@ -0,0 +1,23 @@ + +You've been added as a reviewer to this pull request because you're a Content Lead for the product area or feature covered by one or more of the articles in this PR. + +Before this PR can be merged, a Content Lead needs to add the **Sign off** label to it. Before you sign off a PR, confirm the following: + +- Article content has been checked for technical accuracy. +- There are no errors or warnings in build validation. +- There are no severity 1 or 2 PoliCheck issues. +- All articles have a minimum Acrolinx score of 80. +- All **required** PR checks at the bottom of the PR are passing. + +After you've confirmed the above, do the following to sign off: + +1. Select the gear next to **Labels**. +1. Select **Sign off** from the label list. +1. Click away from the label list. + +After you've signed off, our operations (PubOps) team will review the PR for issues that may impact formatting and the customer experience. If any changes are needed, they will return +the PR to the submitter with required changes. After they've completed those changes, you need to sign off again. + +When the PR passes PubOps review, they'll merge the PR. If the PR is merged into the **main** branch, changes will go live within the next couple hours. + +If you have questions about this process or need help, post a message to https://aka.ms/tcphelp. From 7e19a686e4964a01e3a2e681361c515775111335 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:02:24 -0700 Subject: [PATCH 3/7] Add PR message post and org validation --- .github/workflows/Shared-AutoLabelAssign.yml | 180 ++++++++++++++++++- 1 file changed, 177 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 9e7fb6b8859..24610d89a2b 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -77,6 +77,8 @@ jobs: $PrFileListUrl = "$($GitHubData.event.pull_request.url)/files" $IssueUrl = $GitHubData.event.pull_request.issue_url $PrUrl = $GitHubData.event.pull_request.url + $CommentsUrl = $GitHubData.event.pull_request.comments_url + $PrCreator = $GitHubData.event.pull_request.user.login $AutoAssignUsers = [bool][int]$env:AutoAssignUsers # If it works, it works! @@ -109,9 +111,15 @@ jobs: $ServiceToGitHubUserMapRepo = "officedocs-pr" $ServiceToGitHubUserMapFilePath = "/contents/.github/workflows/resources/service-subservice-to-github-user-map.csv" - $ServiceToGitHubUserMapRef = $DefaultBranch + $ServiceToGitHubUserMapRef = "reviewer-test" $ServiceToGitHubUserMapUrl = $GitHubApiUrl + $ServiceToGitHubUserMapRepo + $ServiceToGitHubUserMapFilePath + "?ref=$ServiceToGitHubUserMapRef" + $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" + $WorkflowsRef = "workflows-test" + + # URL of the org team to check. We're using the "everyone" team in MicrosoftDocs. + $OrgTeamUrl = "https://api.github.com/orgs/microsoftdocs/teams/everyone/memberships/" + # Set repo variables $RepoUrl = $GitHubData.event.repository.url @@ -757,6 +765,156 @@ jobs: } + ##################### + ##################### + # Get-PrMessage + + Function Get-PrMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $PrMessageName + ) + + $PrMessageFile = "$WorkflowsResourcePath/$PrMessageName.md?ref=$WorkflowsRef" + + Try { + + Write-Host "Getting PR message from $PrMessageFile" + + $PrMessageData = Invoke-RestMethod -Uri $PrMessageFile -Headers $GitHubHeaders + $PrMessage = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($PrMessageData.content)); + + } Catch { + + Write-Host "Failed to get PR message $PrMessageName" + + $PrMessage = $Null + + } + + Return $PrMessage + + } + + ##################### + ##################### + # Set-PrConversationMessage + + Function Set-PrConversationMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $Message + ) + + $BodyHash = @{} + $BodyHash.body = $Message + $BodyJson = $BodyHash | ConvertTo-Json + + If (($Message -ne $Null) -and ($Message -ne "")) { + + Try { + + Write-Host "Posting message to PR conversation to $CommentsUrl." + $Result = Invoke-WebRequest -UseBasicParsing -Uri $CommentsUrl -Body $BodyJson -Headers $GitHubHeaders -Method POST -ErrorAction Stop + + } Catch { + + Write-Host "ERROR: Failed to post message to PR conversation. $($Error[0])" + + } + + } Else { + + Write-Host "Message is null or empty. Not posting to PR conversation." + + } + + } + + ##################### + ##################### + # Add-AtPrefix + + Function Add-AtPrefix { + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string[]]$GitHubReviewers, + + [Parameter(Mandatory = $false)] + [string]$Separator = " " + ) + + $PrefixedStrings = @() + + ForEach ($Reviewer In $GitHubReviewers) { + $PrefixedStrings += "@$Reviewer" + } + + Return ($PrefixedStrings -join $Separator) + + } + + ##################### + ##################### + # Test-OrgMembership + + Function Test-OrgMembership { + + [CmdletBinding()] + Param( + [Parameter(Mandatory = $true)] + [string[]]$GitHubReviewers + ) + + $ReviewerHashTable = @{} + + ForEach ($Reviewer in $GitHubReviewers) { + + Write-Host "Checking to see if $Reviewer is a member of the MicrosoftDocs `"everyone`" team." + + # Create the member URL to check + $ReviewerTeamUrl = $OrgTeamUrl + $Reviewer + + Try { + + # Check to see if the reviewer is a member of the MicrosoftDocs "everyone" team + $TeamResult = Invoke-RestMethod -Uri $ReviewerTeamUrl -Headers $AppGitHubAccessHeaders -ErrorAction Stop + + If ($TeamResult.state -eq "active") { + + Write-Host "$Reviewer is an active member of the MicrosoftDocs `"everyone`" team." + + $ReviewerHashTable.Add($Reviewer, $True) + + } Else { + + Write-Host "$Reviewer isn't an active member of the MicrosoftDocs `"everyone`" team." + + $ReviewerHashTable.Add($Reviewer, $False) + + + } + + } Catch { + + Write-Host "ERROR: Failed to look up membership info for $Reviewer. Error: $_" + + # If the call fails, set to false to avoid breaking add reviewer call later in workflow. + $ReviewerHashTable.Add($Reviewer, $False) + + } + + } + + Return $ReviewerHashTable + + + } ##################### ##################### @@ -876,8 +1034,24 @@ jobs: $ReviewerArray = $ReviewerArray | Select-Object -Unique - Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ReviewerArray + $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray + $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts + + $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") + + If ($ValidatedReviewerAccounts.Length -gt 0) { + + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + + Set-PrConversationMessage -Message $ReviewerMessage + + } Else { + + Write-Host "ERROR: No valid reviewers to assign." + Write-Host $TestedReviewerAccounts + } # Don't add assignments to PRs in excluded branches listed in $ExcludedBranchList If (!$ExcludedBranchList.Contains($TargetBranch)) { @@ -918,4 +1092,4 @@ jobs: } # Number of files in PR check - } # PR event and action check \ No newline at end of file + } # PR event and action check From 2ce6efd01c1e9a1f66166823a165b9b4ce6024f6 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:13:30 -0700 Subject: [PATCH 4/7] Only add reviewers on draft and if userperms = read --- .github/workflows/Shared-AutoLabelAssign.yml | 82 +++++++++++++------- 1 file changed, 56 insertions(+), 26 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 24610d89a2b..123c29a1402 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -71,6 +71,7 @@ jobs: $TargetBranch = $GitHubData.event.pull_request.base.ref $GitHubState = $GitHubData.event.pull_request.state $GitHubAction = $GithubData.event.action + $IsPrDraft = $GitHubData.event.pull_request.draft $GitHubSender = $GitHubData.event.sender.login $GitHubRepoName = $GitHubData.event.repository.name $RepoLabelUrl = $GitHubData.event.repository.labels_url @@ -78,6 +79,8 @@ jobs: $IssueUrl = $GitHubData.event.pull_request.issue_url $PrUrl = $GitHubData.event.pull_request.url $CommentsUrl = $GitHubData.event.pull_request.comments_url + $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$GitHubSender/permission" ) + $PrCreator = $GitHubData.event.pull_request.user.login @@ -933,7 +936,7 @@ jobs: Write-Host "Auto label on $GitHubRepoName`: $AutoLabel" Write-Host "Excluded branch list: $ExcludedBranchList" - If ((($GitRequestEvent -eq "pull_request") -or ($GitRequestEvent -eq "pull_request_target")) -and (($GitHubAction -eq "opened") -or ($GitHubAction -eq "reopened") -or ($GitHubAction -eq "synchronize"))) { + If ((($GitRequestEvent -eq "pull_request") -or ($GitRequestEvent -eq "pull_request_target")) -and (($GitHubAction -eq "opened") -or ($GitHubAction -eq "ready_for_review") -or ($GitHubAction -eq "reopened") -or ($GitHubAction -eq "synchronize"))) { Try { @@ -1012,44 +1015,67 @@ jobs: # Auto user assignment can be disabled on repos using the $DataTableName table. If ($AutoAssignUsers -eq $True) { - # Get the service/subservice to GitHub account map - $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable - $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap + # There's a short window between PR open and when Draft is set by the TierManagement workflow where PR reviewers could be added. + # This If statement prevents that from happening. + If (($GitHubAction -ne "opened") -and ($IsPrDraft -eq $False)) { - $ReviewerArray = @() + # Get permission level of user who created the comment. Need to use .role_name instead of .permission because .permission provides only legacy values. + # .role_name provides legacy plus triage, maintain, and custom roles like write-elevated. + $UserPermission = $(Invoke-RestMethod -Method GET -Headers $GitHubHeaders -Uri $UserPermissionUrl).role_name - ForEach ($File in $FileMetadataArray) { - - $FileReviewers = $Null + # Only add reviewers if the submitter can't sign off on their own PR. + If ($UserPermission -eq "read") { - $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + # Get the service/subservice to GitHub account map + $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable + $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap - If ($FileReviewers -ne $Null) { - - $ReviewerArray += $FileReviewers - - } - - } + $ReviewerArray = @() + + ForEach ($File in $FileMetadataArray) { + + $FileReviewers = $Null - $ReviewerArray = $ReviewerArray | Select-Object -Unique + $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService - $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray - $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } - $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts + If ($FileReviewers -ne $Null) { + + $ReviewerArray += $FileReviewers + + } + + } - $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") + $ReviewerArray = $ReviewerArray | Select-Object -Unique - If ($ValidatedReviewerAccounts.Length -gt 0) { + $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray + $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts - Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") - Set-PrConversationMessage -Message $ReviewerMessage + If ($ValidatedReviewerAccounts.Length -gt 0) { + + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + + Set-PrConversationMessage -Message $ReviewerMessage + + } Else { + + Write-Host "ERROR: No valid reviewers to assign." + Write-Host $TestedReviewerAccounts + + } + + } Else { + + Write-Host "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." + + } } Else { - Write-Host "ERROR: No valid reviewers to assign." - Write-Host $TestedReviewerAccounts + Write-Host "Not adding PR reviewers. PR action: $GitHubAction. Draft state: $IsPrDraft." } @@ -1092,4 +1118,8 @@ jobs: } # Number of files in PR check + } Else { + + Write-Host "Event action not ready_for_review, opened, reopened, or synchronize." + } # PR event and action check From 2f4b7d32f616ae37020a96e8bc7d1b7ba398a96b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 3 Oct 2025 15:55:56 -0700 Subject: [PATCH 5/7] Correct permissions url --- .github/workflows/Shared-AutoLabelAssign.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 123c29a1402..b91ed81129e 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -68,6 +68,7 @@ jobs: $GitRequestEvent = $GitHubData.event_name $DefaultBranch = $GitHubData.event.repository.default_branch + $PrCreator = $GitHubData.event.pull_request.user.login $TargetBranch = $GitHubData.event.pull_request.base.ref $GitHubState = $GitHubData.event.pull_request.state $GitHubAction = $GithubData.event.action @@ -79,10 +80,7 @@ jobs: $IssueUrl = $GitHubData.event.pull_request.issue_url $PrUrl = $GitHubData.event.pull_request.url $CommentsUrl = $GitHubData.event.pull_request.comments_url - $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$GitHubSender/permission" ) - - - $PrCreator = $GitHubData.event.pull_request.user.login + $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$PrCreator/permission" ) $AutoAssignUsers = [bool][int]$env:AutoAssignUsers # If it works, it works! $AutoAssignReviewers = [bool][int]$env:AutoAssignReviewers # Ditto From 10ea272bffc3c5ea129d7a4a7470b40a161538e1 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 3 Oct 2025 21:01:36 -0700 Subject: [PATCH 6/7] Update reviewer notification message for clarity --- .../resources/AutoLabelAssign-ContentLeadReviewNotice.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md b/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md index 4b9a8b1fe57..d02afe25bb8 100644 --- a/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md +++ b/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md @@ -1,5 +1,5 @@ -You've been added as a reviewer to this pull request because you're a Content Lead for the product area or feature covered by one or more of the articles in this PR. +You've been added as a reviewer to this pull request because you're [listed](https://aka.ms/getaxonomy) as a Content Lead for the product area or feature covered by one or more articles in this PR. Before this PR can be merged, a Content Lead needs to add the **Sign off** label to it. Before you sign off a PR, confirm the following: @@ -20,4 +20,4 @@ the PR to the submitter with required changes. After they've completed those cha When the PR passes PubOps review, they'll merge the PR. If the PR is merged into the **main** branch, changes will go live within the next couple hours. -If you have questions about this process or need help, post a message to https://aka.ms/tcphelp. +If you think you were incorrectly added as a reviewer, have questions about this process, or need help signing off on this PR, post a message to https://aka.ms/tcphelp. From b9ac7682a55ffcc49e9031e64e863dd1785a2255 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Sat, 4 Oct 2025 22:29:37 -0700 Subject: [PATCH 7/7] Add existing reviewers check --- .github/workflows/Shared-AutoLabelAssign.yml | 67 ++++++++++++++++++-- 1 file changed, 62 insertions(+), 5 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index b91ed81129e..58ebbcd8888 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -917,6 +917,48 @@ jobs: } + ##################### + ##################### + # Compare-PRIndividualReviewers + + Function Compare-PRIndividualReviewers { + + [CmdletBinding()] + [OutputType([bool])] + Param( + [Parameter(Mandatory = $true)] + [string[]]$GitHubReviewers + + ) + + Try { + + # Get PR details from GitHub API + $PrData = Invoke-RestMethod -Uri $PrUrl -Headers $GitHubHeaders -Method Get + + # Extract individual reviewer usernames (user objects in requested_reviewers) + $CurrentReviewers = @() + + If ($PrData.requested_reviewers) { + + $CurrentReviewers = $PrData.requested_reviewers | ForEach-Object { $_.login } + + } + + # Compare arrays - check if any expected accounts are missing from individual reviewers + $MissingAccounts = $GitHubReviewers | Where-Object { $_ -notin $CurrentReviewers } + + Return ($MissingAccounts.Count -gt 0) + + } Catch { + + Write-Error "Failed to retrieve or compare PR reviewers: $_" + Return $false + + } + + } + ##################### ##################### # Main @@ -1046,25 +1088,40 @@ jobs: $ReviewerArray = $ReviewerArray | Select-Object -Unique + Write-Host "Checking org membership." + $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } - $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts - $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts If ($ValidatedReviewerAccounts.Length -gt 0) { - Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ValidatedReviewerAccounts + + If ($MissingReviewers) { + + Write-Host "Additional reviewers found. Setting reviewers and posting PR comment." + + $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") - Set-PrConversationMessage -Message $ReviewerMessage + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + Set-PrConversationMessage -Message $ReviewerMessage + + } Else { + + Write-Host "No additional reviewers to add. Not setting reviewers or posting PR comment." + + } } Else { - Write-Host "ERROR: No valid reviewers to assign." + Write-Host "No valid reviewers to assign." Write-Host $TestedReviewerAccounts } + } Else { Write-Host "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission."