From b36706ed12a1d8343840387a1a96c9fa98e3750f Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 11 Jul 2024 21:19:53 -0700 Subject: [PATCH 01/92] Create workflow --- .github/workflows/Shared-AutoLabelAssign.yml | 605 +++++++++++++++++++ 1 file changed, 605 insertions(+) create mode 100644 .github/workflows/Shared-AutoLabelAssign.yml diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml new file mode 100644 index 00000000000..c5389420bda --- /dev/null +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -0,0 +1,605 @@ +name: Auto label and assign pull requests + +permissions: + pull-requests: write + contents: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + ExcludedUserList: + required: true + type: string + ExcludedBranchList: + required: true + type: string + AutoAssignUsers: + required: true + type: string + AutoLabel: + required: true + type: string + secrets: + AccessToken: + required: true + +jobs: + build: + name: Run Script + runs-on: windows-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + AutoAssignUsers: ${{ inputs.AutoAssignUsers }} + AutoLabel: ${{ inputs.AutoLabel }} + ExcludedUserList: ${{ inputs.ExcludedUserList }} + ExcludedBranchList: ${{ inputs.ExcludedBranchList }} + run: | + # Get runspace info + $RepoRoot = $env:RUNNER_WORKSPACE + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $AccessToken = $env:AccessToken + + $GitRequestEvent = $GitHubData.event_name + $DefaultBranch = $GitHubData.event.repository.default_branch + $TargetBranch = $GitHubData.event.pull_request.base.ref + $GitHubState = $GitHubData.event.pull_request.state + $GitHubAction = $GithubData.event.action + $GitHubSender = $GitHubData.event.sender.login + $GitHubRepoName = $GitHubData.event.repository.name + $RepoLabelUrl = $GitHubData.event.repository.labels_url + $PrFileListUrl = "$($GitHubData.event.pull_request.url)/files" + $IssueUrl = $GitHubData.event.pull_request.issue_url + + $AutoAssignUsers = [bool][int]$env:AutoAssignUsers # If it works, it works! + $AutoLabel = [bool][int]$env:AutoLabel # Ditto + $ExcludedUserList = $env:ExcludedUserList | ConvertFrom-Json + $ExcludedBranchList = $env:ExcludedBranchList | ConvertFrom-Json + + + # Set GitHub REST API headers + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $AccessToken") + $GitHubHeaders.Add("User-Agent", "dstrome") + + # Regex for string matches + $AuthorRegex = "(?m)^(author:\s{0,3})([\w|\-]{1,39})" + $ServiceRegex = "(ms\.service:\s{0,3})([\w|\-|\.]{1,60})" + $SubServiceRegex = "(ms\.subservice:\s{0,3})([\w|\-|\.]{1,60})" + $TechnologyRegex = "(ms\.technology:\s{0,3})([\w|\-|\.]{1,60})" + $ProdRegex = "(ms\.prod:\s{0,3})([\w|\-|\.]{1,60})" + $TierRegex = "(\s*-\s*)(tier\d|Selfserve)" + + $LabelColor = "BFDADC" + $LabelDescription = "" + + # Set repo variables + $RepoUrl = $GitHubData.event.repository.url + + Write-Host "Repository URL: $RepoUrl" + + #$data = Invoke-RestMethod -Uri $RepoUrl -Headers $GitHubHeaders + + + + ##################### + ##################### + # Get-FileMetadata + + Function Get-FileMetadata { + + [cmdletbinding()] + Param ( + $PrFileList + ) + + # Initialize output $FileArray. + $FileArray = @() + + # Iterate through each file in the PR. + ForEach ($File in $PrFileList) { + + # Initialize variables. + $FileData = @{} + $MetadataFound = $False + # Check to see whether current file is markdown or YAML. Those are the only files we care about. Ignore the rest. + $IsContentFile = ($File.filename.EndsWith(".md") -or $File.filename.EndsWith(".yml")) -and !$File.filename.ToLower().Contains("/toc.") + + # Only process content files. + If ($IsContentFile) { + + Write-Host "Processing file $($File.filename)." + + # Retrieve file contents from GitHub. File contents is returned in Base 64 so after contents is retrieved, convert from Base 64 to plain text. + $FileContentsBase64 = Invoke-RestMethod -Method GET -Uri $File.contents_url -Headers $GitHubHeaders + $FileContents = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($FileContentsBase64.content)) + + # Check to see if the file contents contains a string that matches the $AuthorRegex regex pattern. If yes, add value to $Author, if not assign $Null. + If ($FileContents -match $AuthorRegex) { + $Author = $Matches[2] + $MetadataFound = $True + Write-Host "Found author $Author." + } Else { + $Author = $Null + } + + # Check to see if file contents contains a string that matches the $ServiceRegex regex pattern. If yes, add value to $Service. Then check SubService regex pattern. + # If value isn't matched, assign $Null and don't check SubService. + If ($FileContents -match $ServiceRegex) { + $Service = $Matches[2] + $MetadataFound = $True + Write-Host "Found service $Service." + If ($FileContents -match $SubServiceRegex) + { + $SubService = $Matches[2] + $MetadataFound = $True + Write-Host "Found sub service $SubService." + } Else { + $SubService = $Null + } + } Else { + $Service = $Null + } + # Check to see if file contents contains a string that matches the $ProdRegex regex pattern. If yes, add value to $Product. Then check TechnologyRegex regex pattern. + # If value isn't matched, assign $Null and don't check Technology. + If ($FileContents -match $ProdRegex) { + $Product = $Matches[2] + $MetadataFound = $True + Write-Host "Found product $Product." + If ($FileContents -match $TechnologyRegex) { + $Technology = $Matches[2] + $MetadataFound = $True + Write-Host "Found technology $Technology." + } Else { + $Technology = $Null + } + } Else { + $Product = $Null + } + # Check to see if the file contents contains a string that matches the $TierRegex regex pattern. If yes, add value to $Tier, if not assign $Null. + If ($FileContents -match $TierRegex) { + $Tier = $Matches[2] + $MetadataFound = $True + Write-Host "Found tier $Tier." + } Else { + $Tier = $Null + } + + # Add results of above tests to an utput object. Each property contains the results of the associated tests from above. + $FileData = @{ + Service = $Service + SubService = $SubService + Product = $Product + Technology = $Technology + Tier = $Tier + Author = $Author + } + + # If any metadata or author data was found, add it to the $FileArray output array. + If ($MetadataFound) { + Write-Host "Metadata or author data found on $($File.filename). Adding to output array." + $FileArray += $FileData + } + } + } + + # Return $FileArray output array to calling statement. + Return $FileArray + + } + + Function Format-FileMetadata { + + [cmdletbinding()] + Param ( + $FileMetadata + ) + + # Initialize arrays + $MetadataArray = @() + $AuthorArray = @() + + # Iterate through each file in the $FileMetaData array. Each element is an individual file found in the PR with six properties containing metadata field values. + Foreach ($File in $FileMetadata) { + + # Check whether each property contains data and, if so, add it to the $MetaDataArray and AuthorArray arrays with the format or . + If ($File.Service -ne $Null) {$MetadataArray += "$($File.Service)/svc"} + If ($File.SubService -ne $Null) {$MetadataArray += "$($File.SubService)/subsvc"} + If ($File.Product -ne $Null) {$MetadataArray += "$($File.Product)/prod"} + If ($File.Technology -ne $Null) {$MetadataArray += "$($File.Technology)/tech"} + If ($File.Tier -ne $Null) {$MetadataArray += $File.Tier.SubString(0,1).ToUpper() + $File.Tier.SubString(1).ToLower()} + If ($File.Author -ne $Null) {$AuthorArray += $File.Author} + + } + + # Because there might be multiple files in the $MetaDataArray and $AuthorArrays with the same metadata and author data, duplicate values might have been added to the output arrays. + # We only need one instance of each metadata field or author value so the following removes any duplicate values. + $MetadataArray = $MetadataArray | Select-Object -Unique + $AuthorArray = $AuthorArray | Select-Object -Unique + + # Added the output arrays to an output hash table. + $OutputHashTable = @{ + + FileMetadata = $MetadataArray + AuthorMetadata = $AuthorArray + + } + + # Output the output hash table to the calling statement. + Return $OutputHashTable + + } + + ##################### + ##################### + # Test-RepoLabel + + Function Test-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $RepoUri + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $LabelUri = $RepoUri.Replace("{/name}","/$Name") + + # Check to see if the label we want exists in the repo + Try { + + Write-Host "Checking to see if label $Name exists in repo URL $LabelUri." + + $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $GitHubHeaders -ErrorAction Stop + $LabelFound = $True + + } Catch { + + # OK if label doesn't exist. Just means we need to create it. + $LabelFound = $False + + } + + # Return boolean to calling statement + $LabelFound + + } + + ##################### + ##################### + # New-RepoLabel + + Function New-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $Color, + $Description, + $RepoUri + ) + + # Remove placeholder text from repo URL + $RepoUri = $RepoUri.Replace("{/name}","") + $Result = $Null + + # Construct the JSON statement that will be sent to GitHub as the body of the web request. Include the name of the label, its color, and description. + # Convert hash table to JSON + $Body = @{} + $Body.Add("name", $Name) + $Body.Add("color", $Color) + $Body.Add("description", $description) + $Body = $Body | ConvertTo-Json + + # Try to submit the request to GitHub API to create the label + Try { + + Write-Host "Creating label $Name with color $Color on repo $RepoUri." + + $Result = Invoke-RestMethod -Uri $RepoUri -Headers $GitHubHeaders -Body $Body -Method POST + + } Catch { + + Write-Error "ERROR: Failed to create new label $Name on repo $RepoUri. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Test-PrLabel + Function Test-Prlabel { + + [CmdletBinding()] + param( + + $LabelArray, + $IssueUrl + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $IssueLabelUrl = "$IssueUrl/labels" + $LabelHashTable = @{} + $LabelResults = $Null + + # Get list of labels on issue/PR + Try { + + Write-Host "Getting labels on issue $IssueLabelUrl." + + $LabelResults = Invoke-RestMethod -Uri $IssueLabelUrl -Headers $GitHubHeaders -ErrorAction Stop + + } Catch { + + Write-Error "ERROR: Failed to get list of labels on $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + + } + + ForEach ($Label in $LabelArray) { + + If ($LabelResults -ne $Null) { + + If ($LabelResults.name.Contains($Label)) { + + $LabelHashTable.Add($Label, $True) + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } + + + # Return array of labels on Issue/PR + Return $LabelHashTable + + + } + + ##################### + ##################### + # Set-PrLabel + + Function Set-PrLabel { + + param( + + $IssueUrl, + $LabelName + + ) + + # Construct label URL based on issue or pull request URL + $IssueLabelUrl = "$IssueUrl/labels" + + # Construct JSON statement that will be sent to GitHub as the body of the web request. Includes only the label name. GitHub expects an array even thought it's a single value + # Convert array to JSON + $Body = @() + $Body += $LabelName + $Body = ConvertTo-Json -InputObject $Body + + # Try to submit the request to GitHub API to apply they label to the issue or pull request + Try { + + Write-Host "Setting label $LabelName on URL $IssueLabelUrl." + + $Result = Invoke-RestMethod -Uri $IssueLabelUrl -Body $Body -Headers $GitHubHeaders -Method POST + + + } Catch { + + Write-Error "ERROR: Failed to set label on URL $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Set-PrAssignee + + Function Set-PrAssignee { + + param( + + $IssueUrl, + $GitHubAssignees + + ) + + $UsersToAssign = @() + + # $ExcludedUserList is a list of users from $DataTableData that shouldn't be assigned to PRs for some reason. If it's $Null, no users need to be excluded. + If ($ExcludedUserList -ne $Null) { + + # Loop through each of the eligible assignees found in the PR. + ForEach ($User in $GitHubAssignees) { + + # Check if an assignee is found in the exclusion list. If yes, exclude the user by not adding to users to assign list. If no, add the user to the users to assign list. + If ($ExcludedUserList.Contains($User)) { + + Write-Host "Excluding $User from assignment list." + + } Else { + + $UsersToAssign += $User + + } + + } + + } Else { + + $UsersToAssign = $GitHubAssignees + + } + + # 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("assignees", $UsersToAssign) + $Body = $Body | ConvertTo-Json + + $AssigneeUrl = "$IssueUrl/assignees" + + # Try to submit the request to GitHub API to apply they label to the issue or pull request + Try { + + Write-Host "Setting accounts $UsersToAssign on URL $AssigneeUrl." + + $Result = Invoke-RestMethod -Uri $AssigneeUrl -Body $Body -Headers $GitHubHeaders -Method POST + + } Catch { + + Write-Error "ERROR: Failed assign GitHub accounts on URL $AssigneeUrl. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Main + + Write-Host "Repo: $GitHubRepoName" + Write-Host "Sender: $GitHubSender" + Write-Host "Request event: $GitRequestEvent" + Write-Host "GitHub action: $GitHubAction" + Write-Host "GitHub state: $GitHubState" + Write-Host "Request Id: $RequestId" + Write-Host "Default branch: $DefaultBranch" + Write-Host "Target branch: $TargetBranch" + Write-Host "PR files URL: $PrFileListUrl" + + Write-Host "Auto assign users on $GitHubRepoName`: $AutoAssignUsers" + Write-Host "Auto label on $GitHubRepoName`: $AutoLabel" + Write-Host "Excluded branch list: $ExcludedBranchList" + + If (($GitRequestEvent -eq "pull_request") -and (($GitHubAction -eq "opened") -or ($GitHubAction -eq "reopened") -or ($GitHubAction -eq "synchronize"))) { + + Try { + + Write-Host "Getting file list for PR $PrFileListUrl." + + # Intentionally only getting the first 30 files. More than that is just overkill. Keep in mind that this can include non-yml/md files so the resulting + # number of processed files may be less than 30. + $FileList = Invoke-RestMethod -Method GET -Uri $PrFileListUrl -Headers $GitHubHeaders + + } Catch { + + Write-Error "ERROR: Failed to retrieve file list on PR $PrFileListUrl. Error: $($Error[0].Exception.Message)." + + } + + If ($FileList.Count -ge 1) { + + # Get file metadata (metadata = service, subservice, tech, prod, etc and author metadata) + $FileMetadataArray = Get-FileMetadata -PrFileList $FileList + + If ($FileMetadataArray -ne $Null) { + + # Split metadata into .FileMetadata and .AuthorMetadata. .FileMetadata is a one-dimentional array that includes service, subservice, tech, prod, entries. + $FormattedMetadata = Format-FileMetadata -FileMetadata $FileMetadataArray + + Write-Host "File metadata list: $($FormattedMetadata.FileMetadata)." + Write-Host "Author metadata list: $($FormattedMetadata.AuthorMetadata)." + + # Auto labelling can be disabled on repos using the $DataTableName table. + If ($AutoLabel -eq $True) { + + # Loop through each element in the .FileMetadata array to check if value exists as a label on the repo. If not, create the label on the repo. + # Don't confuse with the next ForEach which is PR-level check. + ForEach ($Item in $FormattedMetadata.FileMetadata) { + + # Check if label exists on the repo. + $LabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $Item + + If (!$LabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $Item -Color $LabelColor -Description $LabelDescription + + } + + } + + # Get a list of all labels that exist on the PR in a hash table. Hash table includes the name of the item in the .FileMetadata array and a boolean + # that indicates whether the item exists as a label on the PR. + # The reason this isn't part of the above repo-level check is because the all the PR-level labels can be retrieved with a single call to GitHub. + # If we included the PR-level checks in the same block as the repo-level checks, we'd be requesting the PR-level repos times. + # Don't confuse with the previous ForEach which is a repo-level check. + $LabelResultsArray = Test-Prlabel -LabelArray $FormattedMetadata.FileMetadata -IssueUrl $IssueUrl + + Write-Host "Number of items in LabelResultsArray: $($LabelResultsArray.Count)." + + # Loop through all the keys on the label results array. The keys are the items/label names + ForEach ($Label in $LabelResultsArray.Keys) { + + If (!$LabelResultsArray.$Label) { + + Write-Host "Label $Label doesn't exist on $IssueUrl. Adding label." + + Set-PrLabel -IssueUrl $IssueUrl -LabelName $Label + + } + + } + + } Else { + + Write-Host "Auto labelling for the repo $GitHubRepoName is disabled. Skipping labelling." + + } + + # Auto user assignment can be disabled on repos using the $DataTableName table. + If ($AutoAssignUsers -eq $True) { + + # Don't add assignments to PRs in excluded branches listed in $ExcludedBranchList + If (!$ExcludedBranchList.Contains($TargetBranch)) { + + Set-PrAssignee -IssueUrl $IssueUrl -GitHubAssignees $FormattedMetadata.AuthorMetadata + + } Else { + + Write-Host "Target branch $TargetBranch is an excluded branch. Not adding author assignments." + + } + + } Else { + + Write-Host "Auto assignment of users for the label $GitHubRepoName is disabled. Skipping auto assignment." + + } + + } Else { + + Write-Host "No file metadata found in PR $PrFileListUrl." + + } # Metadata found on files check + + } Else { + + Write-Host "No files found in PR $PrFileListUrl." + + } # Number of files in PR check + + } # PR event and action check + From 6c6d56b5e41ed0169ae66a73c7520fefa86815ff Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 12 Jul 2024 15:12:21 -0700 Subject: [PATCH 02/92] add pull_request_target event --- .github/workflows/Shared-AutoLabelAssign.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index c5389420bda..62b6cc9b5f8 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -493,7 +493,7 @@ jobs: Write-Host "Auto label on $GitHubRepoName`: $AutoLabel" Write-Host "Excluded branch list: $ExcludedBranchList" - If (($GitRequestEvent -eq "pull_request") -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 "reopened") -or ($GitHubAction -eq "synchronize"))) { Try { From 0cf00ae1d168cb30ae9dff0f18177a6bea0e753c Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 16 Jul 2024 12:22:00 -0700 Subject: [PATCH 03/92] Create Shared-ProtectedFiles.yml --- .github/workflows/Shared-ProtectedFiles.yml | 278 ++++++++++++++++++++ 1 file changed, 278 insertions(+) create mode 100644 .github/workflows/Shared-ProtectedFiles.yml diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml new file mode 100644 index 00000000000..4b3fa11acd6 --- /dev/null +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -0,0 +1,278 @@ +name: Protected files + +permissions: + pull-requests: write + statuses: write + contents: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + secrets: + AccessToken: + required: true + +jobs: + build: + name: Run Script + runs-on: windows-latest + steps: + - name: Script + shell: pwsh + env: + + ProtectedFileList: '[".gitignore", ".openpublishing.publish.config.json", "docfx.json", "README.md", "LICENSE-CODE", "ThirdPartyNotices", ".acrolinx-config.edn", ".gitattributes", "ProtectedFiles.yml", "Shared-ProtectedFiles.yml", "AutoLabelAssign.yml", "Shared-AutoLabelAssign.yml"]' + ApproverList: '["dstrome"]' + HelpUrl: 'https://review.learn.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main' + + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + run: | + + # Get GitHub data + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $AccessToken = $env:AccessToken + $GitRequestEvent = $GitHubData.event_name + + # Get data from environment variables and convert lists from JSON + $ProtectedFileList = $env:ProtectedFileList | ConvertFrom-Json + $ApproverList = $env:ApproverList | ConvertFrom-Json + $HelpUrl = $env:HelpUrl + + # Create github HTTP authentication header + $UserAgent = "officedocs" + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", $UserAgent) + + # Start building hash table that contains status check fields to be sent to GitHub. Context and Target_Url always stay the same. + # State and Description change depending on result of checks below. + $Status = @{} + $Status.Add("context", "max/protected-file-check") + $Status.Add("target_url", $HelpUrl) + + $RepoName = $GitHubData.event.repository.name + + $FatalError = $False + + Write-Host "Repo: $RepoName" + Write-Host "Sender: $($GitHubData.event.sender.login)" + Write-Host "Request type: $GitRequestEvent" + Write-Host "GitHub action: $($GitHubData.event.action)" + Write-Host "PR URL: $($GitHubData.event.pull_request.url)/files" + Write-Host "Request ID: $RequestId" + + # Only process event types of 'pull_request_target' that are either 'opened' (PR created) or 'synchronized' (PR commits updated) + If (($GitRequestEvent -eq "pull_request_target") -and (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened"))) { + + # Collect info from payload that we'll need to process the PR + $FileListUrl = "$($GitHubData.event.pull_request.url)/files" + $PrSubmitter = $GitHubData.event.pull_request.user.login + $PrSubmitterPermissionsUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$PrSubmitter/permission") + $StatusUrl = $GitHubData.event.pull_request.statuses_url + $TargetBranch = $GitHubData.event.pull_request.base.ref + $DefaultBranch = $GitHubData.event.repository.default_branch + $PublishBranch = "live" + + Write-Host "Processing PR. Default branch: $DefaultBranch. Publish branch: $PublishBranch." + + # Set status check state fields to show the PR is being processed. The entire process usually only takes a couple seconds but + # can take longer. GitHub will show PR status check is processing. + # Convert hash table to JSON. + $Status.Add("state","pending") + $Status.Add("description", "Checking for protected files.") + $StatusJson = $Status | ConvertTo-Json + + # Catch exceptions if they happen. All web and REST requests are set to halt on error and throw an exception. + Try { + + Write-Host "Sending `"pending`" status to GitHub." + + # Sent POST request to GitHub to set the status check. Subsequent POST and GET requests use similar parameters - + # GitHubHeaders - GitHub authentication token + # StatusUrl - REST API endpoint to GET or POST data from/to + # StatusJson - Payload to send to GitHub for POST requests + Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + + # Short delay so check doesn't finish before others start, which can cause GitHub UI to display confusing behavior. + Start-Sleep 3 + + # Only process PRs being submitted to $PublishBranch or $DefaultBranch branches. Also skip if a fatal error is encountered. + If ((($TargetBranch -eq $DefaultBranch) -or ($TargetBranch -eq $PublishBranch)) -and (!$FatalError)) { + + Write-Host "$DefaultBranch or $PublishBranch branch" + + # Get the list of files modified in the PR. + # Get the permissions of the PR submitter. + $FileList = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $FileListUrl -ErrorAction Stop + $PrSubmitterPerms = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $PrSubmitterPermissionsUrl -ErrorAction Stop + + $ProtectedFileFound = $False + + # Only process PRs that actually have changed files. + If ($($FileList.Count -gt 0)) { + + Write-Host "PR has files`n`rFile list: $($FileList.filename)" + + # Process PRs that are submitted by non-admins + If ($PrSubmitterPerms.permission -ne "admin") { + + Write-Host "Submitter is not an admin" + + # Check to see if submitter is an protected file approver + + $ValidApprover = $False + + If ($ApproverList.Contains($PrSubmitter)) { + + $ValidApprover = $True + + } + + # Only check if there's a protected file in the PR if the PR submitter isn't a protected file approver. + If ($ValidApprover -eq $False) { + + Write-Host "Not an admin or protected file approver. Checking files." + + # Loop through the protected file list and check to see if any of them are present in the changed file list. If we do, set the flag + # and the break out of the loop. + ForEach ($File in $ProtectedFileList) { + + $File = $File.ToLower() + + ForEach ($PrFile in $FileList) { + + $PrFile = $PrFile.filename.ToLower() + + If ($PrFile.Contains($File)) { + + $ProtectedFileFound = $True + + Write-Host "PROTECTED FILE: $PrFile" + + Break + + } + + } + + } + + # If a protected file is found, set state to 'failure'. If protected not found, set to 'success'. Set description accordingly. + If ($ProtectedFileFound) { + + Write-Host "Protected file found" + $Status.state = "failure" + $Status.description = "A protected file was found. Click Details for info." + + } Else { + + Write-Host "No protected files found" + $Status.state = "success" + $Status.description = "No protected files included in PR." + + } + + } Else { + + Write-Host "PR submitter $PrSubmitter is an approved protected file submitter." + + $Status.state = "success" + $Status.description = "PR submitter is an approved protected file submitter." + + } + + # Set status to 'success' since the submitter is an admin. + } Else { + + Write-Host "PR submitter $PrSubmitter is an admin." + + $Status.state = "success" + $Status.description = "PR submitter is an admin." + + } + + } Else { + + # Set status to 'success' since the PR doesn't contain any files. + $Status.state = "success" + $Status.description = "PR doesn't contain any files." + + } + + } Else { + + # Set status to 'success' since the PR isn't being submitted to $DefaultBranch or $PublishBranch. + $Status.state = "success" + $Status.description = "PR base branch isn't $DefaultBranch or $PublishBranch." + + } + + } Catch { + + $FatalError = $True + + } + + # If an exception was encountered, allow the status check to pass since we don't want to block PRs just because the + # check is broken for some reason. + If ($FatalError) { + + $Status.state = "success" + $Status.description = "Error encountered. Please notify marveldocs-admins." + + } + + # Get ready to push status check results that'll either allow or block the PR from progressing. + $StatusJson = $Status | ConvertTo-Json + $SuccessfulPost = $False + $RetryCount = 0 + + Do { + + Try { + + # Send POST request to GitHub + Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + $SuccessfulPost = $True + + } Catch { + + # If the request fails for any reason, retry it after a delay, up to six times. + $RetryCount++ + Start-Sleep 1 + + } + + } Until (($SuccessfulPost) -or ($RetryCount -gt 5)) + + # If an exception was encountered, or if the attempt to post the status check failed, send email notification with error and PR info. + If (($FatalError) -or (!$SuccessfulPost)) { + + $Body = "

An error occurred while processing the protected file check for a pull request.

" + $Body = $Body + "

Error encountered

" + $Body = $Body + "Fatal error: $FatalError
" + $Body = $Body + "Successful post: $SuccessfulPost
" + + ForEach ($Err in $Error) { + + $Body = $Body + "

$($Err.Exception.ToString())

" + $Body = $Body + "

$($Err.InvocationInfo.PositionMessage)

" + + } + + $Body = $Body + "

Pull request info

" + $Body = $Body + "

URL: $($GitHubData.event.pull_request.url)" + $Body = $Body + "

PR Submitter: $PrSubmitter

" + $Body = $Body + "

PR submitter permissions: $($PrSubmitterPerms.permission)

" + $Body = $Body + "

Protected file found: $ProtectedFileFound

" + $Body = $Body + "

File change list: $($FileList.filename)

" + + Write-Host "Error posting status check. $Body" + } + } + + From f2bf97ba9f16f7b0f31e2ba276eeb537051d95ac Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 17 Jul 2024 11:01:45 -0700 Subject: [PATCH 04/92] add pr file count messages --- .../PrFileCountCheck-PrivateBlockMessage.md | 28 +++++++++++++++++++ .../PrFileCountCheck-PrivateWarningMessage.md | 28 +++++++++++++++++++ .../PrFileCountCheck-PublicBlockMessage.md | 18 ++++++++++++ .../PrFileCountCheck-PublicWarningMessage.md | 24 ++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 .github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md create mode 100644 .github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md create mode 100644 .github/workflows/resources/PrFileCountCheck-PublicBlockMessage.md create mode 100644 .github/workflows/resources/PrFileCountCheck-PublicWarningMessage.md diff --git a/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md b/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md new file mode 100644 index 00000000000..702e76f3c4c --- /dev/null +++ b/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md @@ -0,0 +1,28 @@ +## ![STOP](https://docs.microsoft.com/en-us/office/media/internal/prfilecountcheck-stop-sign.png) Too many files - admin review required + +This **private** repo applies limits on the number of files that can be merged in a single pull request (PR): + +- **Warning** Warning message and ability to unblock PR using the *{3}*\* label - {1} +- **Block** Block message and admin review required - {2} + +This pull request will change **{0}** files, which exceeds the **block** limit of **{2}**. The number of files in this PR must be reduced below {2} or be reviewed by a repo admin before it can be merged. Admin reviews can take up to five business days. + +Before this pull request can be merged, you must review the list of files it contains and their changes to confirm they are correct and intentional. **This includes files you did not change and/or do not own**. If there are changes you aren't familiar with, contact the individuals who submitted the commits included in this pull request and review the changes with them. Use the **Preview** links in the build validation comments to verify the integrity of the site and its content. + +**The individual who requests this PR be merged is responsible for ensuring the changes included in it are correct, and for resolving any issues that might result in merging it.** + +### To merge this pull request + +We strongly recommend that you break up this PR into multiple PRs that contain fewer than {0} files. For help breaking up your PR, post a message to https://aka.ms/askanadmin. + +Alternatively, you can request a review by a repo admin by adding the **{4}**\* label. A member of the repo admin team will review your PR. Due to the difficulties and time-consuming nature of reviewing large PRs, an admin review can take up to five business days. + +### To reject this pull request + +If you want to abandon these changes and not merge this PR, click **Close pull request** at the bottom of this page to close it without merging. Work with your team to revert any changes that aren't correct. + +### Need help? + +If you need help, post a message to https://aka.ms/askanadmin. + +\* You must have write or triage access to this repo to add labels. diff --git a/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md b/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md new file mode 100644 index 00000000000..0c56c433765 --- /dev/null +++ b/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md @@ -0,0 +1,28 @@ +## ![STOP](https://docs.microsoft.com/en-us/office/media/internal/prfilecountcheck-stop-sign.png) Caution: Large pull request + +This **private** repo applies limits on the number of files that can be merged in a single pull request (PR): + +- **Warning** Warning message and ability to unblock PR using the *{3}*\* label - {1} +- **Block** Block message and admin review required - {2} + +This pull request will change **{0}** files, which exceeds the **warning** limit of **{1}**. + +Before this pull request can be merged, you must review the list of files it contains and their changes to confirm they are correct and intentional. **This includes files you did not change and/or do not own**. If there are changes you aren't familiar with, contact the individuals who submitted the commits included in this pull request and review the changes with them. Use the **Preview** links in the build validation comments to verify the integrity of the site and its content. + +**The individual who merges this pull request is responsible for ensuring the changes included in it are correct, and for resolving any issues that might result in merging it.** + +### To merge this pull request + +If you have determined that the changes in this pull request are correct, add the **{3}**\* label to it, wait for the **max/pr-file-count** check to pass, and then click **Merge**. To add a label, click the gear icon next to **Labels** and then select **{3}**\*. + +![Label UI screenshot](https://docs.microsoft.com/en-us/office/media/internal/prfilecountcheck-label-screenshot.gif) + +### To reject this pull request + +If you can't confirm the changes in this pull request are correct, click **Close pull request** at the bottom of this page to close it without merging. Work with your team to revert any changes that aren't correct. + +### Need help? + +If you need help, post a message to https://aka.ms/askanadmin. + +\* You must have write or triage access to this repo to add labels. diff --git a/.github/workflows/resources/PrFileCountCheck-PublicBlockMessage.md b/.github/workflows/resources/PrFileCountCheck-PublicBlockMessage.md new file mode 100644 index 00000000000..ff2709fc6d9 --- /dev/null +++ b/.github/workflows/resources/PrFileCountCheck-PublicBlockMessage.md @@ -0,0 +1,18 @@ +## ![STOP](https://docs.microsoft.com/en-us/office/media/internal/prfilecountcheck-stop-sign.png) Too many files - PR blocked + +This **public** repo applies limits on the number of files that can be merged in a single pull request (PR): + +- **Warning** Warning message and ability to unblock PR using the *{3}*\* label - {1} +- **Block** Block message and PR is blocked - {2} + +This pull request will change **{0}** files, which exceeds the **block** limit of **{2}**. **This PR will not be merged**. + +### To merge this pull request + +You must either reduce the number of files being changed to below the {2} file limit or split the changes into multiple PRs. PRs must not have more than {2} files and fewer than {1} files if possible. + +### To reject this pull request + +If you want to abandon these changes and not merge this PR, click **Close pull request** at the bottom of this page to close it without merging. Work with your team to revert any changes that aren't correct. + +\* You must have write or triage access to this repo to add labels. diff --git a/.github/workflows/resources/PrFileCountCheck-PublicWarningMessage.md b/.github/workflows/resources/PrFileCountCheck-PublicWarningMessage.md new file mode 100644 index 00000000000..a8d3858c124 --- /dev/null +++ b/.github/workflows/resources/PrFileCountCheck-PublicWarningMessage.md @@ -0,0 +1,24 @@ +## ![STOP](https://docs.microsoft.com/en-us/office/media/internal/prfilecountcheck-stop-sign.png) Caution: Large pull request + +This **public** repo applies limits on the number of files that can be merged in a single pull request (PR): + +- **Warning** Warning message and ability to unblock PR using the *{3}*\* label - {1} +- **Block** Block message and PR is blocked - {2} + +This pull request will change **{0}** files, which exceeds the **warning** limit of **{1}**. + +Before this pull request can be merged, you must review the list of files it contains and their changes to confirm they are correct and intentional. **This includes files you did not change and/or do not own**. If there are changes you aren't familiar with, contact the individuals who submitted the commits included in this pull request and review the changes with them. + +**The individual who merges this pull request is responsible for ensuring the changes included in it are correct, and for resolving any issues that might result in merging it.** + +### To merge this pull request + +If you have determined that the changes in this pull request are correct, add the **{3}**\* label to it, wait for the **max/pr-file-count** check to pass, and then click **Merge**. To add a label, click the gear icon next to **Labels** and then select **{3}**\*. + +![Label UI screenshot](https://docs.microsoft.com/en-us/office/media/internal/prfilecountcheck-label-screenshot.gif) + +### To reject this pull request + +If you can't confirm the changes in this pull request are correct, click **Close pull request** at the bottom of this page to close it without merging. Work with your team to revert any changes that aren't correct. + +\* You must have write or triage access to this repo to add labels. From 0a75e09d6a63683878f44d90d5d57243e12c2f2b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 17 Jul 2024 16:58:10 -0700 Subject: [PATCH 05/92] Support pagination --- .github/workflows/Shared-ProtectedFiles.yml | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 4b3fa11acd6..d9dc6ceff63 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -69,7 +69,7 @@ jobs: If (($GitRequestEvent -eq "pull_request_target") -and (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened"))) { # Collect info from payload that we'll need to process the PR - $FileListUrl = "$($GitHubData.event.pull_request.url)/files" + $FileListUrl = "$($GitHubData.event.pull_request.url)/files?per_page=100" $PrSubmitter = $GitHubData.event.pull_request.user.login $PrSubmitterPermissionsUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$PrSubmitter/permission") $StatusUrl = $GitHubData.event.pull_request.statuses_url @@ -107,9 +107,14 @@ jobs: # Get the list of files modified in the PR. # Get the permissions of the PR submitter. - $FileList = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $FileListUrl -ErrorAction Stop + $FileListData = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $FileListUrl -FollowRelLink -MaximumFollowRelLink 50 -ErrorAction Stop $PrSubmitterPerms = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $PrSubmitterPermissionsUrl -ErrorAction Stop + $FileList = @() + + # Collapse pages into a single list if there are any and store in $FileList + ForEach ($Page in $FileListData) { $FileList += $Page } + $ProtectedFileFound = $False # Only process PRs that actually have changed files. @@ -275,4 +280,4 @@ jobs: } } - + \ No newline at end of file From 33977500ff76ec7c7e01818fd62bf6dc0e69e44c Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:00:45 -0700 Subject: [PATCH 06/92] add pr file count workflow --- .github/workflows/Shared-PrFileCount.yml | 663 +++++++++++++++++++++++ 1 file changed, 663 insertions(+) create mode 100644 .github/workflows/Shared-PrFileCount.yml diff --git a/.github/workflows/Shared-PrFileCount.yml b/.github/workflows/Shared-PrFileCount.yml new file mode 100644 index 00000000000..4fde7ce3880 --- /dev/null +++ b/.github/workflows/Shared-PrFileCount.yml @@ -0,0 +1,663 @@ +name: PR File Count + +permissions: + pull-requests: write + statuses: write + contents: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + secrets: + AccessToken: + required: true + +jobs: + build: + name: Run Script + runs-on: windows-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + + PrWarnLimit: 30 + PrBlockLimit: 100 + StatusCheckUrl: "https://review.docs.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main" + + run: | + + # Get payload data from GitHub. Azure puts data into a temp file in the file system and we get it by calling Get-Content. + # Payload variable name is set in the Integrate setting page on the Function App in Azure. + # Also get the GitHub Event header value. This is stored in environemntal variables populated by Azure when the web request is received. + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $GitRequestEvent = $GitHubData.event_name + + $AccessToken = $env:AccessToken + $PrWarnLimit = $env:PrWarnLimit + $PrBlockLimit = $env:PrBlockLimit + $StatusCheckUrl = $env:StatusCheckUrl + + # Create github HTTP authentication header + + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", "OfficeDocs") + + $StatusCheckName = "max/pr-file-count" + $Status = @{} + $Status.Add("context", $StatusCheckName) + $Status.Add("target_url", $StatusCheckUrl) + + $StatusUrl = $GitHubData.event.pull_request.statuses_url + $TargetBranch = $GitHubData.event.pull_request.base.ref + $DefaultBranch = $GitHubData.event.repository.default_branch + $PrivateRepo = $GitHubData.event.repository.private + $NumChangedFiles = $GitHubData.event.pull_request.changed_files + $CurrentLabelName = $GitHubData.event.label.name + $CommentsUrl = $GitHubData.event.pull_request.comments_url + $Labels = $GitHubData.event.pull_request.labels + + $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" + $WorkflowsRef = "workflows-prod" + + $WarningLabelName = "Merge large pull request" + $WarningLabelUrlName = $WarningLabelName.Replace(" ", "%20") + $WarningLabelColor = "46ce1c" + + $AdminLabelName = "Admin review" + $StatusCheckAllowedText = "Large PR allowed by {0}." + $StatusCheckBelowLimitText = "Number of changed files below limit." + $StatusCheckWarnFailureText = "Number of changed files exceeds warning limit: {0}." + $StatusCheckBlockFailureText = "Number of changed files exceeds blocking limit: {0}." + $StatusCheckPendingText = "Checking number of files in PR." + $StatusCheckErrorText = "Error processing PR. Contact marveldocs-admins." + $StatusCheckUnmonitoredBranchText = "$TargetBranch isn't a monitored branch." + + $RepoLabelsUrl = $GitHubData.event.repository.url + "/labels" + $MergeLabelUrl = $RepoLabelsUrl + "/" + $WarningLabelUrlName + + + ################## + Function Get-PrMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $PrMessageName + ) + + $PrMessageFile = "$WorkflowsResourcePath/PrFileCountCheck-$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 + + } + + ################## + + Function CheckLabel { + + Try { + + # Check to see if the label exists. If this request fails, the label doesn't exist so the catch statement will try to create it. + $LabelJson = Invoke-WebRequest -Headers $GitHubHeaders -Uri $MergeLabelUrl -UseBasicParsing -ErrorAction Stop + $LabelData = $LabelJson.content | ConvertFrom-Json + + $CurrentLabelColor = $LabelData.color + + # If label exists, check to see if the color is a nice pretty shade of green. If not, make it pretty. + If ($CurrentLabelColor -ne $WarningLabelColor) { + + $UpdateLabelBody = @{} + $UpdateLabelBody.add("color", $WarningLabelColor) + $UpdateLabelBodyJson = $UpdateLabelBody | ConvertTo-Json + + Try { + + $Result = Invoke-WebRequest -Headers $GitHubHeaders -Uri $MergeLabelUrl -Method POST -Body $UpdateLabelBodyJson -UseBasicParsing -ErrorAction Stop + + } Catch { + + # Not doing anything if the request fails - it's just color so it's not critical. + + } + + } + + } Catch { + + $NewLabelBody = @{} + $NewLabelBody.add("name", $WarningLabelName) + $NewLabelBody.add("color", $WarningLabelColor) + $NewLabelBodyJson = $NewLabelBody | ConvertTo-Json + + Try { + + $Result = Invoke-WebRequest -Headers $GitHubHeaders -Uri $RepoLabelsUrl -Method POST -Body $NewLabelBodyJson -UseBasicParsing -ErrorAction Stop + + } Catch { + + # Not doing anything if the request fails - creating the label automatically is desirable but not required. The label can be created manually if needed. + + } + + } + + + } + + ################## + + Function Remove-WarningLabel { + + Write-Host "Delete OK to merge label" + + If ($Labels.Length -gt 0) { + + ForEach ($Label in $Labels) { + + If ($Label.name -eq $WarningLabelName) { + + Try { + + $IssuesUrl = $GitHubData.event.pull_request.issue_url + $LabelUrl = "$IssuesUrl/labels/$WarningLabelUrlName" + + $Result = Invoke-WebRequest -UseBasicParsing -Uri $LabelUrl -Headers $GitHubHeaders -Method Delete -ErrorAction Stop + + Write-Host "Successfully removed warn label." + + $RemoveLabelSuccess = $True + + } Catch { + + Write-Host "ERROR: Failed to remove warn label." + + $RemoveLabelSuccess = $False + + + } + + } + + } + + } + + Return $RemoveLabelSuccess + + } + + ################## + + Function LabelAdded { + + $Labels = $GitHubData.event.pull_request.labels + $LabelSubmitter = $GitHubData.event.sender.login + $LabelSuccess = $False + + If ($NumChangedFiles -lt $PrBlockLimit) { + + If ($Labels.Length -gt 0) { + + ForEach ($Label in $Labels) { + + If ($Label.name -eq $WarningLabelName) { + + Write-Host "Merge label added. Allowing merge." + + $Status.state = "success" + $Status.description = $StatusCheckAllowedText -f $LabelSubmitter + + $StatusJson = $Status | ConvertTo-Json + $StatusJson + + Try { + + $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + $LabelSuccess = $True + + Break + + } Catch { + + $LabelSuccess = $False + + } + + + } Else { + + $LabelSuccess = $True + + } + + } + + } Else { + + $LabelSuccess = $True + + } + + } Else { + + If ($CurrentLabelName -eq $WarningLabelName) { + + Write-Host "Number of changed files $NumChangedFiles is greater than BLOCK limit $PrBlockLimit. Removing merge label." + + Set-PrConversationMessage -Message "The number of files in this PR exceeds the block limit of $PrBlockLimit files. The **$WarningLabelName** label has been removed and can't be re-added while the total number of files exceeds the block limit. `n`nReduce the number of files in this PR to below $PrBlockLimit files or request an admin review to merge this PR." + $LabelSuccess = Remove-WarningLabel + + $Status.state = "error" + $Status.description = $StatusCheckBlockFailureText -f $PrBlockLimit + + $StatusJson = $Status | ConvertTo-Json + $StatusJson + + } Else { + + $LabelSuccess = $True + + } + } + + $PropertyList = @{ + + status = $LabelSuccess + + } + + $ReturnObject = New-Object -TypeName psobject -Property $PropertyList + + $ReturnObject + + } + + ################## + + Function LabelRemoved { + + $NumChangedFiles = $GitHubData.event.pull_request.changed_files + $Labels = $GitHubData.event.pull_request.labels + $MergeLabelExists = $False + $LabelRemovedSuccess = $False + + If ($Labels.Length -gt 0) { + + ForEach ($Label in $Labels) { + + If ($Label.name -eq $WarningLabelName) { + + $MergeLabelExists = $True + Break + + + } + + } + + } Else { + + $LabelRemovedSuccess = $True + + } + + If (!$MergeLabelExists) { + + If ($NumChangedFiles -gt $PrBlockLimit) { + + Write-Host "PR file count $NumChangedFiles is greater than PR change limit $PrBlockLimit. Setting status to 'failure'." + + $Status.state = "failure" + $Status.description = $StatusCheckBlockFailureText -f $PrBlockLimit + + } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { + + Write-Host "PR file count $NumChangedFiles is greater than PR change limit $PrWarnLimit. Setting status to 'failure'." + + $Status.state = "failure" + $Status.description = $StatusCheckWarnFailureText -f $PrWarnLimit + + + } Else { + + Write-Host "PR file count $NumChangedFiles is less than PR change limit $PrWarnLimit. Setting status to 'success'." + + $Status.state = "success" + $Status.description = $StatusCheckBelowLimitText + + + } + + $StatusJson = $Status | ConvertTo-Json + $StatusJson + + Try { + + Write-Host "Submitting $($Status.state) state to GitHub" + + $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + + Write-Host $Output + + $LabelRemovedSuccess = $True + + } Catch { + + Write-Host "Failed to submit POST to GitHub. `nStatus URL: $StatusUrl `nBody: $StatusJson" + + $LabelRemovedSuccess = $False + + } + + } Else { + + $LabelRemovedSuccess = $True + + } + + $PropertyList = @{ + + status = $LabelRemovedSuccess + + } + + $ReturnObject = New-Object -TypeName psobject -Property $PropertyList + + $ReturnObject + + } + + ################### + + Function Set-PrConversationMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $Message + ) + + $BodyHash = @{} + $BodyHash.body = $Message + $BodyJson = $BodyHash | ConvertTo-Json + $BodyJson + + 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 + + $PostCommentSuccess = $True + + } Catch { + + Write-Host "Failed to post message to PR conversation. $($Error[0])" + $PostCommentSuccess = $False + + } + + } Else { + + Write-Host "Message is null or empty. Not posting to PR conversation." + $PostCommentSuccess = $False + + } + + Return $PostCommentSuccess + + + } + + ################### + + Function ProcessPr { + + # Set status check state fields to show the PR is being processed. The entire process usually only takes a couple seconds but + # can take longer. GitHub will show PR status check is processing. + # Convert hash table to JSON. + $Status.Add("state","pending") + $Status.Add("description", $StatusCheckPendingText) + $StatusJson = $Status | ConvertTo-Json + + $ProcessPrSuccess = $False + + # Sent POST request to GitHub to set the status check. Subsequent POST and GET requests use similar parameters - + # GitHubHeaders - GitHub authentication token + # StatusUrl - REST API endpoint to GET or POST data from/to + # StatusJson - Payload to send to GitHub for POST requests + + Try { + + Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + + $ProcessPrSuccess = $True + + } Catch { + + $ProcessPrSuccess = $False + + } + + # Short delay so check doesn't finish before others start, which can cause GitHub UI to display confusing behavior. + Start-Sleep 3 + + # Setting state to 'error' in case something breaks. + $Status.state = "error" + $Status.description = $StatusCheckErrorText + + Write-Host "Number of changed files in PR: $NumChangedFiles" + + If ($ProcessPrSuccess) { + + If ($NumChangedFiles -gt $PrBlockLimit) { + + Write-Host "Number of files in PR, $NumChangedFiles, exceeds the BLOCK file limit of $PrBlockLimit" + + $Status.state = "error" + $Status.description = $StatusCheckBlockFailureText -f $PrBlockLimit + + $ProcessPrSuccess = Remove-WarningLabel + + If ($PrivateRepo) { + + $PrPrivateBlockMessage = Get-PrMessage -PrMessageName "PrivateBlockMessage" + $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPrivateBlockMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName, $AdminLabelName) + + } Else { + + $PrPublicBlockMessage = Get-PrMessage -PrMessageName "PublicBlockMessage" + $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPublicBlockMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) + + } + + } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { + + Write-Host "Number of files in PR, $NumChangedFiles, exceeds the WARN file limit of $PrWarnLimit" + + + If ($PrivateRepo) { + + $PrPrivateWarningMessage = Get-PrMessage -PrMessageName "PrivateWarningMessage" + $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPrivateWarningMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) + + } Else { + + $PrPublicWarningMessage = Get-PrMessage -PrMessageName "PublicWarningMessage" + $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPublicWarningMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) + + } + + If ($ProcessPrSuccess) { + + $Status.state = "failure" + $Status.description = $StatusCheckWarnFailureText -f $PrWarnLimit + + } Else { + + $Status.state = "error" + $Status.description = $StatusCheckErrorText + + } + + CheckLabel + + If (($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened")) { + + $ProcessPrSuccess = Remove-WarningLabel + + } + + + } Else { + + $Status.state = "success" + $Status.description = $StatusCheckBelowLimitText + + $ProcessPrSuccess = $True + + } + + } + + $StatusJson = $Status | ConvertTo-Json + $StatusJson + + Try { + + $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + + $ProcessPrSuccess = $True + + } Catch { + + $ProcessPrSuccess = $False + + } + + $PropertyList = @{ + + status = $ProcessPrSuccess + + } + + $ReturnObject = New-Object -TypeName psobject -Property $PropertyList + + $ReturnObject + + } + + + ################### + ################### + # Main + + $RuntimeInfo = "Repo: $($GitHubData.event.repository.name)" + $RuntimeInfo = $RuntimeInfo + " Sender: $($GitHubData.event.sender.login)" + $RuntimeInfo = $RuntimeInfo + " Request type: $GitRequestEvent" + $RuntimeInfo = $RuntimeInfo + " GitHub action: $($GitHubData.event.action)" + $RuntimeInfo = $RuntimeInfo + " Request Id: $RequestId" + + Write-Host $RuntimeInfo + + $PropertyList = @{ + + status = $True + + } + + $MainSuccess = New-Object -TypeName psobject -Property $PropertyList + + # Only process event types of 'pull_request' in $DefaultBranch + If ($GitRequestEvent -eq "pull_request_target") { + + Write-Host "Pull request target branch: $TargetBranch. Default branch $DefaultBranch." + + If ($TargetBranch -eq $DefaultBranch) { + + Write-Host "Pull request in monitored branch" + + If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened")) { + + Write-Host "Opened, reopened, or synchronized. Process PR." + + $MainSuccess = ProcessPr + + } ElseIf ($GitHubData.event.action -eq "labeled") { + + Write-Host "Labeled" + + $MainSuccess = LabelAdded + + } ElseIf ($GitHubData.event.action -eq "unlabeled") { + + Write-Host "Unlabeled" + + $MainSuccess = LabelRemoved + + } + + } Else { + + Write-Host "Pull request in unmonitored branch" + + $Status.Add("state", "success") + $Status.Add("description", $StatusCheckUnmonitoredBranchText) + + $StatusJson = $Status | ConvertTo-Json + $StatusJson + + Try { + + $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + + $MainSuccess.status = $True + + } Catch { + + $MainSuccess.status = $False + + } + + } + + } + + + Write-Host "Execution status $($MainSuccess.status)" + + If (!$($MainSuccess.status)) { + + Write-Host "ERROR" + + $MailBody = "

An error occurred running script PrFileCountCheck. The following are the details:

" + $MailBody = $MailBody + "

$RuntimeInfo

" + $MailBody = $MailBody + "

$Error

" + + Write-Host $MailBody + + } + From 9940230b5ddb68af2b6a6df68e5e926a65a38368 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:13:37 -0700 Subject: [PATCH 07/92] Create LiveMergeCheck-LiveMergeMessage.md --- .../LiveMergeCheck-LiveMergeMessage.md | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/resources/LiveMergeCheck-LiveMergeMessage.md diff --git a/.github/workflows/resources/LiveMergeCheck-LiveMergeMessage.md b/.github/workflows/resources/LiveMergeCheck-LiveMergeMessage.md new file mode 100644 index 00000000000..938729f2a90 --- /dev/null +++ b/.github/workflows/resources/LiveMergeCheck-LiveMergeMessage.md @@ -0,0 +1,22 @@ +## ![STOP](https://docs.microsoft.com/office/media/internal/prfilecountcheck-stop-sign.png) Invalid base branch - PR blocked + +The target (base) branch - **{0}** - you're trying to merge this pull request into only accepts pull requests from the **{1}** branch. + +If you're trying to publish changes you've made to the live site, you must first merge this pull request into the **{1}** branch. After this pull request is merged into the {1} branch, your changes will be published live at the next scheduled publish time. For information about the Marvel publishing process, see [Marvel PubOps publishing process](https://review.docs.microsoft.com/office-authoring-guide/pubops-publishing-process?branch=main). + +To change the target branch from {0} to {1}, use the following steps: + +1. Select the **Edit** button next to the pull request title. + + ![STOP](https://docs.microsoft.com/Office/media/internal/fa-prcriteriacheck-livemergecheck-fixbranch-step1.png) + +2. Select **base:{0}**. +3. In the list that appears, select **{1}**. + + ![STOP](https://docs.microsoft.com/Office/media/internal/fa-prcriteriacheck-livemergecheck-fixbranch-steps2-3.png) + +4. Select **Change base**. + + ![STOP](https://docs.microsoft.com/Office/media/internal/fa-prcriteriacheck-livemergecheck-fixbranch-step4.png) + + **NOTE** Clicking **Change base** is safe. The message is specific to more complex branching methods that aren't typically used in Marvel repos. Even with those methods, this message only applies to how commits are tracked in the repo's history. There is no risk of data loss. From b469c2fbdd47bba6fdc00915df1621acf81e4acb Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 18 Jul 2024 13:44:50 -0700 Subject: [PATCH 08/92] add live merge check workflow --- .github/workflows/Shared-LiveMergeCheck.yml | 239 ++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 .github/workflows/Shared-LiveMergeCheck.yml diff --git a/.github/workflows/Shared-LiveMergeCheck.yml b/.github/workflows/Shared-LiveMergeCheck.yml new file mode 100644 index 00000000000..9229100e9f7 --- /dev/null +++ b/.github/workflows/Shared-LiveMergeCheck.yml @@ -0,0 +1,239 @@ +name: Live Merge Check + +permissions: + pull-requests: write + statuses: write + contents: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + secrets: + AccessToken: + required: true + +jobs: + build: + name: Run Script + runs-on: windows-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + StatusCheckUrl: "https://review.docs.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main" + + + run: | + + # Get payload data and event from GitHub + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $GitRequestEvent = $GitHubData.event_name + $StatusCheckHelpUrl = $env:StatusCheckUrl + + $PublishBranch = "live" + $StatusCheckName = "max/live-compare-merge" + + # Retrieve GitHub token, create github HTTP authentication header + $AccessToken = $env:AccessToken + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", "officedocs") + + $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" + $WorkflowsRef = "workflows-prod" + + $StatusUrl = $GitHubData.event.pull_request.statuses_url + $RequiredRepo = $GitHubData.event.repository.full_name + $TargetBranch = $GitHubData.event.pull_request.base.ref + $OriginBranch = $GitHubData.event.pull_request.head.ref + $OriginRepo = $GitHubData.event.pull_request.head.repo.full_name + $DefaultBranch = $GitHubData.event.repository.default_branch + + $StatusCheckPendingText = "Checking base and compare branches." + $StatusCheckUnmonitoredBranchText = "OK to merge into $TargetBranch." + $AllowedBranchMergeText = "Compare branch $OriginBranch can merge into $PublishBranch." + $DisallowedBranchMergeText = "Only $RequiredRepo/$DefaultBranch can merge into $PublishBranch." + + ################## + 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-Status takes in the status check result and message values and creates the JSON body to send to GitHub. + Function Set-Status { + + [cmdletbinding()] + Param ( + $StatusResult, + $StatusMessage + ) + + # Build hash table that contains status check fields to be sent to GitHub. Context and Target_Url always stay the same. + # State and Description change depending on result of checks below. + $Status = @{} + $Status.Add("context", $StatusCheckName) + $Status.Add("target_url", $StatusCheckHelpUrl) + $Status.state = $StatusResult + $Status.description = $StatusMessage + $StatusJson = $Status | ConvertTo-Json + + Return $StatusJson + + } + + ############# + # Send-Status takes the JSON body created by Set-Status and submits it to GitHub. + Function Send-Status { + + [cmdletbinding()] + Param ( + $Url, + $Body + ) + + Try { + + # Send status to GitHub + $Result = Invoke-WebRequest -Headers $GitHubHeaders -Uri $Url -UseBasicParsing -Method POST -Body $Body -ErrorAction Stop + + } Catch { + + Write-Host "ERROR: Failed to submit status to GitHub. Error: $($error[0].Exception.Message). Request ID: $RequestId." + + } + } + + ################### + + Function Set-PrConversationMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $Message + ) + + $CommentsUrl = $GitHubData.event.pull_request.comments_url + + $BodyHash = @{} + $BodyHash.body = $Message + $BodyJson = $BodyHash | ConvertTo-Json + + Try { + + $Result = Invoke-WebRequest -UseBasicParsing -Uri $CommentsUrl -Body $BodyJson -Headers $GitHubHeaders -Method POST -ErrorAction Stop + + $PostCommentSuccess = $True + + } Catch { + + $PostCommentSuccess = $False + + Write-Host "ERROR: Failed to submit message to PR conversation. Error: $($error[0].Exception.Message). Request ID: $RequestId." + + + + } + + Return $PostCommentSuccess + + } + + ################### + ################### + # Main + + $RuntimeInfo = "Repo: $($GitHubData.event.repository.name)" + $RuntimeInfo = $RuntimeInfo + " PR origin repo: $OriginRepo" + $RuntimeInfo = $RuntimeInfo + " Sender: $($GitHubData.event.sender.login)" + $RuntimeInfo = $RuntimeInfo + " Request type: $GitRequestEvent" + $RuntimeInfo = $RuntimeInfo + " GitHub action: $($GitHubData.event.action)" + $RuntimeInfo = $RuntimeInfo + " Request Id: $RequestId" + + Write-Host $RuntimeInfo + + # Only process event types of 'pull_request' + If ($GitRequestEvent -eq "pull_request_target") { + + Write-Host "Request type is pull_request. Processing request. Request ID: $RequestId." + + If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened") -or ($GitHubData.event.action -eq "edited")) { + + Write-Host "Setting pending status. Request ID: $RequestId." + + # Show the status of the check as "pending" in the PR on GitHub + # This is being set only if $TargetBranch equals $PublishBranch because we only want to run this check for PRs that originally targetted + # $PublishBranch. Trying to avoid cluttering the status check on PRs submitted to anything other than $PublishBranch. + $StatusOutput = Set-Status -StatusResult "pending" -StatusMessage $StatusCheckPendingText + Send-Status -Url $StatusUrl -Body $StatusOutput + + If ($TargetBranch -eq $PublishBranch) { + + Write-Host "Target branch $TargetBranch matches publish branch $PublishBranch. Request ID: $RequestId." + + If (($OriginBranch -eq $DefaultBranch) -and ($OriginRepo -eq $RequiredRepo)) { + + Write-Host "Origin branch $OriginBranch is allowed to merge to $PublishBranch. Request ID: $RequestId." + + $StatusOutput = Set-Status -StatusResult "success" -StatusMessage $AllowedBranchMergeText + + + } Else { + + Write-Host "Origin branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch. Request ID: $RequestId." + + $StatusOutput = Set-Status -StatusResult "error" -StatusMessage $DisallowedBranchMergeText + + $LiveMergeMessage = Get-PrMessage -PrMessageName "LiveMergeCheck-LiveMergeMessage" + + Set-PrConversationMessage -Message $($LiveMergeMessage -F $TargetBranch, $DefaultBranch) + + } # OriginBranch + + } Else { + + Write-Host "Target branch is not $PublishBranch. Setting status check to 'success'." + + $StatusOutput = Set-Status -StatusResult "success" -StatusMessage $StatusCheckUnmonitoredBranchText + + + } # Target banch + + Send-Status -Url $StatusUrl -Body $StatusOutput + + } # PR state + + } # Is PR check + + From 1526761c6754c9d28b025aa82c2174e43cff3c90 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:55:45 -0700 Subject: [PATCH 09/92] Switch to exit code instead of statuses API --- .github/workflows/Shared-LiveMergeCheck.yml | 83 +----- .github/workflows/Shared-PrFileCount.yml | 310 +++----------------- .github/workflows/Shared-ProtectedFiles.yml | 185 ++++-------- 3 files changed, 111 insertions(+), 467 deletions(-) diff --git a/.github/workflows/Shared-LiveMergeCheck.yml b/.github/workflows/Shared-LiveMergeCheck.yml index 9229100e9f7..d3357062a49 100644 --- a/.github/workflows/Shared-LiveMergeCheck.yml +++ b/.github/workflows/Shared-LiveMergeCheck.yml @@ -30,6 +30,9 @@ jobs: run: | + # This script outputs nothing if all checks pass. If a check fails, an exception is thrown which results in an + # exit code of 1 being returned to GitHub. An exit code of 1 fails the status check. + # Get payload data and event from GitHub $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $GitRequestEvent = $GitHubData.event_name @@ -89,51 +92,6 @@ jobs: } - ############# - # Set-Status takes in the status check result and message values and creates the JSON body to send to GitHub. - Function Set-Status { - - [cmdletbinding()] - Param ( - $StatusResult, - $StatusMessage - ) - - # Build hash table that contains status check fields to be sent to GitHub. Context and Target_Url always stay the same. - # State and Description change depending on result of checks below. - $Status = @{} - $Status.Add("context", $StatusCheckName) - $Status.Add("target_url", $StatusCheckHelpUrl) - $Status.state = $StatusResult - $Status.description = $StatusMessage - $StatusJson = $Status | ConvertTo-Json - - Return $StatusJson - - } - - ############# - # Send-Status takes the JSON body created by Set-Status and submits it to GitHub. - Function Send-Status { - - [cmdletbinding()] - Param ( - $Url, - $Body - ) - - Try { - - # Send status to GitHub - $Result = Invoke-WebRequest -Headers $GitHubHeaders -Uri $Url -UseBasicParsing -Method POST -Body $Body -ErrorAction Stop - - } Catch { - - Write-Host "ERROR: Failed to submit status to GitHub. Error: $($error[0].Exception.Message). Request ID: $RequestId." - - } - } - ################### Function Set-PrConversationMessage { @@ -160,7 +118,7 @@ jobs: $PostCommentSuccess = $False - Write-Host "ERROR: Failed to submit message to PR conversation. Error: $($error[0].Exception.Message). Request ID: $RequestId." + Write-Host "ERROR: Failed to submit message to PR conversation. Error: $($error[0].Exception.Message)." @@ -179,61 +137,44 @@ jobs: $RuntimeInfo = $RuntimeInfo + " Sender: $($GitHubData.event.sender.login)" $RuntimeInfo = $RuntimeInfo + " Request type: $GitRequestEvent" $RuntimeInfo = $RuntimeInfo + " GitHub action: $($GitHubData.event.action)" - $RuntimeInfo = $RuntimeInfo + " Request Id: $RequestId" Write-Host $RuntimeInfo # Only process event types of 'pull_request' If ($GitRequestEvent -eq "pull_request_target") { - Write-Host "Request type is pull_request. Processing request. Request ID: $RequestId." + Write-Host "Request type is pull_request. Processing request." If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened") -or ($GitHubData.event.action -eq "edited")) { - Write-Host "Setting pending status. Request ID: $RequestId." - - # Show the status of the check as "pending" in the PR on GitHub - # This is being set only if $TargetBranch equals $PublishBranch because we only want to run this check for PRs that originally targetted - # $PublishBranch. Trying to avoid cluttering the status check on PRs submitted to anything other than $PublishBranch. - $StatusOutput = Set-Status -StatusResult "pending" -StatusMessage $StatusCheckPendingText - Send-Status -Url $StatusUrl -Body $StatusOutput - + # Only run checks if the base branch matches the branch defined in $PublishBranch. If ($TargetBranch -eq $PublishBranch) { - Write-Host "Target branch $TargetBranch matches publish branch $PublishBranch. Request ID: $RequestId." + Write-Host "Target branch $TargetBranch matches publish branch $PublishBranch." If (($OriginBranch -eq $DefaultBranch) -and ($OriginRepo -eq $RequiredRepo)) { - Write-Host "Origin branch $OriginBranch is allowed to merge to $PublishBranch. Request ID: $RequestId." - - $StatusOutput = Set-Status -StatusResult "success" -StatusMessage $AllowedBranchMergeText - + Write-Host "Origin branch $OriginBranch is allowed to merge to $PublishBranch." } Else { - Write-Host "Origin branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch. Request ID: $RequestId." - - $StatusOutput = Set-Status -StatusResult "error" -StatusMessage $DisallowedBranchMergeText - $LiveMergeMessage = Get-PrMessage -PrMessageName "LiveMergeCheck-LiveMergeMessage" Set-PrConversationMessage -Message $($LiveMergeMessage -F $TargetBranch, $DefaultBranch) + Throw "Origin branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch." + } # OriginBranch } Else { - Write-Host "Target branch is not $PublishBranch. Setting status check to 'success'." - - $StatusOutput = Set-Status -StatusResult "success" -StatusMessage $StatusCheckUnmonitoredBranchText - + Write-Host "Target branch is not $PublishBranch." } # Target banch - Send-Status -Url $StatusUrl -Body $StatusOutput - } # PR state } # Is PR check + diff --git a/.github/workflows/Shared-PrFileCount.yml b/.github/workflows/Shared-PrFileCount.yml index 4fde7ce3880..624ebb65f45 100644 --- a/.github/workflows/Shared-PrFileCount.yml +++ b/.github/workflows/Shared-PrFileCount.yml @@ -49,12 +49,6 @@ jobs: $GitHubHeaders.Add("Authorization","token $($AccessToken)") $GitHubHeaders.Add("User-Agent", "OfficeDocs") - $StatusCheckName = "max/pr-file-count" - $Status = @{} - $Status.Add("context", $StatusCheckName) - $Status.Add("target_url", $StatusCheckUrl) - - $StatusUrl = $GitHubData.event.pull_request.statuses_url $TargetBranch = $GitHubData.event.pull_request.base.ref $DefaultBranch = $GitHubData.event.repository.default_branch $PrivateRepo = $GitHubData.event.repository.private @@ -134,11 +128,12 @@ jobs: Try { + Write-Host "Correcting label color from $CurrentLabelColor to $WarningLabelColor." $Result = Invoke-WebRequest -Headers $GitHubHeaders -Uri $MergeLabelUrl -Method POST -Body $UpdateLabelBodyJson -UseBasicParsing -ErrorAction Stop } Catch { - # Not doing anything if the request fails - it's just color so it's not critical. + Write-Host "Failed to change label color." } @@ -153,11 +148,12 @@ jobs: Try { + Write-Host "Adding warning label `"$WarningLabelName`" with color $WarningLabelColor." $Result = Invoke-WebRequest -Headers $GitHubHeaders -Uri $RepoLabelsUrl -Method POST -Body $NewLabelBodyJson -UseBasicParsing -ErrorAction Stop } Catch { - # Not doing anything if the request fails - creating the label automatically is desirable but not required. The label can be created manually if needed. + Write-Host "ERROR: Failed to create warning label." } @@ -170,7 +166,7 @@ jobs: Function Remove-WarningLabel { - Write-Host "Delete OK to merge label" + Write-Host "Remove `"$WarningLabelName`" label from PR." If ($Labels.Length -gt 0) { @@ -185,17 +181,12 @@ jobs: $Result = Invoke-WebRequest -UseBasicParsing -Uri $LabelUrl -Headers $GitHubHeaders -Method Delete -ErrorAction Stop - Write-Host "Successfully removed warn label." - - $RemoveLabelSuccess = $True - + Write-Host "Successfully removed `"$WarningLabelName`" label." + } Catch { - Write-Host "ERROR: Failed to remove warn label." + Write-Host "ERROR: Failed to remove `"$WarningLabelName`" label." - $RemoveLabelSuccess = $False - - } } @@ -204,8 +195,6 @@ jobs: } - Return $RemoveLabelSuccess - } ################## @@ -214,7 +203,6 @@ jobs: $Labels = $GitHubData.event.pull_request.labels $LabelSubmitter = $GitHubData.event.sender.login - $LabelSuccess = $False If ($NumChangedFiles -lt $PrBlockLimit) { @@ -224,74 +212,29 @@ jobs: If ($Label.name -eq $WarningLabelName) { - Write-Host "Merge label added. Allowing merge." - - $Status.state = "success" - $Status.description = $StatusCheckAllowedText -f $LabelSubmitter - - $StatusJson = $Status | ConvertTo-Json - $StatusJson - - Try { - - $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop - $LabelSuccess = $True - - Break - - } Catch { - - $LabelSuccess = $False - - } - - - } Else { - - $LabelSuccess = $True + Write-Host "`"$WarningLabelName`" label added. Allowing merge." } } - } Else { - - $LabelSuccess = $True - - } + } } Else { If ($CurrentLabelName -eq $WarningLabelName) { - Write-Host "Number of changed files $NumChangedFiles is greater than BLOCK limit $PrBlockLimit. Removing merge label." + Write-Host "Number of changed files $NumChangedFiles is greater than BLOCK limit $PrBlockLimit. Removing `"$WarningLabelName`" label and posting message to PR conversation." Set-PrConversationMessage -Message "The number of files in this PR exceeds the block limit of $PrBlockLimit files. The **$WarningLabelName** label has been removed and can't be re-added while the total number of files exceeds the block limit. `n`nReduce the number of files in this PR to below $PrBlockLimit files or request an admin review to merge this PR." - $LabelSuccess = Remove-WarningLabel - - $Status.state = "error" - $Status.description = $StatusCheckBlockFailureText -f $PrBlockLimit + + Remove-WarningLabel - $StatusJson = $Status | ConvertTo-Json - $StatusJson + Throw "Number of changed files $NumChangedFiles is greater than BLOCK limit $PrBlockLimit. " - } Else { - - $LabelSuccess = $True - - } - } - - $PropertyList = @{ - - status = $LabelSuccess - + } } - $ReturnObject = New-Object -TypeName psobject -Property $PropertyList - - $ReturnObject - } ################## @@ -301,7 +244,6 @@ jobs: $NumChangedFiles = $GitHubData.event.pull_request.changed_files $Labels = $GitHubData.event.pull_request.labels $MergeLabelExists = $False - $LabelRemovedSuccess = $False If ($Labels.Length -gt 0) { @@ -317,75 +259,26 @@ jobs: } - } Else { - - $LabelRemovedSuccess = $True - - } + } If (!$MergeLabelExists) { If ($NumChangedFiles -gt $PrBlockLimit) { - Write-Host "PR file count $NumChangedFiles is greater than PR change limit $PrBlockLimit. Setting status to 'failure'." + Throw "PR file count $NumChangedFiles is greater than PR change limit $PrBlockLimit." - $Status.state = "failure" - $Status.description = $StatusCheckBlockFailureText -f $PrBlockLimit } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { - Write-Host "PR file count $NumChangedFiles is greater than PR change limit $PrWarnLimit. Setting status to 'failure'." - - $Status.state = "failure" - $Status.description = $StatusCheckWarnFailureText -f $PrWarnLimit - + Throw "PR file count $NumChangedFiles is greater than PR change limit $PrWarnLimit." } Else { - Write-Host "PR file count $NumChangedFiles is less than PR change limit $PrWarnLimit. Setting status to 'success'." - - $Status.state = "success" - $Status.description = $StatusCheckBelowLimitText - - - } - - $StatusJson = $Status | ConvertTo-Json - $StatusJson - - Try { - - Write-Host "Submitting $($Status.state) state to GitHub" - - $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop - - Write-Host $Output - - $LabelRemovedSuccess = $True - - } Catch { - - Write-Host "Failed to submit POST to GitHub. `nStatus URL: $StatusUrl `nBody: $StatusJson" - - $LabelRemovedSuccess = $False + Write-Host "PR file count $NumChangedFiles is less than PR change limit $PrWarnLimit." } - } Else { - - $LabelRemovedSuccess = $True - - } - - $PropertyList = @{ - - status = $LabelRemovedSuccess - - } - - $ReturnObject = New-Object -TypeName psobject -Property $PropertyList - - $ReturnObject + } } @@ -402,7 +295,6 @@ jobs: $BodyHash = @{} $BodyHash.body = $Message $BodyJson = $BodyHash | ConvertTo-Json - $BodyJson If (($Message -ne $Null) -and ($Message -ne "")) { @@ -411,23 +303,19 @@ jobs: Write-Host "Posting message to PR conversation to $CommentsUrl." $Result = Invoke-WebRequest -UseBasicParsing -Uri $CommentsUrl -Body $BodyJson -Headers $GitHubHeaders -Method POST -ErrorAction Stop - $PostCommentSuccess = $True - } Catch { - Write-Host "Failed to post message to PR conversation. $($Error[0])" - $PostCommentSuccess = $False + 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." - $PostCommentSuccess = $False } - Return $PostCommentSuccess + } @@ -436,137 +324,57 @@ jobs: Function ProcessPr { - # Set status check state fields to show the PR is being processed. The entire process usually only takes a couple seconds but - # can take longer. GitHub will show PR status check is processing. - # Convert hash table to JSON. - $Status.Add("state","pending") - $Status.Add("description", $StatusCheckPendingText) - $StatusJson = $Status | ConvertTo-Json - - $ProcessPrSuccess = $False - - # Sent POST request to GitHub to set the status check. Subsequent POST and GET requests use similar parameters - - # GitHubHeaders - GitHub authentication token - # StatusUrl - REST API endpoint to GET or POST data from/to - # StatusJson - Payload to send to GitHub for POST requests - - Try { - - Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop - - $ProcessPrSuccess = $True - - } Catch { - - $ProcessPrSuccess = $False - - } - # Short delay so check doesn't finish before others start, which can cause GitHub UI to display confusing behavior. Start-Sleep 3 - # Setting state to 'error' in case something breaks. - $Status.state = "error" - $Status.description = $StatusCheckErrorText - Write-Host "Number of changed files in PR: $NumChangedFiles" - If ($ProcessPrSuccess) { - If ($NumChangedFiles -gt $PrBlockLimit) { - Write-Host "Number of files in PR, $NumChangedFiles, exceeds the BLOCK file limit of $PrBlockLimit" - - $Status.state = "error" - $Status.description = $StatusCheckBlockFailureText -f $PrBlockLimit + CheckLabel - $ProcessPrSuccess = Remove-WarningLabel + Remove-WarningLabel If ($PrivateRepo) { $PrPrivateBlockMessage = Get-PrMessage -PrMessageName "PrivateBlockMessage" - $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPrivateBlockMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName, $AdminLabelName) + Set-PrConversationMessage -Message $($PrPrivateBlockMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName, $AdminLabelName) } Else { $PrPublicBlockMessage = Get-PrMessage -PrMessageName "PublicBlockMessage" - $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPublicBlockMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) + Set-PrConversationMessage -Message $($PrPublicBlockMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) } - } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { + Throw "Number of files in PR, $NumChangedFiles, exceeds the BLOCK file limit of $PrBlockLimit." - Write-Host "Number of files in PR, $NumChangedFiles, exceeds the WARN file limit of $PrWarnLimit" + } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { + + CheckLabel If ($PrivateRepo) { $PrPrivateWarningMessage = Get-PrMessage -PrMessageName "PrivateWarningMessage" - $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPrivateWarningMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) + Set-PrConversationMessage -Message $($PrPrivateWarningMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) } Else { $PrPublicWarningMessage = Get-PrMessage -PrMessageName "PublicWarningMessage" - $ProcessPrSuccess = Set-PrConversationMessage -Message $($PrPublicWarningMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) - - } - - If ($ProcessPrSuccess) { - - $Status.state = "failure" - $Status.description = $StatusCheckWarnFailureText -f $PrWarnLimit - - } Else { - - $Status.state = "error" - $Status.description = $StatusCheckErrorText + Set-PrConversationMessage -Message $($PrPublicWarningMessage -F $NumChangedFiles, $PrWarnLimit, $PrBlockLimit, $WarningLabelName) } - CheckLabel - If (($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened")) { - $ProcessPrSuccess = Remove-WarningLabel + Remove-WarningLabel } - - - } Else { - - $Status.state = "success" - $Status.description = $StatusCheckBelowLimitText - - $ProcessPrSuccess = $True - - } - - } - - $StatusJson = $Status | ConvertTo-Json - $StatusJson - - Try { - $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop + Throw "Number of files in PR, $NumChangedFiles, exceeds the WARN file limit of $PrWarnLimit." - $ProcessPrSuccess = $True - - } Catch { - - $ProcessPrSuccess = $False - - } - - $PropertyList = @{ - - status = $ProcessPrSuccess - - } - - $ReturnObject = New-Object -TypeName psobject -Property $PropertyList - - $ReturnObject + } } @@ -579,17 +387,9 @@ jobs: $RuntimeInfo = $RuntimeInfo + " Sender: $($GitHubData.event.sender.login)" $RuntimeInfo = $RuntimeInfo + " Request type: $GitRequestEvent" $RuntimeInfo = $RuntimeInfo + " GitHub action: $($GitHubData.event.action)" - $RuntimeInfo = $RuntimeInfo + " Request Id: $RequestId" Write-Host $RuntimeInfo - $PropertyList = @{ - - status = $True - - } - - $MainSuccess = New-Object -TypeName psobject -Property $PropertyList # Only process event types of 'pull_request' in $DefaultBranch If ($GitRequestEvent -eq "pull_request_target") { @@ -604,19 +404,19 @@ jobs: Write-Host "Opened, reopened, or synchronized. Process PR." - $MainSuccess = ProcessPr + ProcessPr } ElseIf ($GitHubData.event.action -eq "labeled") { Write-Host "Labeled" - $MainSuccess = LabelAdded + LabelAdded } ElseIf ($GitHubData.event.action -eq "unlabeled") { Write-Host "Unlabeled" - $MainSuccess = LabelRemoved + LabelRemoved } @@ -624,40 +424,6 @@ jobs: Write-Host "Pull request in unmonitored branch" - $Status.Add("state", "success") - $Status.Add("description", $StatusCheckUnmonitoredBranchText) - - $StatusJson = $Status | ConvertTo-Json - $StatusJson - - Try { - - $Output = Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop - - $MainSuccess.status = $True - - } Catch { - - $MainSuccess.status = $False - - } - } } - - - Write-Host "Execution status $($MainSuccess.status)" - - If (!$($MainSuccess.status)) { - - Write-Host "ERROR" - - $MailBody = "

An error occurred running script PrFileCountCheck. The following are the details:

" - $MailBody = $MailBody + "

$RuntimeInfo

" - $MailBody = $MailBody + "

$Error

" - - Write-Host $MailBody - - } - diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index d9dc6ceff63..c3dc840a7a6 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -32,6 +32,9 @@ jobs: AccessToken: ${{ secrets.AccessToken }} run: | + # This script outputs nothing if all checks pass. If a check fails, an exception is thrown which results in an + # exit code of 1 being returned to GitHub. An exit code of 1 fails the status check. + # Get GitHub data $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $AccessToken = $env:AccessToken @@ -48,12 +51,6 @@ jobs: $GitHubHeaders.Add("Authorization","token $($AccessToken)") $GitHubHeaders.Add("User-Agent", $UserAgent) - # Start building hash table that contains status check fields to be sent to GitHub. Context and Target_Url always stay the same. - # State and Description change depending on result of checks below. - $Status = @{} - $Status.Add("context", "max/protected-file-check") - $Status.Add("target_url", $HelpUrl) - $RepoName = $GitHubData.event.repository.name $FatalError = $False @@ -63,7 +60,6 @@ jobs: Write-Host "Request type: $GitRequestEvent" Write-Host "GitHub action: $($GitHubData.event.action)" Write-Host "PR URL: $($GitHubData.event.pull_request.url)/files" - Write-Host "Request ID: $RequestId" # Only process event types of 'pull_request_target' that are either 'opened' (PR created) or 'synchronized' (PR commits updated) If (($GitRequestEvent -eq "pull_request_target") -and (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened"))) { @@ -79,183 +75,124 @@ jobs: Write-Host "Processing PR. Default branch: $DefaultBranch. Publish branch: $PublishBranch." - # Set status check state fields to show the PR is being processed. The entire process usually only takes a couple seconds but - # can take longer. GitHub will show PR status check is processing. - # Convert hash table to JSON. - $Status.Add("state","pending") - $Status.Add("description", "Checking for protected files.") - $StatusJson = $Status | ConvertTo-Json - - # Catch exceptions if they happen. All web and REST requests are set to halt on error and throw an exception. - Try { - Write-Host "Sending `"pending`" status to GitHub." - - # Sent POST request to GitHub to set the status check. Subsequent POST and GET requests use similar parameters - - # GitHubHeaders - GitHub authentication token - # StatusUrl - REST API endpoint to GET or POST data from/to - # StatusJson - Payload to send to GitHub for POST requests - Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop - - # Short delay so check doesn't finish before others start, which can cause GitHub UI to display confusing behavior. - Start-Sleep 3 + # Short delay so check doesn't finish before others start, which can cause GitHub UI to display confusing behavior. + Start-Sleep 3 - # Only process PRs being submitted to $PublishBranch or $DefaultBranch branches. Also skip if a fatal error is encountered. - If ((($TargetBranch -eq $DefaultBranch) -or ($TargetBranch -eq $PublishBranch)) -and (!$FatalError)) { + # Only process PRs being submitted to $PublishBranch or $DefaultBranch branches. Also skip if a fatal error is encountered. + If ((($TargetBranch -eq $DefaultBranch) -or ($TargetBranch -eq $PublishBranch)) -and (!$FatalError)) { - Write-Host "$DefaultBranch or $PublishBranch branch" + Write-Host "$DefaultBranch or $PublishBranch branch" + Try { + # Get the list of files modified in the PR. # Get the permissions of the PR submitter. $FileListData = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $FileListUrl -FollowRelLink -MaximumFollowRelLink 50 -ErrorAction Stop $PrSubmitterPerms = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $PrSubmitterPermissionsUrl -ErrorAction Stop + + } Catch { - $FileList = @() + $FatalError = $True - # Collapse pages into a single list if there are any and store in $FileList - ForEach ($Page in $FileListData) { $FileList += $Page } + } - $ProtectedFileFound = $False + $FileList = @() - # Only process PRs that actually have changed files. - If ($($FileList.Count -gt 0)) { + # Collapse pages into a single list if there are any and store in $FileList + ForEach ($Page in $FileListData) { $FileList += $Page } - Write-Host "PR has files`n`rFile list: $($FileList.filename)" + $ProtectedFileFound = $False - # Process PRs that are submitted by non-admins - If ($PrSubmitterPerms.permission -ne "admin") { + # Only process PRs that actually have changed files. + If ($($FileList.Count -gt 0)) { - Write-Host "Submitter is not an admin" + Write-Host "PR has files`n`rFile list: $($FileList.filename)" - # Check to see if submitter is an protected file approver + # Process PRs that are submitted by non-admins + If ($PrSubmitterPerms.permission -ne "admin") { - $ValidApprover = $False + Write-Host "Submitter is not an admin" - If ($ApproverList.Contains($PrSubmitter)) { + # Check to see if submitter is an protected file approver - $ValidApprover = $True + $ValidApprover = $False - } + If ($ApproverList.Contains($PrSubmitter)) { - # Only check if there's a protected file in the PR if the PR submitter isn't a protected file approver. - If ($ValidApprover -eq $False) { + $ValidApprover = $True - Write-Host "Not an admin or protected file approver. Checking files." + } + + # Only check if there's a protected file in the PR if the PR submitter isn't a protected file approver. + If ($ValidApprover -eq $False) { - # Loop through the protected file list and check to see if any of them are present in the changed file list. If we do, set the flag - # and the break out of the loop. - ForEach ($File in $ProtectedFileList) { + Write-Host "Not an admin or protected file approver. Checking files." - $File = $File.ToLower() + # Loop through the protected file list and check to see if any of them are present in the changed file list. If we do, set the flag + # and the break out of the loop. + ForEach ($File in $ProtectedFileList) { - ForEach ($PrFile in $FileList) { + $File = $File.ToLower() - $PrFile = $PrFile.filename.ToLower() + ForEach ($PrFile in $FileList) { - If ($PrFile.Contains($File)) { + $PrFile = $PrFile.filename.ToLower() - $ProtectedFileFound = $True + If ($PrFile.Contains($File)) { - Write-Host "PROTECTED FILE: $PrFile" + $ProtectedFileFound = $True - Break + Write-Host "PROTECTED FILE: $PrFile" - } + Break } } - # If a protected file is found, set state to 'failure'. If protected not found, set to 'success'. Set description accordingly. - If ($ProtectedFileFound) { - - Write-Host "Protected file found" - $Status.state = "failure" - $Status.description = "A protected file was found. Click Details for info." - - } Else { + } - Write-Host "No protected files found" - $Status.state = "success" - $Status.description = "No protected files included in PR." + # If a protected file is found, throw an exception to cause the script to exit with an exit code of 1. + If ($ProtectedFileFound) { - } + Throw "A protected file was found." } Else { - - Write-Host "PR submitter $PrSubmitter is an approved protected file submitter." - $Status.state = "success" - $Status.description = "PR submitter is an approved protected file submitter." + Write-Host "No protected files found" } - # Set status to 'success' since the submitter is an admin. } Else { - - Write-Host "PR submitter $PrSubmitter is an admin." - - $Status.state = "success" - $Status.description = "PR submitter is an admin." + + Write-Host "PR submitter $PrSubmitter is an approved protected file submitter." } - + + # Set status to 'success' since the submitter is an admin. } Else { - # Set status to 'success' since the PR doesn't contain any files. - $Status.state = "success" - $Status.description = "PR doesn't contain any files." + Write-Host "PR submitter $PrSubmitter is an admin." } - - } Else { - - # Set status to 'success' since the PR isn't being submitted to $DefaultBranch or $PublishBranch. - $Status.state = "success" - $Status.description = "PR base branch isn't $DefaultBranch or $PublishBranch." - } - - } Catch { - - $FatalError = $True - - } - - # If an exception was encountered, allow the status check to pass since we don't want to block PRs just because the - # check is broken for some reason. - If ($FatalError) { - - $Status.state = "success" - $Status.description = "Error encountered. Please notify marveldocs-admins." - - } - - # Get ready to push status check results that'll either allow or block the PR from progressing. - $StatusJson = $Status | ConvertTo-Json - $SuccessfulPost = $False - $RetryCount = 0 - - Do { + } Else { - Try { + Write-Host "PR doesn't contain any files." - # Send POST request to GitHub - Invoke-WebRequest -Headers $GitHubHeaders -Uri $StatusUrl -UseBasicParsing -Method POST -Body $StatusJson -ErrorAction Stop - $SuccessfulPost = $True - } Catch { + } - # If the request fails for any reason, retry it after a delay, up to six times. - $RetryCount++ - Start-Sleep 1 + } Else { - } + Write-Host "PR base branch isn't $DefaultBranch or $PublishBranch." + + } - } Until (($SuccessfulPost) -or ($RetryCount -gt 5)) # If an exception was encountered, or if the attempt to post the status check failed, send email notification with error and PR info. - If (($FatalError) -or (!$SuccessfulPost)) { + If ($FatalError) { $Body = "

An error occurred while processing the protected file check for a pull request.

" $Body = $Body + "

Error encountered

" From f5a657683c949aa08a810461a77f0e0c3d2a9b6a Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 24 Jul 2024 14:56:00 -0700 Subject: [PATCH 10/92] remove RequestID reference --- .github/workflows/Shared-AutoLabelAssign.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 62b6cc9b5f8..6e2e606a2db 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -484,7 +484,6 @@ jobs: Write-Host "Request event: $GitRequestEvent" Write-Host "GitHub action: $GitHubAction" Write-Host "GitHub state: $GitHubState" - Write-Host "Request Id: $RequestId" Write-Host "Default branch: $DefaultBranch" Write-Host "Target branch: $TargetBranch" Write-Host "PR files URL: $PrFileListUrl" @@ -602,4 +601,3 @@ jobs: } # Number of files in PR check } # PR event and action check - From 3c54559c1c403094fdf4f0c822b7a2ab70bc76af Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:36:50 -0700 Subject: [PATCH 11/92] Add new workflow --- .github/workflows/Shared-ExtractPayload.yml | 85 +++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/Shared-ExtractPayload.yml diff --git a/.github/workflows/Shared-ExtractPayload.yml b/.github/workflows/Shared-ExtractPayload.yml new file mode 100644 index 00000000000..ebbb2638293 --- /dev/null +++ b/.github/workflows/Shared-ExtractPayload.yml @@ -0,0 +1,85 @@ +name: Download and extract payload artifact + +permissions: + pull-requests: write + contents: read + actions: read + +on: + workflow_call: + inputs: + WorkflowId: + required: true + type: string + OrgRepo: + required: true + type: string + secrets: + AccessToken: + required: true + outputs: + WorkflowPayload: + value: ${{ jobs.build.outputs.JobPayload }} + +jobs: + build: + name: Run Script + runs-on: windows-latest + outputs: + JobPayload: ${{ steps.get-payload.outputs.WorkflowPayload }} + steps: + - name: Script + id: get-payload + shell: pwsh + env: + WorkflowId: ${{ inputs.WorkflowId }} + AccessToken: ${{ secrets.AccessToken }} + OrgRepo: ${{ inputs.OrgRepo }} + + run: | + + $AccessToken = $env:AccessToken + $WorkflowId = $env:WorkflowId + $OrgRepo = $env:OrgRepo + $WorkspacePath = "$env:GITHUB_WORKSPACE" + + $ArtifactName = "PayloadJson" + $ArtifactFilePath = Join-Path $WorkspacePath -ChildPath "$ArtifactName.zip" + $GitHubDataPath = Join-Path $WorkspacePath -ChildPath "$ArtifactName.json" + $ArtifactUrl = "https://api.github.com/repos/$OrgRepo/actions/runs/$WorkflowId/artifacts" + + # Set GitHub REST API headers + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $AccessToken") + $GitHubHeaders.Add("User-Agent", "officedocs") + + Write-Host "Repo: $OrgRepo" + Write-Host "Parent workflow ID: $WorkflowId" + Write-Host "Workspace path: $WorkspacePath" + Write-Host "Artifact URL: $ArtifactUrl" + + Write-Host "Retrieve parent workflow artifacts" + $ArtifactData = Invoke-RestMethod -Uri $ArtifactUrl -Headers $GitHubHeaders + + $ArtifactDownloadUrl = $ArtifactData.artifacts.archive_download_url + + Write-Host "Retrieve payload artifact from $ArtifactDownloadUrl and save to $ArtifactFilePath" + Invoke-RestMethod -Uri $ArtifactDownloadUrl -Headers $GitHubHeaders -OutFile $ArtifactFilePath + + Write-Host "Expand artifact payload zip $ArtifactFilePath to $WorkspacePath" + Expand-Archive $ArtifactFilePath -DestinationPath $WorkspacePath + + Write-Host "Get payload data from $GitHubDataPath" + $GitHubDataJson = Get-Content $GitHubDataPath -Raw + $GitHubData = $GitHubDataJson | ConvertFrom-Json -Depth 50 + $GitRequestEvent = $GitHubData.event_name + $GitHubAction = $GitHubData.event.action + $OriginRepo = $GitHubData.event.repository.full_name + $PrNumber = $GitHubData.event.number + + Write-Host "GitHub event: $GitRequestEvent" + Write-Host "GitHub action: $GitHubAction" + Write-Host "Origin repo: $OriginRepo" + Write-Host "PR Number: $PrNumber" + + echo "WorkflowPayload=$GitHubDataJson" >> $env:GITHUB_OUTPUT \ No newline at end of file From 6106a05a7c59911f49b2a1b5090eacfaaac25594 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:00:03 -0700 Subject: [PATCH 12/92] Update protected files list --- .github/workflows/Shared-ProtectedFiles.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index c3dc840a7a6..7501266a4f0 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -24,7 +24,26 @@ jobs: shell: pwsh env: - ProtectedFileList: '[".gitignore", ".openpublishing.publish.config.json", "docfx.json", "README.md", "LICENSE-CODE", "ThirdPartyNotices", ".acrolinx-config.edn", ".gitattributes", "ProtectedFiles.yml", "Shared-ProtectedFiles.yml", "AutoLabelAssign.yml", "Shared-AutoLabelAssign.yml"]' + ProtectedFileList: '[ + ".gitignore", + ".openpublishing.publish.config.json", + "docfx.json", + "README.md", + "LICENSE-CODE", + "ThirdPartyNotices", + ".acrolinx-config.edn", + ".gitattributes", + "ProtectedFiles.yml", + "AutoLabelAssign.yml", + "LiveMergeCheck.yml", + "PrFileCount.yml", + "BackgroundTasks.yml", + "Shared-ProtectedFiles.yml", + "Shared-AutoLabelAssign.yml", + "Shared-LiveMergeCheck.yml", + "Shared-PrFileCount.yml", + "Shared-ExtractPayload.yml" + ]' ApproverList: '["dstrome"]' HelpUrl: 'https://review.learn.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main' From ef70601a9d97c0d8c53c2a273192662218307788 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 29 Jul 2024 11:44:51 -0700 Subject: [PATCH 13/92] new workflow --- .../Shared-AutoLabelMsftContributor.yml | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 .github/workflows/Shared-AutoLabelMsftContributor.yml diff --git a/.github/workflows/Shared-AutoLabelMsftContributor.yml b/.github/workflows/Shared-AutoLabelMsftContributor.yml new file mode 100644 index 00000000000..2a0830b2535 --- /dev/null +++ b/.github/workflows/Shared-AutoLabelMsftContributor.yml @@ -0,0 +1,289 @@ +name: Label Microsoft contrib + +permissions: + pull-requests: write + contents: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + secrets: + AccessToken: + required: true + TeamReadAccessToken: + required: true +jobs: + build: + name: Run Script + runs-on: windows-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + TeamReadAccessToken: ${{ secrets.TeamReadAccessToken }} + + run: | + + $CurrentDateTime = Get-Date + + # Get payload data from GitHub. + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $AccessToken = $env:AccessToken + $TeamReadAccessToken = $env:TeamReadAccessToken + $GitRequestEvent = $GitHubData.event_name + + $RepoName = $GitHubData.event.repository.name + $RepoUrl = $GitHubData.event.repository.url + $RepoTopicUrl = "$RepoUrl/topics" + + # 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/" + + ##################### + ##################### + # Test-Label + + Function Test-Label { + + [CmdletBinding()] + param( + + $Name, + $RepoUri, + $Headers + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $LabelUri = $RepoUri.Replace("{/name}","/$Name") + + # Check to see if the label we want exists in the repo + Try { + + $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $Headers -ErrorAction Stop + $LabelFound = $True + + } Catch { + + $LabelFound = $False + + } + + # Return boolean to calling statement + $LabelFound + + } + + ##################### + ##################### + # New-Label + + Function New-Label { + + [CmdletBinding()] + param( + + $Name, + $Color, + $Description, + $RepoUri, + $Headers + ) + + # Remove placeholder text from repo URL + $RepoUri = $RepoUri.Replace("{/name}","") + $Result = $Null + + # Construct the JSON statement that will be sent to GitHub as the body of the web request. Include the name of the label, its color, and description. + # Convert hash table to JSON + $Body = @{} + $Body.Add("name", $Name) + $Body.Add("color", $Color) + $Body.Add("description", $description) + $Body = $Body | ConvertTo-Json + + # Try to submit the request to GitHub API to create the label + Try { + + $Result = Invoke-WebRequest -UseBasicParsing -Uri $RepoUri -Headers $Headers -Body $Body -Method POST + + } Catch { + + $Result = $Error[0].Exception.Message + + } + + # Return boolean to calling statement + $Result + } + + ##################### + ##################### + # Set-Label + + Function Set-Label { + + param( + + $IssueUrl, + $LabelName, + $Headers + + ) + + # Construct label URL based on pull request URL + $IssueLabelUrl = "$IssueUrl/labels" + + # Construct JSON statement that will be sent to GitHub as the body of the web request. Includes only the label name. GitHub expects an array even thought it's a single value + # Convert array to JSON + $Body = @() + $Body += $LabelName + $Body = ConvertTo-Json -InputObject $Body + + # Try to submit the request to GitHub API to apply they label to the pull request + Try { + + $Result = Invoke-WebRequest -UseBasicParsing -Uri $IssueLabelUrl -Body $Body -Headers $Headers -Method POST + + + } Catch { + + } + + # Send results back to calling statement as an array + $functionresult = @() + $Functionresult += $IssueLabelUrl + $functionresult += $error + $functionresult += $Result + $functionresult + + } + + ##################### + ##################### + # Main + + + + Write-Host "Repo: $($GitHubData.event.repository.name)" + Write-Host "Sender: $($GitHubData.event.sender.login)" + Write-Host "Request type: $GitRequestEvent" + Write-Host "GitHub action: $($GitHubData.event.action)" + + # Create general GitHub HTTP authentication header + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", "OfficeDocs") + $GitHubHeaders.Add("Accept","application/vnd.github.mercy-preview+json") + + # Create team read GitHub HTTP authentication header. Need a token that has access to org scope. GITHUB_TOKEN that + # is used to populate $AccessToken doesn't have access to org scope so using a custom fine-grained token with limited member read scope. + $TeamReadGitHubHeaders = @{} + $TeamReadGitHubHeaders.Add("Authorization","token $($TeamReadAccessToken)") + $TeamReadGitHubHeaders.Add("User-Agent", "OfficeDocs") + $TeamReadGitHubHeaders.Add("Accept","application/vnd.github.mercy-preview+json") + + # -and ($GitHubData.event.action -eq "opened") + # Only process event types of 'pull_request_target' that are 'opened' (PR created) + If (($GitRequestEvent -eq "pull_request_target") ) { + + $Contributor = $GitHubData.event.sender.login + $LabelName = "Microsoft submitter" + $LabelColor = "0269ef" + $LabelDescription = "" + $LabelExists = $False + + # Create the member URL to check + $ContributorTeamUrl = $OrgTeamUrl + $Contributor + + Write-Host "Checking to see if $Contributor is a member of the MicrosoftDocs `"everyone`" team." + + # Check to see if the contributor is a member of the MicrosoftDocs "everyone" team + $TeamResult = Invoke-RestMethod -Uri $ContributorTeamUrl -Headers $TeamReadGitHubHeaders + + If ($TeamResult.state -eq "active") { + + Write-Host "Submitter is an active member of the MicrosoftDocs `"everyone`" team." + + # Check to see if the label exists in the repo + If (Test-Label -Name $LabelName -RepoUri $GitHubData.event.repository.labels_url -Headers $GitHubHeaders) { + + Write-Host "$LabelName label exists." + + $LabelExists = $True + + } Else { + + Write-Host "$LabelName doesn't exist. Creating." + + Try { + + # Create the label because it doesn't exist in the repo + $LabelResult = New-Label -Name $LabelName -Color $LabelColor -Description $LabelDescription -RepoUri $GitHubData.event.repository.labels_url -Headers $GitHubHeaders -ErrorAction Stop + + } Catch { + + $LabelExists = $False + + } + + # Check the result of the New-Label command to verify it was created successfully + If ($($LabelResult.statusdescription) -eq "Created") { + + Write-Host "$LabelName created." + $LabelExists = $True + + } Else { + + Write-Host "WARNING: $LabelName creation failed. Error: $($Error[0].Exception.Message)." + + $LabelExists = $False + + } + + } + + # Only attempt to apply the label if it exists in the repo + If ($LabelExists) { + + Write-Host "Setting label on PR." + + + $LabelUrl = $GitHubData.event.pull_request.issue_url + + Write-Host "Using pull request URL $LabelUrl." + + + # Apply the label to the pull request + Try { + + Set-Label -IssueUrl $LabelUrl -LabelName $LabelName -Headers $GitHubHeaders + + + } Catch { + + Write-Host "ERROR: Error setting label. $($Error[0].ErrorDetails.Message)." + } + + + + } + + + } Else { + + Write-Host "Submitter isn't an active member of the MicrosoftDocs `"everyone`" team." + + } + + } Else { + + Write-Host "Not an opened pull request." + + } + + From 69e6f5ec3b76e97f1bd1dd1b07dfed8bc82ef387 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:50:42 -0700 Subject: [PATCH 14/92] Fixed submitter bug and added exception handling for team membership check --- .../Shared-AutoLabelMsftContributor.yml | 90 ++++++++++--------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelMsftContributor.yml b/.github/workflows/Shared-AutoLabelMsftContributor.yml index 2a0830b2535..57e232f026b 100644 --- a/.github/workflows/Shared-AutoLabelMsftContributor.yml +++ b/.github/workflows/Shared-AutoLabelMsftContributor.yml @@ -191,7 +191,7 @@ jobs: # Only process event types of 'pull_request_target' that are 'opened' (PR created) If (($GitRequestEvent -eq "pull_request_target") ) { - $Contributor = $GitHubData.event.sender.login + $Contributor = $GitHubData.event.pull_request.user.login $LabelName = "Microsoft submitter" $LabelColor = "0269ef" $LabelDescription = "" @@ -202,80 +202,86 @@ jobs: Write-Host "Checking to see if $Contributor is a member of the MicrosoftDocs `"everyone`" team." - # Check to see if the contributor is a member of the MicrosoftDocs "everyone" team - $TeamResult = Invoke-RestMethod -Uri $ContributorTeamUrl -Headers $TeamReadGitHubHeaders + Try { - If ($TeamResult.state -eq "active") { + # Check to see if the contributor is a member of the MicrosoftDocs "everyone" team + $TeamResult = Invoke-RestMethod -Uri $ContributorTeamUrl -Headers $TeamReadGitHubHeaders -ErrorAction Stop - Write-Host "Submitter is an active member of the MicrosoftDocs `"everyone`" team." + If ($TeamResult.state -eq "active") { - # Check to see if the label exists in the repo - If (Test-Label -Name $LabelName -RepoUri $GitHubData.event.repository.labels_url -Headers $GitHubHeaders) { + Write-Host "Submitter is an active member of the MicrosoftDocs `"everyone`" team." - Write-Host "$LabelName label exists." + # Check to see if the label exists in the repo + If (Test-Label -Name $LabelName -RepoUri $GitHubData.event.repository.labels_url -Headers $GitHubHeaders) { - $LabelExists = $True + Write-Host "$LabelName label exists." - } Else { - - Write-Host "$LabelName doesn't exist. Creating." + $LabelExists = $True - Try { - - # Create the label because it doesn't exist in the repo - $LabelResult = New-Label -Name $LabelName -Color $LabelColor -Description $LabelDescription -RepoUri $GitHubData.event.repository.labels_url -Headers $GitHubHeaders -ErrorAction Stop + } Else { + + Write-Host "$LabelName doesn't exist. Creating." - } Catch { + Try { + + # Create the label because it doesn't exist in the repo + $LabelResult = New-Label -Name $LabelName -Color $LabelColor -Description $LabelDescription -RepoUri $GitHubData.event.repository.labels_url -Headers $GitHubHeaders -ErrorAction Stop - $LabelExists = $False + } Catch { - } + $LabelExists = $False - # Check the result of the New-Label command to verify it was created successfully - If ($($LabelResult.statusdescription) -eq "Created") { + } - Write-Host "$LabelName created." - $LabelExists = $True + # Check the result of the New-Label command to verify it was created successfully + If ($($LabelResult.statusdescription) -eq "Created") { - } Else { + Write-Host "$LabelName created." + $LabelExists = $True + + } Else { - Write-Host "WARNING: $LabelName creation failed. Error: $($Error[0].Exception.Message)." + Write-Host "WARNING: $LabelName creation failed. Error: $($Error[0].Exception.Message)." - $LabelExists = $False + $LabelExists = $False + + } } - } + # Only attempt to apply the label if it exists in the repo + If ($LabelExists) { - # Only attempt to apply the label if it exists in the repo - If ($LabelExists) { + Write-Host "Setting label on PR." - Write-Host "Setting label on PR." + + $LabelUrl = $GitHubData.event.pull_request.issue_url - - $LabelUrl = $GitHubData.event.pull_request.issue_url + Write-Host "Using pull request URL $LabelUrl." - Write-Host "Using pull request URL $LabelUrl." + # Apply the label to the pull request + Try { - # Apply the label to the pull request - Try { + Set-Label -IssueUrl $LabelUrl -LabelName $LabelName -Headers $GitHubHeaders + - Set-Label -IssueUrl $LabelUrl -LabelName $LabelName -Headers $GitHubHeaders - + } Catch { - } Catch { + Write-Host "ERROR: Error setting label. $($Error[0].ErrorDetails.Message)." + } - Write-Host "ERROR: Error setting label. $($Error[0].ErrorDetails.Message)." } - - } + } Else { + Write-Host "Submitter isn't an active member of the MicrosoftDocs `"everyone`" team." - } Else { + } + } Catch { + Write-Host "Submitter isn't an active member of the MicrosoftDocs `"everyone`" team." } From 7968010d5f646b9e8a1309fb702273170ff7fc53 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:37:09 -0700 Subject: [PATCH 15/92] Switch to ubuntu and close loophole --- .github/workflows/Shared-PrFileCount.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Shared-PrFileCount.yml b/.github/workflows/Shared-PrFileCount.yml index 624ebb65f45..faeec974ca3 100644 --- a/.github/workflows/Shared-PrFileCount.yml +++ b/.github/workflows/Shared-PrFileCount.yml @@ -18,7 +18,7 @@ on: jobs: build: name: Run Script - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Script shell: pwsh @@ -400,9 +400,9 @@ jobs: Write-Host "Pull request in monitored branch" - If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened")) { + If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened") -or ($GitHubData.event.action -eq "edited")) { - Write-Host "Opened, reopened, or synchronized. Process PR." + Write-Host "Opened, reopened, synchronized, or edited. Process PR." ProcessPr From c1a4b9385fe69683c6cbc00307efb90318c8d079 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 31 Jul 2024 13:37:20 -0700 Subject: [PATCH 16/92] Switch to ubuntu --- .github/workflows/AutoLabelAssign.yml | 46 +++++++++++++------ .github/workflows/Shared-AutoLabelAssign.yml | 2 +- .../Shared-AutoLabelMsftContributor.yml | 2 +- .github/workflows/Shared-ExtractPayload.yml | 2 +- .github/workflows/Shared-LiveMergeCheck.yml | 3 +- .github/workflows/Shared-ProtectedFiles.yml | 2 +- 6 files changed, 38 insertions(+), 19 deletions(-) diff --git a/.github/workflows/AutoLabelAssign.yml b/.github/workflows/AutoLabelAssign.yml index 1acf95af1d5..f977fa48123 100644 --- a/.github/workflows/AutoLabelAssign.yml +++ b/.github/workflows/AutoLabelAssign.yml @@ -1,23 +1,41 @@ -name: Auto label and assign pull requests +name: Assign and label PR permissions: pull-requests: write contents: read + actions: read -on: [pull_request_target] +on: + workflow_run: + workflows: [Background tasks] + types: + - completed jobs: + download-payload: + name: Download and extract payload artifact + uses: MicrosoftDocs/microsoft-365-docs/.github/workflows/Shared-ExtractPayload.yml@workflows-prod + with: + WorkflowId: ${{ github.event.workflow_run.id }} + OrgRepo: ${{ github.repository }} + secrets: + AccessToken: ${{ secrets.GITHUB_TOKEN }} - auto-label-assign: - uses: MicrosoftDocs/microsoft-365-docs/.github/workflows/Shared-AutoLabelAssign.yml@workflows-prod - with: - PayloadJson: ${{ toJSON(github) }} - AutoAssignUsers: 1 - AutoLabel: 1 - ExcludedUserList: '["user1", "user2"]' - ExcludedBranchList: '["branch1", "branch2"]' - secrets: - AccessToken: ${{ secrets.GITHUB_TOKEN }} + label-assign: + name: Run assign and label + needs: [download-payload] + uses: MicrosoftDocs/microsoft-365-docs/.github/workflows/Shared-AutoLabelAssign.yml@workflows-prod + with: + PayloadJson: ${{ needs.download-payload.outputs.WorkflowPayload }} + AutoAssignUsers: 1 + AutoLabel: 1 + ExcludedUserList: '["user1", "user2"]' + ExcludedBranchList: '["branch1", "branch2"]' + secrets: + AccessToken: ${{ secrets.GITHUB_TOKEN }} + + + + - - + diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 6e2e606a2db..4147ee3b359 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -29,7 +29,7 @@ on: jobs: build: name: Run Script - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Script shell: pwsh diff --git a/.github/workflows/Shared-AutoLabelMsftContributor.yml b/.github/workflows/Shared-AutoLabelMsftContributor.yml index 57e232f026b..cc8bf86cf6d 100644 --- a/.github/workflows/Shared-AutoLabelMsftContributor.yml +++ b/.github/workflows/Shared-AutoLabelMsftContributor.yml @@ -18,7 +18,7 @@ on: jobs: build: name: Run Script - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Script shell: pwsh diff --git a/.github/workflows/Shared-ExtractPayload.yml b/.github/workflows/Shared-ExtractPayload.yml index ebbb2638293..bd0d7c2f8ef 100644 --- a/.github/workflows/Shared-ExtractPayload.yml +++ b/.github/workflows/Shared-ExtractPayload.yml @@ -24,7 +24,7 @@ on: jobs: build: name: Run Script - runs-on: windows-latest + runs-on: ubuntu-latest outputs: JobPayload: ${{ steps.get-payload.outputs.WorkflowPayload }} steps: diff --git a/.github/workflows/Shared-LiveMergeCheck.yml b/.github/workflows/Shared-LiveMergeCheck.yml index d3357062a49..c65fd9c4689 100644 --- a/.github/workflows/Shared-LiveMergeCheck.yml +++ b/.github/workflows/Shared-LiveMergeCheck.yml @@ -18,7 +18,7 @@ on: jobs: build: name: Run Script - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Script shell: pwsh @@ -144,6 +144,7 @@ jobs: If ($GitRequestEvent -eq "pull_request_target") { Write-Host "Request type is pull_request. Processing request." + Write-Host "Target branch is $TargetBranch." If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened") -or ($GitHubData.event.action -eq "edited")) { diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 7501266a4f0..af9b9d003cb 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -18,7 +18,7 @@ on: jobs: build: name: Run Script - runs-on: windows-latest + runs-on: ubuntu-latest steps: - name: Script shell: pwsh From 7e202146cfaac7520a892350650b15465c1d77ee Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 2 Aug 2024 13:53:54 -0700 Subject: [PATCH 17/92] Add job summary outputs --- .github/workflows/Shared-LiveMergeCheck.yml | 16 ++++++--- .github/workflows/Shared-PrFileCount.yml | 37 +++++++++++++++++---- .github/workflows/Shared-ProtectedFiles.yml | 20 ++++++++--- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/.github/workflows/Shared-LiveMergeCheck.yml b/.github/workflows/Shared-LiveMergeCheck.yml index c65fd9c4689..adca0412602 100644 --- a/.github/workflows/Shared-LiveMergeCheck.yml +++ b/.github/workflows/Shared-LiveMergeCheck.yml @@ -25,8 +25,6 @@ jobs: env: PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} - StatusCheckUrl: "https://review.docs.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main" - run: | @@ -36,10 +34,8 @@ jobs: # Get payload data and event from GitHub $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $GitRequestEvent = $GitHubData.event_name - $StatusCheckHelpUrl = $env:StatusCheckUrl $PublishBranch = "live" - $StatusCheckName = "max/live-compare-merge" # Retrieve GitHub token, create github HTTP authentication header $AccessToken = $env:AccessToken @@ -52,6 +48,7 @@ jobs: $StatusUrl = $GitHubData.event.pull_request.statuses_url $RequiredRepo = $GitHubData.event.repository.full_name + $PrUrl = $GitHubData.event.pull_request.html_url $TargetBranch = $GitHubData.event.pull_request.base.ref $OriginBranch = $GitHubData.event.pull_request.head.ref $OriginRepo = $GitHubData.event.pull_request.head.repo.full_name @@ -146,6 +143,9 @@ jobs: Write-Host "Request type is pull_request. Processing request." Write-Host "Target branch is $TargetBranch." + # Make the job summary section show up so the job always looks consistent. + echo "" >> $env:GITHUB_STEP_SUMMARY + If (($GitHubData.event.action -eq "opened") -or ($GitHubData.event.action -eq "synchronize") -or ($GitHubData.event.action -eq "reopened") -or ($GitHubData.event.action -eq "edited")) { # Only run checks if the base branch matches the branch defined in $PublishBranch. @@ -163,7 +163,13 @@ jobs: Set-PrConversationMessage -Message $($LiveMergeMessage -F $TargetBranch, $DefaultBranch) - Throw "Origin branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch." + # Populates the job summary if an invalid target branch is found. + echo "# Pull request validation error" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "The branch **$OriginRepo/$OriginBranch** isn't allowed to merge into **$PublishBranch** in PR: $PrUrl." >> $env:GITHUB_STEP_SUMMARY + + Write-Host "The branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch. Click Summary in the left pane for more information." + } # OriginBranch diff --git a/.github/workflows/Shared-PrFileCount.yml b/.github/workflows/Shared-PrFileCount.yml index faeec974ca3..e9365888225 100644 --- a/.github/workflows/Shared-PrFileCount.yml +++ b/.github/workflows/Shared-PrFileCount.yml @@ -28,7 +28,6 @@ jobs: PrWarnLimit: 30 PrBlockLimit: 100 - StatusCheckUrl: "https://review.docs.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main" run: | @@ -41,7 +40,6 @@ jobs: $AccessToken = $env:AccessToken $PrWarnLimit = $env:PrWarnLimit $PrBlockLimit = $env:PrBlockLimit - $StatusCheckUrl = $env:StatusCheckUrl # Create github HTTP authentication header @@ -56,6 +54,7 @@ jobs: $CurrentLabelName = $GitHubData.event.label.name $CommentsUrl = $GitHubData.event.pull_request.comments_url $Labels = $GitHubData.event.pull_request.labels + $PrUrl = $GitHubData.event.pull_request.html_url $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" $WorkflowsRef = "workflows-prod" @@ -230,7 +229,10 @@ jobs: Remove-WarningLabel - Throw "Number of changed files $NumChangedFiles is greater than BLOCK limit $PrBlockLimit. " + echo "The PR $PrUrl changes **$NumChangedFiles** files which exceeds the **BLOCK** limit of **$PrBlockLimit**. The **$WarningLabelName** label has been removed and can't be re-added while the total number of files exceeds the block limit." >> $env:GITHUB_STEP_SUMMARY + echo "Reduce the number of files in this PR to below $PrBlockLimit files or request an admin review to merge this PR." >> $env:GITHUB_STEP_SUMMARY + + Throw "Number of changed files $NumChangedFiles is greater than BLOCK limit $PrBlockLimit. Click Summary in the left pane for more information." } } @@ -265,12 +267,18 @@ jobs: If ($NumChangedFiles -gt $PrBlockLimit) { - Throw "PR file count $NumChangedFiles is greater than PR change limit $PrBlockLimit." + echo "The PR $PrUrl changes **$NumChangedFiles** files which exceeds the **BLOCK** limit of **$PrBlockLimit**." >> $env:GITHUB_STEP_SUMMARY + echo "Reduce the number of files in this PR to below $PrBlockLimit files or request an admin review to merge this PR." >> $env:GITHUB_STEP_SUMMARY + + Throw "PR file count $NumChangedFiles is greater than PR change limit $PrBlockLimit. Click Summary in the left pane for more information." } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { - Throw "PR file count $NumChangedFiles is greater than PR change limit $PrWarnLimit." + echo "The PR $PrUrl changes **$NumChangedFiles** files which exceeds the **WARN** limit of **$PrWarnLimit**." >> $env:GITHUB_STEP_SUMMARY + echo "Review the files changed in the PR and confirm the changes are expected. To attest the changes are expected and merge the PR, add the **$WarningLabelName** label to the PR." >> $env:GITHUB_STEP_SUMMARY + + Throw "PR file count $NumChangedFiles is greater than PR change limit $PrWarnLimit. Click Summary in the left pane for more information." } Else { @@ -347,7 +355,10 @@ jobs: } - Throw "Number of files in PR, $NumChangedFiles, exceeds the BLOCK file limit of $PrBlockLimit." + echo "The PR $PrUrl changes **$NumChangedFiles** files which exceeds the **BLOCK** limit of **$PrBlockLimit**." >> $env:GITHUB_STEP_SUMMARY + echo "Reduce the number of files in this PR to below $PrBlockLimit files or request an admin review to merge this PR." >> $env:GITHUB_STEP_SUMMARY + + Throw "Number of files in PR, $NumChangedFiles, exceeds the BLOCK file limit of $PrBlockLimit. Click Summary in the left pane for more information." } ElseIf ($NumChangedFiles -gt $PrWarnLimit) { @@ -372,7 +383,10 @@ jobs: } - Throw "Number of files in PR, $NumChangedFiles, exceeds the WARN file limit of $PrWarnLimit." + echo "The PR $PrUrl changes **$NumChangedFiles** files which exceeds the **WARN** limit of **$PrWarnLimit**." >> $env:GITHUB_STEP_SUMMARY + echo "Review the files changed in the PR and confirm the changes are expected. To merge the PR and attest the changes are expected, add the **$WarningLabelName** label to the PR." >> $env:GITHUB_STEP_SUMMARY + + Throw "Number of files in PR, $NumChangedFiles, exceeds the WARN file limit of $PrWarnLimit. Click Summary in the left pane for more information." } @@ -396,6 +410,12 @@ jobs: Write-Host "Pull request target branch: $TargetBranch. Default branch $DefaultBranch." + + # Setting job summary here just in case the number of files exceeds the warn or block limits. + # If the number files is below the limits, the job summary will be cleared and this won't be shown. + echo "# Pull request validation error" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + If ($TargetBranch -eq $DefaultBranch) { Write-Host "Pull request in monitored branch" @@ -426,4 +446,7 @@ jobs: } + # If we got here, the check passed. Clear the job summary. + echo "" > $env:GITHUB_STEP_SUMMARY + } diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index af9b9d003cb..97669301114 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -29,6 +29,7 @@ jobs: ".openpublishing.publish.config.json", "docfx.json", "README.md", + "LICENSE", "LICENSE-CODE", "ThirdPartyNotices", ".acrolinx-config.edn", @@ -45,7 +46,6 @@ jobs: "Shared-ExtractPayload.yml" ]' ApproverList: '["dstrome"]' - HelpUrl: 'https://review.learn.microsoft.com/en-us/office-authoring-guide/pull-request-status-checks?branch=main' PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} @@ -62,7 +62,6 @@ jobs: # Get data from environment variables and convert lists from JSON $ProtectedFileList = $env:ProtectedFileList | ConvertFrom-Json $ApproverList = $env:ApproverList | ConvertFrom-Json - $HelpUrl = $env:HelpUrl # Create github HTTP authentication header $UserAgent = "officedocs" @@ -87,6 +86,7 @@ jobs: $FileListUrl = "$($GitHubData.event.pull_request.url)/files?per_page=100" $PrSubmitter = $GitHubData.event.pull_request.user.login $PrSubmitterPermissionsUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$PrSubmitter/permission") + $PrUrl = $GitHubData.event.pull_request.html_url $StatusUrl = $GitHubData.event.pull_request.statuses_url $TargetBranch = $GitHubData.event.pull_request.base.ref $DefaultBranch = $GitHubData.event.repository.default_branch @@ -146,6 +146,13 @@ jobs: # Only check if there's a protected file in the PR if the PR submitter isn't a protected file approver. If ($ValidApprover -eq $False) { + # Setting job summary here just in case there's a protected file. If there's no protected file, the job summary + # will be cleared and this won't be shown. + echo "# Pull request validation error" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "The following protected files were found in PR: $PrUrl." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + Write-Host "Not an admin or protected file approver. Checking files." # Loop through the protected file list and check to see if any of them are present in the changed file list. If we do, set the flag @@ -162,10 +169,10 @@ jobs: $ProtectedFileFound = $True + echo "- $PrFile" >> $env:GITHUB_STEP_SUMMARY + Write-Host "PROTECTED FILE: $PrFile" - Break - } } @@ -175,12 +182,15 @@ jobs: # If a protected file is found, throw an exception to cause the script to exit with an exit code of 1. If ($ProtectedFileFound) { - Throw "A protected file was found." + Throw "A protected file was found. Click Summary in the left pane for more information." } Else { Write-Host "No protected files found" + # Clear the job summary because no protected files were found. + echo "" > $env:GITHUB_STEP_SUMMARY + } } Else { From c268c95acebd9b04dde90f8aac17e2136ad0391e Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 7 Aug 2024 23:03:55 -0700 Subject: [PATCH 18/92] remove "license" --- .github/workflows/Shared-ProtectedFiles.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 97669301114..1de5c9437f9 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -29,7 +29,6 @@ jobs: ".openpublishing.publish.config.json", "docfx.json", "README.md", - "LICENSE", "LICENSE-CODE", "ThirdPartyNotices", ".acrolinx-config.edn", @@ -246,4 +245,4 @@ jobs: } } - \ No newline at end of file + From 24737e1fa2d09bf021ad38cc535ed1797a21d401 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:39:45 -0700 Subject: [PATCH 19/92] Improve file matching --- .github/workflows/Shared-ProtectedFiles.yml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 1de5c9437f9..5b0ea754a19 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -29,6 +29,7 @@ jobs: ".openpublishing.publish.config.json", "docfx.json", "README.md", + "LICENSE", "LICENSE-CODE", "ThirdPartyNotices", ".acrolinx-config.edn", @@ -44,7 +45,7 @@ jobs: "Shared-PrFileCount.yml", "Shared-ExtractPayload.yml" ]' - ApproverList: '["dstrome"]' + ApproverList: '["user1"]' PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} @@ -128,7 +129,7 @@ jobs: Write-Host "PR has files`n`rFile list: $($FileList.filename)" # Process PRs that are submitted by non-admins - If ($PrSubmitterPerms.permission -ne "admin") { + If ($PrSubmitterPerms.permission -ne "aadmin") { Write-Host "Submitter is not an admin" @@ -158,20 +159,21 @@ jobs: # and the break out of the loop. ForEach ($File in $ProtectedFileList) { - $File = $File.ToLower() - ForEach ($PrFile in $FileList) { - $PrFile = $PrFile.filename.ToLower() + $PrFile = $PrFile.filename + $LastIndexOfBackslash = $PrFile.LastIndexOf("/") + + $PrFile = $PrFile.Remove(0, $LastIndexOfBackslash + 1) - If ($PrFile.Contains($File)) { + If ($PrFile -ceq $File) { $ProtectedFileFound = $True - echo "- $PrFile" >> $env:GITHUB_STEP_SUMMARY - Write-Host "PROTECTED FILE: $PrFile" + Break + } } From e418431f04b275f79531f05235423615057b7977 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:47:24 -0700 Subject: [PATCH 20/92] Add job summary --- .github/workflows/Shared-ProtectedFiles.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 5b0ea754a19..764628df769 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -129,7 +129,7 @@ jobs: Write-Host "PR has files`n`rFile list: $($FileList.filename)" # Process PRs that are submitted by non-admins - If ($PrSubmitterPerms.permission -ne "aadmin") { + If ($PrSubmitterPerms.permission -ne "admin") { Write-Host "Submitter is not an admin" @@ -170,6 +170,8 @@ jobs: $ProtectedFileFound = $True + echo "- $PrFile" >> $env:GITHUB_STEP_SUMMARY + Write-Host "PROTECTED FILE: $PrFile" Break From 19112141f86104d0da3be962c40df407128c50c2 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 8 Aug 2024 11:55:39 -0700 Subject: [PATCH 21/92] Remove break --- .github/workflows/Shared-ProtectedFiles.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 764628df769..fdfb765a7d5 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -173,9 +173,7 @@ jobs: echo "- $PrFile" >> $env:GITHUB_STEP_SUMMARY Write-Host "PROTECTED FILE: $PrFile" - - Break - + } } From 73ae87488f72cdb966feaaf14bc8d503ebcdd246 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:43:49 -0700 Subject: [PATCH 22/92] New workflow --- .github/workflows/Shared-Stale.yml | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 .github/workflows/Shared-Stale.yml diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml new file mode 100644 index 00000000000..487fd8e0f71 --- /dev/null +++ b/.github/workflows/Shared-Stale.yml @@ -0,0 +1,99 @@ +name: Mark stale pull requests + +permissions: + issues: write + pull-requests: write + +on: + workflow_call: + inputs: + RunDebug: + type: boolean + required: true + RepoVisibility: + type: string + required: true + secrets: + AccessToken: + required: true + +jobs: + stale-private: + name: Stale check - private repos + runs-on: ubuntu-latest + if: inputs.RepoVisibility == 'private' + steps: + - uses: actions/stale@v9 + with: + repo-token: ${{ secrets.AccessToken }} + debug-only: ${{ inputs.RunDebug }} + operations-per-run: 1000 + days-before-issue-stale: -1 + days-before-issue-close: -1 + days-before-pr-stale: 90 + days-before-pr-close: 14 + stale-pr-label: Inactive + close-pr-label: Auto Closed + exempt-pr-labels: Keep open + stale-pr-message: > +

Inactive PR marked for closure

+

+ This pull request has been inactive for over 90 days, and an Inactive label has been added to it. If this PR remains inactive with no new comments, commits, or updates from main, it will be closed automatically in 14 days. +

+

Instructions for writer

+

+ If you're ready to merge this PR, add the Sign off label to have the PR reviewed and merged by PubOps. +

+ If you're not ready to merge this PR yet and want to keep it open, click on Update branch at the bottom of this PR to bring it up to date with the main branch. +

+ To view a list of any open, inactive, PRs assigned to you, click Inactive PRs. +

+ Thank you! +

+ PS: Mention @marveldocs-pubops in the comments if you need assistance. + close-pr-message: > + This pull request has been inactive for a further 14 days and is now being closed. If you decide to continue working on your changes, that's no problem. Simply select the Reopen pull request button at the bottom of the pull request. +
+ Thank you! +
+ PS: Mention @marveldocs-pubops in the comments if you need assistance. + +# stale-public: +# name: Stale check - public repos +# runs-on: ubuntu-latest +# if: inputs.RepoVisibility == 'public' +# steps: +# - uses: actions/stale@v9 +# with: +# repo-token: ${{ secrets.AccessToken }} +# debug-only: ${{ inputs.RunDebug }} +# operations-per-run: 1000 +# days-before-issue-stale: -1 +# days-before-issue-close: -1 +# days-before-pr-stale: -1 +# days-before-pr-close: -1 +# stale-pr-label: Inactive +# close-pr-label: Auto closed +# exempt-pr-labels: Keep open +# stale-pr-message: > +#

Inactive PR marked for closure

+#

+# This pull request has been inactive for over 90 days, and an Inactive label has been added to it. If this PR remains inactive with no new comments, commits, or updates from main, it will be closed automatically in 14 days. +#

+#

Instructions for writer

+#

+# If you're ready to merge this PR, add the Sign off label to have the PR reviewed and merged by PubOps. +#

+# If you're not ready to merge this PR yet and want to keep it open, click on Update branch at the bottom of this PR to bring it up to date with the main branch. +#

+# To view a list of any open, inactive, PRs assigned to you, click Inactive PRs. +#

+# Thank you! +#

+# PS: Mention @marveldocs-pubops in the comments if you need assistance. +# close-pr-message: > +# This pull request has been inactive for a further 14 days and is now being closed. If you decide to continue working on your changes, that's no problem. Simply select the Reopen pull request button at the bottom of the pull request. +#
+# Thank you! +#
+# PS: Mention @marveldocs-pubops in the comments if you need assistance. \ No newline at end of file From dbe85ddaced0642d46b368bd472ad480d5d79cbe Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 14 Aug 2024 11:48:41 -0700 Subject: [PATCH 23/92] Account for multiple artifacts --- .github/workflows/Shared-ExtractPayload.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-ExtractPayload.yml b/.github/workflows/Shared-ExtractPayload.yml index bd0d7c2f8ef..4cec82312a5 100644 --- a/.github/workflows/Shared-ExtractPayload.yml +++ b/.github/workflows/Shared-ExtractPayload.yml @@ -61,7 +61,8 @@ jobs: Write-Host "Retrieve parent workflow artifacts" $ArtifactData = Invoke-RestMethod -Uri $ArtifactUrl -Headers $GitHubHeaders - $ArtifactDownloadUrl = $ArtifactData.artifacts.archive_download_url + # Using [-1] to get the last element in the array in the event there are mulitple artifacts returned. + $ArtifactDownloadUrl = $ArtifactData.artifacts[-1].archive_download_url Write-Host "Retrieve payload artifact from $ArtifactDownloadUrl and save to $ArtifactFilePath" Invoke-RestMethod -Uri $ArtifactDownloadUrl -Headers $GitHubHeaders -OutFile $ArtifactFilePath From cab6a9d4fe3002891855f417950959c666eb93d2 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:44:15 -0700 Subject: [PATCH 24/92] Create Shared-TierManagement.yml --- .github/workflows/Shared-TierManagement.yml | 446 ++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 .github/workflows/Shared-TierManagement.yml diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml new file mode 100644 index 00000000000..cd714d29eb9 --- /dev/null +++ b/.github/workflows/Shared-TierManagement.yml @@ -0,0 +1,446 @@ +name: Tier management + +permissions: + pull-requests: write + contents: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + EnableWriteSignOff: + required: True + type: string + EnableReadOnlySignoff: + required: True + type: string + secrets: + AccessToken: + required: true + +jobs: + build: + name: Run Script + runs-on: ubuntu-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + EnableWriteSignOff: ${{ inputs.EnableWriteSignOff }} + EnableReadOnlySignoff: ${{ inputs.EnableReadOnlySignoff }} + + run: | + + # Get GitHub data and event + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $GitRequestEvent = $GitHubData.event_name + + $AccessToken = $env:AccessToken + $EnableWriteSignOff = [bool][int]$env:EnableWriteSignOff + $EnableReadOnlySignoff = [bool][int]$env:EnableReadOnlySignoff + + $DefaultBranch = $GitHubData.event.repository.default_branch + $GitHubState = $GitHubData.event.issue.state + $GitHubAction = $GitHubData.event.action + $GitHubSender = $GitHubData.event.sender.login + $GitHubRepoName = $GitHubData.event.repository.name + $RepoLabelUrl = $GitHubData.event.repository.labels_url + $IssueUrl = $GitHubData.event.issue.url + $CommentsUrl = $GitHubData.event.issue.comments_url + $PrIssueNumber = $GitHubData.event.issue.number + $PrUrl = $GitHubData.event.issue.pull_request.url + $CommentUser = $GitHubData.event.comment.user.login + $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$CommentUser/permission" ) + + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", "OfficeDocs") + + $ReadyLabelColor = "13FC1F" + $ReadyLabelDescription = "PR is ready for managed service review" + $ReadyLabel = "Ready for Managed Service" + $ReviewResponseHours = 24 + $SignOffString = "#sign-off" + $SignOffLabelColor = "46ce1c" + $SignOffLabelDescription = "The pull request is ready to be reviewed and merged by PubOps." + $SignOffLabel = "Sign off" + $InvalidTargetBranchString = "Hi @{1}

The target (base) branch of this PR, **{2}**, doesn't support the **{0}** command. The **{0}** command can only be used to merge pull requests into the default branch **{3}** or into branches beginning with **release-**.

Please retarget your PR to either the default branch **{3}** or to a release branch.

If you have questions, please post a message to https://aka.ms/askanadmin." # Variable substitution happens in script. + $Tier3LabelMissingString = "hi @{1},

The **{0}** command can't be used on this PR because the none of the files included in it are classified as Tier3/Selfserve. At least one file needs to be classified as Tier3/Selfserve. Please work with the owner of the article(s) (found in the **ms.author** metadata field in the article) to review and merge this PR.

If you're the owner of the article(s) and your alias is specified in **ms.author**, work with the Magic content team that owns the content set to classify the article(s) as Tier3/Selfserve.

If you have questions, please post a message to https://aka.ms/askanadmin." -f $SignOffString, $CommentUser + $PrSubmittedToReviewString = "Hi @{0},

This PR has been sent to our publishing team for review to ensure content quality and completeness. A member of our publishing team will contact you within {1} business hours.

If you have questions, please post a message to https://aka.ms/askanadmin." -f $CommentUser, $ReviewResponseHours + + $SignOffRegex = "(?m)^\s*$SignOffString\s*$" + + ##################### + ##################### + # Test-RepoLabel + + Function Test-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $RepoUri + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $LabelUri = $RepoUri.Replace("{/name}","/$Name") + + # Check to see if the label we want exists in the repo + Try { + + Write-Host "Checking to see if label $Name exists in repo URL $LabelUri." + + $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $GitHubHeaders -ErrorAction Stop + $LabelFound = $True + + } Catch { + + # OK if label doesn't exist. Just means we need to create it. + $LabelFound = $False + + } + + # Return boolean to calling statement + $LabelFound + + } + + ##################### + ##################### + # New-RepoLabel + + Function New-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $Color, + $Description, + $RepoUri + ) + + # Remove placeholder text from repo URL + $RepoUri = $RepoUri.Replace("{/name}","") + $Result = $Null + + # Construct the JSON statement that will be sent to GitHub as the body of the web request. Include the name of the label, its color, and description. + # Convert hash table to JSON + $Body = @{} + $Body.Add("name", $Name) + $Body.Add("color", $Color) + $Body.Add("description", $description) + $Body = $Body | ConvertTo-Json + + # Try to submit the request to GitHub API to create the label + Try { + + Write-Host "Creating label $Name with color $Color on repo $RepoUri." + + $Result = Invoke-RestMethod -Uri $RepoUri -Headers $GitHubHeaders -Body $Body -Method POST + + } Catch { + + Write-Error "ERROR: Failed to create new label $Name on repo $RepoUri. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Test-PrLabel + Function Test-Prlabel { + + [CmdletBinding()] + param( + + $LabelArray, + $IssueUrl + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $IssueLabelUrl = "$IssueUrl/labels" + $LabelHashTable = @{} + $LabelResults = $Null + + # Get list of labels on issue/PR + Try { + + Write-Host "Getting labels on issue $IssueLabelUrl." + + $LabelResults = Invoke-RestMethod -Uri $IssueLabelUrl -Headers $GitHubHeaders -ErrorAction Stop + + } Catch { + + Write-Error "ERROR: Failed to get list of labels on $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + + } + + ForEach ($Label in $LabelArray) { + + If ($LabelResults -ne $Null) { + + If ($LabelResults.name.Contains($Label)) { + + $LabelHashTable.Add($Label, $True) + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } + + + # Return array of labels on Issue/PR + Return $LabelHashTable + + + } + + ##################### + ##################### + # Set-PrLabel + + Function Set-PrLabel { + + param( + + $IssueUrl, + $LabelName + + ) + + # Construct label URL based on issue or pull request URL + $IssueLabelUrl = "$IssueUrl/labels" + + # Construct JSON statement that will be sent to GitHub as the body of the web request. Includes only the label name. GitHub expects an array even thought it's a single value + # Convert array to JSON + $Body = @() + $Body += $LabelName + $Body = ConvertTo-Json -InputObject $Body + + # Try to submit the request to GitHub API to apply they label to the issue or pull request + Try { + + Write-Host "Setting label $LabelName on URL $IssueLabelUrl." + + $Result = Invoke-RestMethod -Uri $IssueLabelUrl -Body $Body -Headers $GitHubHeaders -Method POST + + + } Catch { + + Write-Error "ERROR: Failed to set label on URL $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Set-PrMessage + + Function Set-PrMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $Message + ) + + $BodyHash = @{} + $BodyHash.body = $Message + $BodyJson = $BodyHash | ConvertTo-Json + $BodyJson + + Try { + + $Result = Invoke-WebRequest -UseBasicParsing -Uri $CommentsUrl -Body $BodyJson -Headers $GitHubHeaders -Method POST -ErrorAction Stop + + $PostCommentSuccess = $True + + } Catch { + + $PostCommentSuccess = $False + + } + + Return $PostCommentSuccess + + } + + + ##################### + ##################### + # Main + + + Write-Host "Repo: $GitHubRepoName" + Write-Host "Sender: $GitHubSender" + Write-Host "Request event: $GitRequestEvent" + Write-Host "GitHub action: $GitHubAction" + Write-Host "GitHub state: $GitHubState" + Write-Host "Default branch: $DefaultBranch" + Write-Host "PR/Issue number: $PrIssueNumber" + Write-Host "Write sign-off enabled: $EnableWriteSignOff" + Write-Host "Read sign-off enabled: $EnableReadOnlySignoff" + + If (($GitRequestEvent -eq "issue_comment") -and (($GitHubAction -eq "created") -or ($GitHubAction -eq "edited"))) { + + Write-Host "Comment added or edited on PR." + + # Get the contents of the comment that was added to the PR + $CommentBody = $GitHubData.event.comment.body + + # Check to see if comment includes $SignOffString by checking if it matches $SignOffRegex + $SignOffFound = $CommentBody -match $SignOffRegex + + Write-Host "Regex result: $SignOffFound." + + If ($SignOffFound) { + + Write-Host "Sign off string `"$SignOffString`" found on PR/Issue #$PrIssueNumber." + + # 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 + + Write-Host "User $CommentUser permission level: $UserPermission." + + # If user has triage or above, add $SignOffLabel. If not, check whether $ReadyLabel should be added. + If (($UserPermission -like "write*") -or ($UserPermission -eq "maintain") -or ($UserPermission -eq "triage") -or ($UserPermission -eq "admin")) { + + # Check to see if sign off for triage and above is enabled for the current repo. + If ($EnableWriteSignOff) { + + Write-Host "User $CommentUser has the permission level: $UserPermission. Setting $SignOffLabel label." + + # Get PR data so we can get the base branch of the PR. Doing this here so we don't need to do unnecessary calls if other criteria fail. + $PrData = Invoke-RestMethod -Method GET -Headers $GitHubHeaders -Uri $PrUrl + $TargetBranch = $PrData.base.ref + + # Check if label exists on the repo. + $LabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel + + If (!$LabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel -Color $SignOffLabelColor -Description $SignOffLabelDescription + + } + + $LabelResultsArray = Test-Prlabel -LabelArray $SignOffLabel -IssueUrl $IssueUrl + + # Only add the label if it doesn't already exist on the PR + If (!$LabelResultsArray.$SignOffLabel) { + + Write-Host "Label $SignOffLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $SignOffLabel + + } + + } Else { + + Write-Host "Sign off for users with triage permission or higher is disabled on $GitHubRepoName." + + } + + } Else { + + # Check to see if sign off for read only is enabled for the current repo. + If ($EnableReadOnlySignoff) { + + # Get PR data so we can get the base branch of the PR. Doing this here so we don't need to do unnecessary calls if other criteria fail. + $PrData = Invoke-RestMethod -Method GET -Headers $GitHubHeaders -Uri $PrUrl + $TargetBranch = $PrData.base.ref + + # Make sure the PR targets branches we care about. + If (($TargetBranch -eq $DefaultBranch) -or ($TargetBranch -like "release-*")) { + + Write-Host "Pull request target branch $TargetBranch matches the default branch $DefaultBranch." + + # Check if label exists on the repo. + $LabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $ReadyLabel + + If (!$LabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $ReadyLabel -Color $ReadyLabelColor -Description $ReadyLabelDescription + + } + + # Check to see whether $ReadyLabel or Tier3/Selfserve labels exist on the PR. Hashtable that's returned includes the label name and a boolean + # that indicates whether the labels exist on the PR. + # The reason this isn't part of the above repo-level check is because the all the PR-level labels can be retrieved with a single call to GitHub. + # If we included the PR-level checks in the same block as the repo-level checks, we'd be requesting the PR-level repos times. + # Don't confuse with the previous ForEach which is a repo-level check. + $LabelResultsArray = Test-Prlabel -LabelArray $ReadyLabel, "Tier3", "Selfserve" -IssueUrl $IssueUrl + + Write-Host "Number of items in LabelResultsArray: $($LabelResultsArray.Count)." + + # Only add $ReadyLabel if the labels "Tier3" or "Selfserve" exist on the PR + If (($LabelResultsArray.Tier3) -or ($LabelResultsArray.Selfserve)) { + + Write-Host "Tier3/Selfserve label exists on $IssueUrl. OK to add $ReadyLabel label." + + # Only add the label if it doesn't already exist on the PR + If (!$LabelResultsArray.$ReadyLabel) { + + Write-Host "Label $ReadyLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR and post a message to the commenter + Set-PrLabel -IssueUrl $IssueUrl -LabelName $ReadyLabel + Set-PrMessage -Message $PrSubmittedToReviewString + + } + + } Else { + + Write-Host "Tier3/Selfserve label doesn't exist on $IssueUrl. Not adding $ReadyLabel label." + + # Post a message to the commenter telling them they don't have the right permissions to use the command. + Set-PrMessage -Message $Tier3LabelMissingString + + } + + } Else { + + Write-Host "Pull request target branch $TargetBranch doesn't match the default branch $DefaultBranch." + + # Need to do the string variable replacement here because $TargetBranch doesn't exist until the PR data call a few lines above. + $InvalidTargetBranchString = $InvalidTargetBranchString -f $SignOffString, $CommentUser, $TargetBranch, $DefaultBranch + + # Post a message to the commenter telling them they're targeting a branch other than $DefaultBranch or "release-*". + Set-PrMessage -Message $InvalidTargetBranchString + + } # Target branch check + + } Else { + + Write-Host "Sign off for users with read-only permission is disabled on $GitHubRepoName." + + } + + } # Permission check (triage/write/maintain/admin vs readonly) + + } # Sign off string check + + } # PR event and action check + + From ac02b40ff77e6a95f7ba0035de54680f8a4ba2f2 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:00:56 -0700 Subject: [PATCH 25/92] Add files to protected file list --- .github/workflows/Shared-ProtectedFiles.yml | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index fdfb765a7d5..a35b6909422 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -34,16 +34,25 @@ jobs: "ThirdPartyNotices", ".acrolinx-config.edn", ".gitattributes", - "ProtectedFiles.yml", "AutoLabelAssign.yml", - "LiveMergeCheck.yml", - "PrFileCount.yml", + "AutoLabelMsftContributor.yml", "BackgroundTasks.yml", - "Shared-ProtectedFiles.yml", + "compare-live-main.yml", + "LiveMergeCheck.yml", + "M365Endpoints.yml", + "PrFileCount.yml", + "ProtectedFiles.yml", + "Stale.yml", + "TierManagement.yml", + "workflow-status-report.yml", "Shared-AutoLabelAssign.yml", + "Shared-AutoLabelMsftContributor.yml", + "Shared-ExtractPayload.yml", "Shared-LiveMergeCheck.yml", "Shared-PrFileCount.yml", - "Shared-ExtractPayload.yml" + "Shared-ProtectedFiles.yml", + "Shared-Stale.yml", + "Shared-TierManagement.yml" ]' ApproverList: '["user1"]' From cf014e90c15d40e1bd4cdac39b9855bcdedc93ab Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:10:51 -0700 Subject: [PATCH 26/92] Add exception handling for Set-PrAssignee --- .github/workflows/Shared-AutoLabelAssign.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 4147ee3b359..ea9db84ffb3 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -574,7 +574,15 @@ jobs: # Don't add assignments to PRs in excluded branches listed in $ExcludedBranchList If (!$ExcludedBranchList.Contains($TargetBranch)) { - Set-PrAssignee -IssueUrl $IssueUrl -GitHubAssignees $FormattedMetadata.AuthorMetadata + Try { + + Set-PrAssignee -IssueUrl $IssueUrl -GitHubAssignees $FormattedMetadata.AuthorMetadata + + } Catch { + + Write-Host "Error setting PR assignee. GitHub may return an `"Unprocessable entity (422)`" error periodically. This can be ignored. Error: $($Error[0])" + + } } Else { From 4d10094f36a1261f21822aa08e0df281d26622b1 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 20 Sep 2024 14:50:49 -0700 Subject: [PATCH 27/92] Add start date to stale.yml --- .github/workflows/Shared-Stale.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index 487fd8e0f71..c9d4574be87 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -32,6 +32,7 @@ jobs: days-before-issue-close: -1 days-before-pr-stale: 90 days-before-pr-close: 14 + start-date: '2024-05-01T00:00:00Z' stale-pr-label: Inactive close-pr-label: Auto Closed exempt-pr-labels: Keep open @@ -96,4 +97,4 @@ jobs: #
# Thank you! #
-# PS: Mention @marveldocs-pubops in the comments if you need assistance. \ No newline at end of file +# PS: Mention @marveldocs-pubops in the comments if you need assistance. From 99d5c6e57962a06ab3829c95b7537b9a2e5dd415 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 23 Sep 2024 12:02:44 -0700 Subject: [PATCH 28/92] update stale and close messages --- .github/workflows/Shared-Stale.yml | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index c9d4574be87..0b39eee082d 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -46,6 +46,8 @@ jobs: If you're ready to merge this PR, add the Sign off label to have the PR reviewed and merged by PubOps.

If you're not ready to merge this PR yet and want to keep it open, click on Update branch at the bottom of this PR to bring it up to date with the main branch. +

+ If the changes in this PR are no longer needed, click on Close at the bottom of the page. Please also delete the branch by clicking on Delete branch.

To view a list of any open, inactive, PRs assigned to you, click Inactive PRs.

@@ -53,10 +55,19 @@ jobs:

PS: Mention @marveldocs-pubops in the comments if you need assistance. close-pr-message: > - This pull request has been inactive for a further 14 days and is now being closed. If you decide to continue working on your changes, that's no problem. Simply select the Reopen pull request button at the bottom of the pull request. -
- Thank you! -
+

Closing inactive PR

+ This pull request has been inactive for a further 14 days and is now being closed. +

+

Instructions for writer

+

+ If you decide to continue working on your changes, that's no problem. Simply select the Reopen pull request button at the bottom of the pull request. +

+ If the changes in this PR are no longer needed, please also delete the branch by clicking on Delete branch. +

+ Warning This branch may be subject to automatic deletion at a future date. Changes in the deleted branch will be lost. +

+ Thank you! +

PS: Mention @marveldocs-pubops in the comments if you need assistance. # stale-public: From e9a85c31e8b5bf5cda280c0c5bf5502c54d8c895 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 1 Oct 2024 14:10:31 -0700 Subject: [PATCH 29/92] Add month selection step --- .github/workflows/Shared-Stale.yml | 46 +++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index 0b39eee082d..0eb293f1209 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -23,7 +23,39 @@ jobs: runs-on: ubuntu-latest if: inputs.RepoVisibility == 'private' steps: + + - name: Get selected month + id: get-month + shell: pwsh + run: | + + # Define the starting week date and starting month date + $StartingWeekDate = Get-Date "2024-11-04" + $StartingMonthDate = Get-Date "2024-05-01" + + # Get the current date + $CurrentDate = Get-Date + + # Calculate the number of weeks passed since the starting week date. If the current date is before the starting week date, the number of weeks passed is 0 + $DaysDifference = ($CurrentDate - $StartingWeekDate).Days + $WeeksPassed = [math]::Max(0, [int]($DaysDifference / 7)) + + # Calculate the month selected by subtracting the number of weeks passed from the starting month date + $MonthSelectedDate = $StartingMonthDate.AddMonths(-$WeeksPassed) + + # Format the output date + $FriendlyFormattedMonthSelected = $MonthSelectedDate.ToString("MMMM d, yyyy") + $WorkflowFormattedMonthSelected = $MonthSelectedDate.ToString("yyyy-MM-ddT00:00:00Z") + + # Output the result + Write-Host "For the current date $($CurrentDate.ToString('MMMM d, yyyy')), the month selected should be $FriendlyFormattedMonthSelected." + Write-Host "Workflow-formatted date: $WorkflowFormattedMonthSelected" + + echo "SelectedMonth=$WorkflowFormattedMonthSelected" >> $env:GITHUB_OUTPUT + + - uses: actions/stale@v9 + if: endsWith(steps.get-month.outputs.SelectedMonth, 'Z') # Make sure there's a date before running otherwise the stale action will run as if start-date doesn't exist. with: repo-token: ${{ secrets.AccessToken }} debug-only: ${{ inputs.RunDebug }} @@ -32,7 +64,7 @@ jobs: days-before-issue-close: -1 days-before-pr-stale: 90 days-before-pr-close: 14 - start-date: '2024-05-01T00:00:00Z' + start-date: ${{ steps.get-month.outputs.SelectedMonth }} stale-pr-label: Inactive close-pr-label: Auto Closed exempt-pr-labels: Keep open @@ -70,6 +102,18 @@ jobs:

PS: Mention @marveldocs-pubops in the comments if you need assistance. + - name: Error on missing date + if: endsWith(steps.get-month.outputs.SelectedMonth, 'Z') == false + shell: pwsh + env: + SelectedMonth: ${{ steps.get-month.outputs.SelectedMonth}} + run: | + + $SelectedMonth = $env:SelectedMonth + Write-Error "The value in SelectedMonth, `"$SelectedMonth`", isn't a valid datetime. The stale action wasn't run." + exit 1 + + # stale-public: # name: Stale check - public repos # runs-on: ubuntu-latest From d4fbade5351870cfb17baa12ec0c611a4a031a8b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 4 Nov 2024 11:12:14 -0800 Subject: [PATCH 30/92] Update starting week date --- .github/workflows/Shared-Stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index 0eb293f1209..953cfbdd9bf 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -30,7 +30,7 @@ jobs: run: | # Define the starting week date and starting month date - $StartingWeekDate = Get-Date "2024-11-04" + $StartingWeekDate = Get-Date "2024-10-31" $StartingMonthDate = Get-Date "2024-05-01" # Get the current date From 45679b402b71a97e3ca4f370efed183bfdc53647 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 20 Nov 2024 16:35:35 -0800 Subject: [PATCH 31/92] Add BuildValidation workflow --- .github/workflows/Shared-BuildValidation.yml | 164 +++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 .github/workflows/Shared-BuildValidation.yml diff --git a/.github/workflows/Shared-BuildValidation.yml b/.github/workflows/Shared-BuildValidation.yml new file mode 100644 index 00000000000..e3a55cc9903 --- /dev/null +++ b/.github/workflows/Shared-BuildValidation.yml @@ -0,0 +1,164 @@ +name: Build validation (shared) + +permissions: + pull-requests: write + statuses: write + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + secrets: + AccessToken: + required: true + +jobs: + build: + name: Run Script + runs-on: ubuntu-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + BuildUser: "learn-build-service-prod[bot]" + + run: | + + # Get GitHub data and event + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $GitRequestEvent = $GitHubData.event_name + $BuildUser = $env:BuildUser + + $AccessToken = $env:AccessToken + + $DefaultBranch = $GitHubData.event.repository.default_branch + $GitHubState = $GitHubData.event.issue.state + $GitHubAction = $GitHubData.event.action + $GitHubSender = $GitHubData.event.sender.login + $GitHubRepoName = $GitHubData.event.repository.name + $CommentUser = $GitHubData.event.comment.user.login + $PrIssueNumber = $GitHubData.event.issue.number + $PrUrl = $GitHubData.event.issue.pull_request.url + + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", "OfficeDocs") + + $StatusUrl = "https://review.learn.microsoft.com/en-us/office-authoring-guide/pr-criteria?branch=main" + $StatusCheckName = "PR has no warnings or errors" + $Status = @{} + $Status.Add("context", $StatusCheckName) + $Status.Add("target_url", $StatusUrl) + + $ValidationRegex = "(?m)\s*(Validation status: )([Pp]assed|[Ss]uggestion[s]?|[Ww]arning[s]?|[Ee]rror[s]?)\s*$" + + Write-Host "Repo: $GitHubRepoName" + Write-Host "Sender: $GitHubSender" + Write-Host "Request event: $GitRequestEvent" + Write-Host "GitHub action: $GitHubAction" + Write-Host "GitHub state: $GitHubState" + Write-Host "Default branch: $DefaultBranch" + Write-Host "PR number: $PrIssueNumber" + + # Make the job summary section show up so the job always looks consistent. + echo "" >> $env:GITHUB_STEP_SUMMARY + + If (($GitRequestEvent -eq "issue_comment") -and (($GitHubAction -eq "created"))) { + + Write-Host "Comment added on PR." + + If ($CommentUser -eq $BuildUser) { + + Write-Host "Comment from $BuildUser. Processing." + + # Get the contents of the comment that was added to the PR + $CommentBody = $GitHubData.event.comment.body + + # Check to see if comment contains validation status + $StatusFound = $CommentBody -match $ValidationRegex + + If ($StatusFound) { + + Write-Host "Regex result: $StatusFound." + + $ValidationResult = $Matches[2] + + Write-Host "Validation status: $ValidationResult" + + $PrData = Invoke-RestMethod -Method GET -ContentType "application/json" -Headers $GitHubHeaders -Uri $PrUrl -ErrorAction Stop + $StatusUrl = $PrData.statuses_url + $PrHtmlUrl = $PrData.html_url + + Write-Host "PR status url: $StatusUrl" + + If (($ValidationResult -like "*error*") -or ($ValidationResult -like "*warning*")) { + + # Populates the job summary if an PR validation has an error or warning. + echo "# Pull request validation error" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "Build validation completed with a status of `"$ValidationResult`" in PR: $PrHtmlUrl. Builds must be free of errors and warnings before PRs can be merged." >> $env:GITHUB_STEP_SUMMARY + + $Status.state = "failure" + $Status.description = "Validation status: $ValidationResult" + + } Else { + + write-host "Build validation completed with a status of `"$ValidationResult`". Allowing merge." + + $Status.state = "success" + $Status.description = "Validation status: $ValidationResult" + + } + + $StatusJson = $Status | ConvertTo-Json + $SuccessfulPost = $False + $RetryCount = 0 + + Do { + + Try { + + # Send POST request to GitHub + Invoke-RestMethod -Headers $GitHubHeaders -Uri $StatusUrl -Method POST -Body $StatusJson -ErrorAction Stop + $SuccessfulPost = $True + + } Catch { + + # If the request fails for any reason, retry it after a delay, up to six times. + $RetryCount++ + Start-Sleep 1 + + } + + } Until (($SuccessfulPost) -or ($RetryCount -gt 5)) + + + If (($ValidationResult -like "*error*") -or ($ValidationResult -like "*warning*")) { + + # Force the workflow to fail so the validation failure can be tracked in Actions. + + Throw "Build validation completed with a status of `"$ValidationResult`" in PR: $PrHtmlUrl. Builds must be free of errors and warnings before PRs can be merged." + + } + + } Else { + + Write-Host "Comment was from $BuildUser but no status was found." + + } + + } Else { + + Write-Host "Comment not from $BuildUser. Exiting." + + } + + } Else { + + Write-Host "Not an added comment on PR." + + } # PR event and action check \ No newline at end of file From b90a26e4a260d0cb7a98b704fcdf0d1fdcf0f125 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 22 Nov 2024 14:41:14 -0800 Subject: [PATCH 32/92] Update Shared-BuildValidation.yml --- .github/workflows/Shared-BuildValidation.yml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Shared-BuildValidation.yml b/.github/workflows/Shared-BuildValidation.yml index e3a55cc9903..59504e4720e 100644 --- a/.github/workflows/Shared-BuildValidation.yml +++ b/.github/workflows/Shared-BuildValidation.yml @@ -102,16 +102,26 @@ jobs: echo "" >> $env:GITHUB_STEP_SUMMARY echo "Build validation completed with a status of `"$ValidationResult`" in PR: $PrHtmlUrl. Builds must be free of errors and warnings before PRs can be merged." >> $env:GITHUB_STEP_SUMMARY + # Capitalize first letter of result. + $CapValidationResult = $ValidationResult.Substring(0,1).ToUpper() + $ValidationResult.Substring(1) + $Status.state = "failure" - $Status.description = "Validation status: $ValidationResult" + $Status.description = "Blocking merge. $CapValidationResult must be resolved before merge." - } Else { + } ElseIf ($ValidationResult -like "*suggestion*") { write-host "Build validation completed with a status of `"$ValidationResult`". Allowing merge." $Status.state = "success" - $Status.description = "Validation status: $ValidationResult" + $Status.description = "Allowing merge. Please resolve $ValidationResult." + + } Else { + + write-host "Build validation completed with a status of `"$ValidationResult`". Allowing merge." + $Status.state = "success" + $Status.description = "Allowing merge." + } $StatusJson = $Status | ConvertTo-Json @@ -161,4 +171,4 @@ jobs: Write-Host "Not an added comment on PR." - } # PR event and action check \ No newline at end of file + } # PR event and action check From 6570e57e55644139c9365fc0fea4c9a48236c123 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 28 Jan 2025 11:30:58 -0800 Subject: [PATCH 33/92] Add build validation workflows to protected file list. --- .github/workflows/Shared-ProtectedFiles.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index a35b6909422..3d05bd6d79e 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -37,6 +37,7 @@ jobs: "AutoLabelAssign.yml", "AutoLabelMsftContributor.yml", "BackgroundTasks.yml", + "BuildValidation.yml", "compare-live-main.yml", "LiveMergeCheck.yml", "M365Endpoints.yml", @@ -47,6 +48,7 @@ jobs: "workflow-status-report.yml", "Shared-AutoLabelAssign.yml", "Shared-AutoLabelMsftContributor.yml", + "Shared-BuildValidation.yml", "Shared-ExtractPayload.yml", "Shared-LiveMergeCheck.yml", "Shared-PrFileCount.yml", From c37e4e4f43896d67dba95c348500229d6193b65b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:47:42 -0800 Subject: [PATCH 34/92] Add throw to fail check --- .github/workflows/Shared-LiveMergeCheck.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/Shared-LiveMergeCheck.yml b/.github/workflows/Shared-LiveMergeCheck.yml index adca0412602..649c7e1871a 100644 --- a/.github/workflows/Shared-LiveMergeCheck.yml +++ b/.github/workflows/Shared-LiveMergeCheck.yml @@ -170,6 +170,7 @@ jobs: Write-Host "The branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch. Click Summary in the left pane for more information." + Throw "The branch $OriginRepo/$OriginBranch isn't allowed to merge into $PublishBranch. Click Summary in the left pane for more information." } # OriginBranch From 4378c6c8f8fd296aa2d3fff5b68dd2a507c8d572 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 10 Mar 2025 14:20:57 -0700 Subject: [PATCH 35/92] switch from PAT to GitHub app --- .../Shared-AutoLabelMsftContributor.yml | 149 +++++++++++++++++- 1 file changed, 142 insertions(+), 7 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelMsftContributor.yml b/.github/workflows/Shared-AutoLabelMsftContributor.yml index cc8bf86cf6d..db9e3215894 100644 --- a/.github/workflows/Shared-AutoLabelMsftContributor.yml +++ b/.github/workflows/Shared-AutoLabelMsftContributor.yml @@ -13,7 +13,9 @@ on: secrets: AccessToken: required: true - TeamReadAccessToken: + ClientId: + required: true + PrivateKey: required: true jobs: build: @@ -25,7 +27,8 @@ jobs: env: PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} - TeamReadAccessToken: ${{ secrets.TeamReadAccessToken }} + ClientId: ${{ secrets.ClientId }} + PrivateKey: ${{ secrets.PrivateKey }} run: | @@ -33,9 +36,10 @@ jobs: # Get payload data from GitHub. $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 - $AccessToken = $env:AccessToken - $TeamReadAccessToken = $env:TeamReadAccessToken $GitRequestEvent = $GitHubData.event_name + $AccessToken = $env:AccessToken + $ClientId = $env:ClientId + $PrivateKey = $env:PrivateKey $RepoName = $GitHubData.event.repository.name $RepoUrl = $GitHubData.event.repository.url @@ -44,6 +48,134 @@ jobs: # 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/" + ########################################### + ########################################### + # GitHub app/installation token block start + ########################################### + ########################################### + + Function ConvertTo-JwtBase64 { + param( + [Parameter(Mandatory = $True)] + [string]$RawJson + ) + + # Convert UTF-8 bytes to Base64, then make it URL-safe for JWT + $Encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($RawJson)) + Return $Encoded.TrimEnd('=') -replace '\+','-' -replace '/','_' + } + + Function New-GitHubAppJWT { + param( + [Parameter(Mandatory = $True)] + [string]$ClientId, + + [Parameter(Mandatory = $True)] + [string]$PrivateKey, + + [int]$ExpiresInMinutes = 10 + ) + + Write-Host "Create JWT" + + # Build the header + $Header = @{ + alg = "RS256" + typ = "JWT" + } | ConvertTo-Json -Compress + + $HeaderEncoded = ConvertTo-JwtBase64 -RawJson $Header + + # Build the payload + $Now = [System.DateTimeOffset]::UtcNow + $Payload = @{ + iat = $now.AddSeconds(-10).ToUnixTimeSeconds() + exp = $now.AddMinutes($ExpiresInMinutes).ToUnixTimeSeconds() + iss = $ClientId + } | ConvertTo-Json -Compress + + $PayloadEncoded = ConvertTo-JwtBase64 -RawJson $Payload + + # Combine and sign + $Rsa = [System.Security.Cryptography.RSA]::Create() + $Rsa.ImportFromPem($PrivateKey) + + $Combined = "$HeaderEncoded.$PayloadEncoded" + $SignatureBytes = $Rsa.SignData( + [System.Text.Encoding]::UTF8.GetBytes($Combined), + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + + $SignatureEncoded = [Convert]::ToBase64String($SignatureBytes).TrimEnd('=') -replace '\+','-' -replace '/','_' + + Return "$HeaderEncoded.$PayloadEncoded.$SignatureEncoded" + } + + Function Get-GitHubAppInstallationToken { + param( + [Parameter(Mandatory = $true)] + [string]$ClientId, + + [Parameter(Mandatory = $true)] + [string]$PrivateKey, + + [Parameter(Mandatory = $true)] + [string]$Organization, + + [int]$TokenTTLMinutes = 10 + ) + + # Create the JWT + $Jwt = New-GitHubAppJWT -ClientId $ClientId -PrivateKey $PrivateKey -ExpiresInMinutes $TokenTTLMinutes + + # Prepare headers + $JwtHeaders = @{ + Authorization = "Bearer $Jwt" + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version"= "2022-11-28" + } + + Write-Host "Request org installation ID" + + # 1) Retrieve the installation ID for the org + Try { + $InstallationInfo = Invoke-RestMethod ` + -Uri "https://api.github.com/orgs/$Organization/installation" ` + -Headers $JwtHeaders ` + -ErrorAction Stop + + $InstallationId = $InstallationInfo.id + } + Catch { + Write-Error "Failed to get installation ID from GitHub. $_" + Return $Null + } + + Write-Host "Get installation token" + + # 2) Use the installation ID to request an installation token + Try { + $TokenResponse = Invoke-RestMethod ` + -Uri "https://api.github.com/app/installations/$InstallationId/access_tokens" ` + -Headers $JwtHeaders ` + -Method Post ` + -ErrorAction Stop + + Return $TokenResponse.token + } + Catch { + Write-Error "Failed to get access token from GitHub. $_" + Return $Null + } + } + + ######################################### + ######################################### + # GitHub app/installation token block end + ######################################### + ######################################### + ##################### ##################### # Test-Label @@ -180,10 +312,13 @@ jobs: $GitHubHeaders.Add("User-Agent", "OfficeDocs") $GitHubHeaders.Add("Accept","application/vnd.github.mercy-preview+json") - # Create team read GitHub HTTP authentication header. Need a token that has access to org scope. GITHUB_TOKEN that - # is used to populate $AccessToken doesn't have access to org scope so using a custom fine-grained token with limited member read scope. + # Create team read GitHub HTTP authentication header. Need a token that has access to org scope. Get-GitHubAppInstallationToken + # requests an installation token from our GitHub app so that we can authenticate. GITHUB_TOKEN that is used to populate $AccessToken + # doesn't have access to org scope so using our GitHub app's access. + $GitHubAccessToken = Get-GitHubAppInstallationToken -ClientId $ClientId -PrivateKey $PrivateKey -Organization MicrosoftDocs -TokenTTLMinutes 10 + $TeamReadGitHubHeaders = @{} - $TeamReadGitHubHeaders.Add("Authorization","token $($TeamReadAccessToken)") + $TeamReadGitHubHeaders.Add("Authorization","token $($GitHubAccessToken)") $TeamReadGitHubHeaders.Add("User-Agent", "OfficeDocs") $TeamReadGitHubHeaders.Add("Accept","application/vnd.github.mercy-preview+json") From 933196c3bb7ebe615cd8f5ce0096d4049b3dd08b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:35:46 -0700 Subject: [PATCH 36/92] Comment out start date code --- .github/workflows/Shared-Stale.yml | 64 +++++++++++++++--------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index 953cfbdd9bf..ffa7fe2257d 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -24,38 +24,38 @@ jobs: if: inputs.RepoVisibility == 'private' steps: - - name: Get selected month - id: get-month - shell: pwsh - run: | + # - name: Get selected month + # id: get-month + # shell: pwsh + # run: | - # Define the starting week date and starting month date - $StartingWeekDate = Get-Date "2024-10-31" - $StartingMonthDate = Get-Date "2024-05-01" + # # Define the starting week date and starting month date + # $StartingWeekDate = Get-Date "2024-10-31" + # $StartingMonthDate = Get-Date "2024-05-01" - # Get the current date - $CurrentDate = Get-Date + # # Get the current date + # $CurrentDate = Get-Date - # Calculate the number of weeks passed since the starting week date. If the current date is before the starting week date, the number of weeks passed is 0 - $DaysDifference = ($CurrentDate - $StartingWeekDate).Days - $WeeksPassed = [math]::Max(0, [int]($DaysDifference / 7)) + # # Calculate the number of weeks passed since the starting week date. If the current date is before the starting week date, the number of weeks passed is 0 + # $DaysDifference = ($CurrentDate - $StartingWeekDate).Days + # $WeeksPassed = [math]::Max(0, [int]($DaysDifference / 7)) - # Calculate the month selected by subtracting the number of weeks passed from the starting month date - $MonthSelectedDate = $StartingMonthDate.AddMonths(-$WeeksPassed) + # # Calculate the month selected by subtracting the number of weeks passed from the starting month date + # $MonthSelectedDate = $StartingMonthDate.AddMonths(-$WeeksPassed) - # Format the output date - $FriendlyFormattedMonthSelected = $MonthSelectedDate.ToString("MMMM d, yyyy") - $WorkflowFormattedMonthSelected = $MonthSelectedDate.ToString("yyyy-MM-ddT00:00:00Z") + # # Format the output date + # $FriendlyFormattedMonthSelected = $MonthSelectedDate.ToString("MMMM d, yyyy") + # $WorkflowFormattedMonthSelected = $MonthSelectedDate.ToString("yyyy-MM-ddT00:00:00Z") - # Output the result - Write-Host "For the current date $($CurrentDate.ToString('MMMM d, yyyy')), the month selected should be $FriendlyFormattedMonthSelected." - Write-Host "Workflow-formatted date: $WorkflowFormattedMonthSelected" + # # Output the result + # Write-Host "For the current date $($CurrentDate.ToString('MMMM d, yyyy')), the month selected should be $FriendlyFormattedMonthSelected." + # Write-Host "Workflow-formatted date: $WorkflowFormattedMonthSelected" - echo "SelectedMonth=$WorkflowFormattedMonthSelected" >> $env:GITHUB_OUTPUT + # echo "SelectedMonth=$WorkflowFormattedMonthSelected" >> $env:GITHUB_OUTPUT - uses: actions/stale@v9 - if: endsWith(steps.get-month.outputs.SelectedMonth, 'Z') # Make sure there's a date before running otherwise the stale action will run as if start-date doesn't exist. +# if: endsWith(steps.get-month.outputs.SelectedMonth, 'Z') # Make sure there's a date before running otherwise the stale action will run as if start-date doesn't exist. with: repo-token: ${{ secrets.AccessToken }} debug-only: ${{ inputs.RunDebug }} @@ -64,7 +64,7 @@ jobs: days-before-issue-close: -1 days-before-pr-stale: 90 days-before-pr-close: 14 - start-date: ${{ steps.get-month.outputs.SelectedMonth }} +# start-date: ${{ steps.get-month.outputs.SelectedMonth }} stale-pr-label: Inactive close-pr-label: Auto Closed exempt-pr-labels: Keep open @@ -102,16 +102,16 @@ jobs:

PS: Mention @marveldocs-pubops in the comments if you need assistance. - - name: Error on missing date - if: endsWith(steps.get-month.outputs.SelectedMonth, 'Z') == false - shell: pwsh - env: - SelectedMonth: ${{ steps.get-month.outputs.SelectedMonth}} - run: | +# - name: Error on missing date +# if: endsWith(steps.get-month.outputs.SelectedMonth, 'Z') == false +# shell: pwsh +# env: +# SelectedMonth: ${{ steps.get-month.outputs.SelectedMonth}} +# run: | - $SelectedMonth = $env:SelectedMonth - Write-Error "The value in SelectedMonth, `"$SelectedMonth`", isn't a valid datetime. The stale action wasn't run." - exit 1 +# $SelectedMonth = $env:SelectedMonth +# Write-Error "The value in SelectedMonth, `"$SelectedMonth`", isn't a valid datetime. The stale action wasn't run." +# exit 1 # stale-public: From c680561f88175571462b7d4bf6fde781ae5d6609 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 13 Mar 2025 10:41:16 -0700 Subject: [PATCH 37/92] Update Inactive message --- .github/workflows/Shared-Stale.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index ffa7fe2257d..d1257dad99d 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -77,7 +77,7 @@ jobs:

If you're ready to merge this PR, add the Sign off label to have the PR reviewed and merged by PubOps.

- If you're not ready to merge this PR yet and want to keep it open, click on Update branch at the bottom of this PR to bring it up to date with the main branch. + If you're not ready to merge this PR yet and want to keep it open, click on Update branch at the bottom of this PR to bring it up to date with the main branch. The Inactive label will be removed within six hours.

If the changes in this PR are no longer needed, click on Close at the bottom of the page. Please also delete the branch by clicking on Delete branch.

From b3dcdbe8047e6795617ce8ef68ad99cc6d96d0c5 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 13 Mar 2025 14:24:16 -0700 Subject: [PATCH 38/92] Create Shared-StaleBranch.yml --- .github/workflows/Shared-StaleBranch.yml | 277 +++++++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 .github/workflows/Shared-StaleBranch.yml diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml new file mode 100644 index 00000000000..acb80ddf7bb --- /dev/null +++ b/.github/workflows/Shared-StaleBranch.yml @@ -0,0 +1,277 @@ +name: (Scheduled) Stale branch removal + +permissions: + contents: write + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + RepoBranchSkipList: + required: true + type: string + ReportOnly: + required: true + type: boolean + secrets: + AccessToken: + required: true + +jobs: + + stale-branch: + name: Removal stale branches + runs-on: ubuntu-latest + steps: + - name: Process branches + shell: pwsh + env: + DefaultSkipBranchList: '[ + "live", + "main", + "repo_sync_working_branch" + ]' + RepoBranchSkipList: ${{ inputs.RepoBranchSkipList }} + PayloadJson: ${{ inputs.PayloadJson }} + ReportOnly: ${{ inputs.ReportOnly }} + AccessToken: ${{ secrets.AccessToken }} + + run: | + + # Get GitHub data + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $AccessToken = $env:AccessToken + $DefaultSkipBranchList = $env:DefaultSkipBranchList | ConvertFrom-Json + $RepoBranchSkipList = $env:RepoBranchSkipList | ConvertFrom-Json + $ReportOnly = [System.Convert]::ToBoolean($env:ReportOnly) + + Function Get-TotalElements { + param( + [Parameter(Mandatory=$true)] + [Object[]]$Array + ) + + $count = 0 + foreach ($item in $Array) { + if ($item -is [System.Array]) { + # Add the number of elements in the nested array + $count += $item.Count + } else { + # Otherwise count it as one element + $count++ + } + } + return $count + } + + + If ($ReportOnly) { + + Write-Host "`n`nRUNNING IN REPORTING MODE`n`n" + + $ReportOnlyString = "REPORT ONLY -" + + } Else { + + $ReportOnlyString = $Null + + } + + Write-Host "Default branch skip list: $DefaultSkipBranchList" + Write-Host "Repo branch skip list: $RepoBranchSkipList" + + $SkipBranchList = $DefaultSkipBranchList + $RepoBranchSkipList | Select-Object -Unique + + # WARNING - Setting $MaxCommitsAhead to anything other than 0 means that the workflow will delete branches with changes not in default branch. + # !!! > 0 WILL RESULT IN DATA LOSS !!! + $MaxCommitsAhead = 0 # This is the maximum number of commits a branch can be ahead of default branch. + $AllowDataLoss = $False # This flag must be set to $True to allow branches with commits not in default branch to be deleted. + # !!! > 0 WILL RESULT IN DATA LOSS !!! + + $MaxDaysBehind = 90 + $DateLimit = (Get-Date).AddDays(-$MaxDaysBehind) + + # Create github HTTP authentication header + $UserAgent = "officedocs" + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", $UserAgent) + + $RepoUrl = $GitHubData.event.repository.url + $RepoData = Invoke-RestMethod -Headers $GitHubHeaders -Uri $RepoUrl -Method GET + + $BranchesUrl = $RepoData.branches_url.Replace("{/branch}", "?per_page=100") + + $DefaultBranch = $RepoData.default_branch + $CompareUrl = $RepoData.compare_url.Replace("{base}...{head}", "$DefaultBranch...") + + $Branches = Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchesUrl -Method GET -FollowRelLink -MaximumFollowRelLink 50 -ResponseHeadersVariable ResponseHeaders + + $StartBranchCount = Get-TotalElements -Array $Branches + + $ReportBranchList = @() + $DeleteBranchCount = 0 + $WatchListCount = 0 + $DataLossCount = 0 + $DataLossBlockedCount = 0 + + # Block the script from running if Ahead by is greater than 0 when maximum branch age is less than 90. Doing this could result in loss of data that may still be + # wanted. Do not change the values unless you accept the high risk of unwanted data loss. + If (($MaxCommitsAhead -gt 0) -and ($MaxDaysBehind -lt 90)) { + + Throw "ERROR: You can't set MaxCommitsAhead above 0 when MaxDaysBehind is set to less than 90. The potential for data loss is too high." + + } + + # Block the script from running if maximum age is less than 30 days. + If ($MaxDaysBehind -lt 30) { + + Throw "ERROR: You can't set MaxDaysBehind to less than 30. Branch churn too high." + + } + + ForEach ($Page in $Branches) { + + ForEach ($Branch in $Page) { + + $AheadBy = $BehindBy = $LastCommitDate = $CompareData = $Null + $ProtectedBranch = $True + + $BranchName = $Branch.name + $CommitsUrl = $RepoData.commits_url.Replace("{/sha}", "?sha=$BranchName&per_page=1&page=1") + $BranchDeleteUrl = $RepoData.url + "/git/refs/heads/$BranchName" + + Write-Host "`nBranch name: $BranchName" + + If ($SkipBranchList -contains $BranchName) { + + Write-Host " Skipped. Branch is on the branch skip list." + + continue + + } + + $ProtectedBranch = $Branch.protected + + Write-Host " Protected: $ProtectedBranch." + + If ($ProtectedBranch) { + + Write-Host " Skipped. Branch is protected." + + continue + + } + + $LastCommitDate = (Invoke-RestMethod -Headers $GitHubHeaders -uri $CommitsUrl).commit.committer.date + + Write-Host " Last commit date: $LastCommitDate." + + If ($LastCommitDate -ge $DateLimit) { + + Write-Host " Skipped. Last commit date is after $DateLimit." + + continue + + } + + $CompareData = Invoke-RestMethod -Headers $GitHubHeaders -Uri "$CompareUrl$BranchName" -Method GET -ResponseHeadersVariable ResponseHeaders + + $BehindBy = $CompareData.behind_by + $AheadBy = $CompareData.ahead_by + + Write-Host " Ahead of $DefaultBranch by: $AheadBy `n Behind by: $BehindBy." + + If ($AheadBy -gt $MaxCommitsAhead) { + + Write-Host " Skipped. Branch exceeds `"ahead by`" limit of $MaxCommitsAhead." + + $ReportBranchList += ">>> Branch watch list <<< $BranchName exceeds maximum age but has outstanding commits that exceed maximum Ahead By limit. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + + $WatchListCount++ + + continue + + } + + If ($AheadBy -eq 0) { + + Write-Host " $ReportOnlyString Delete branch $BranchName" + + If (!$ReportOnly) { + + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + + } + + $ReportBranchList += "$ReportOnlyString $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + + $DeleteBranchCount++ + + } Else { + + If ($AllowDataLoss) { + + Write-Host " $ReportOnlyString Delete branch $BranchName with data loss" + + If (!$ReportOnly) { + + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + + } + + $ReportBranchList += "$ReportOnlyString !!! DATA LOSS !!! $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + + $DeleteBranchCount++ + $DataLossCount++ + + } Else { + + Write-Host " $ReportOnlyString Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." + + $ReportBranchList += "$ReportOnlyString *** DATA LOSS BLOCKED *** $BranchName was marked for deletion with data loss but the data loss flag is disabled. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + + $DataLossBlockedCount++ + + } # AllowDataLoss If + + } # AheadBy If + + } # Branch loop + + } # Result pages loop + + $Branches = Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchesUrl -Method GET -FollowRelLink -MaximumFollowRelLink 50 -ResponseHeadersVariable ResponseHeaders + + $EndBranchCount = Get-TotalElements -Array $Branches + + Write-Host "`n`n`n" + + $ReportBranchList = $ReportBranchList | Sort-Object + + ForEach ($Item in $ReportBranchList) { + + Write-Host $Item + + } + + Write-Host "`nReport only mode: $ReportOnly" + Write-Host "Allow data loss: $AllowDataLoss" + Write-Host "Maximum commits ahead by limit: $MaxCommitsAhead" + Write-Host "Maximum days behind limit: $MaxDaysBehind" + Write-Host "Maximum branch age based on days behind limit: $DateLimit" + Write-Host "===========" + Write-Host "Default branch skip list: $DefaultSkipBranchList" + Write-Host "Repo branch skip list: $RepoBranchSkipList" + Write-Host "===========" + Write-Host "Total branches before run: $StartBranchCount" + Write-Host "Total branches after run: $EndBranchCount" + Write-Host "===========" + Write-Host "Watch list branches: $WatchListCount" + Write-Host "$ReportOnlyString Data loss blocked branches: $DataLossBlockedCount" + Write-Host "===========" + Write-Host "$ReportOnlyString Branches deleted with data loss: $DataLossCount" + Write-Host "$ReportOnlyString Total deleted branches: $DeleteBranchCount" + From 3d6d3f6df936e7b94b713051c1ed5e47d710a190 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 14 Mar 2025 14:55:07 -0700 Subject: [PATCH 39/92] Add some error checking and validation --- .github/workflows/Shared-StaleBranch.yml | 100 ++++++++++++++++------- 1 file changed, 69 insertions(+), 31 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index acb80ddf7bb..504d4621621 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -116,6 +116,8 @@ jobs: $WatchListCount = 0 $DataLossCount = 0 $DataLossBlockedCount = 0 + $RetrieveBranchDataErrorCount = 0 + $RetrieveBranchDataError = $False # Block the script from running if Ahead by is greater than 0 when maximum branch age is less than 90. Doing this could result in loss of data that may still be # wanted. Do not change the values unless you accept the high risk of unwanted data loss. @@ -177,44 +179,43 @@ jobs: } - $CompareData = Invoke-RestMethod -Headers $GitHubHeaders -Uri "$CompareUrl$BranchName" -Method GET -ResponseHeadersVariable ResponseHeaders + Try { - $BehindBy = $CompareData.behind_by - $AheadBy = $CompareData.ahead_by + $CompareData = Invoke-RestMethod -Headers $GitHubHeaders -Uri "$CompareUrl$BranchName" -Method GET -ResponseHeadersVariable ResponseHeaders -ErrorAction Stop - Write-Host " Ahead of $DefaultBranch by: $AheadBy `n Behind by: $BehindBy." + $BehindBy = [int]$CompareData.behind_by + $AheadBy = [int]$CompareData.ahead_by - If ($AheadBy -gt $MaxCommitsAhead) { - - Write-Host " Skipped. Branch exceeds `"ahead by`" limit of $MaxCommitsAhead." - - $ReportBranchList += ">>> Branch watch list <<< $BranchName exceeds maximum age but has outstanding commits that exceed maximum Ahead By limit. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + } Catch { + + Write-Host " ERROR: Failed to retrieve branch data. Skipping. URL: "$CompareUrl$BranchName" Error: $($_)" - $WatchListCount++ + $RetrieveBranchDataError = $True + $RetrieveBranchDataErrorCount++ continue - + } - If ($AheadBy -eq 0) { - - Write-Host " $ReportOnlyString Delete branch $BranchName" + If (($CompareData) -and ($BehindBy -is [int]) -and ($AheadBy -is [int]) -and ($LastCommitDate -is [datetime])) { - If (!$ReportOnly) { + Write-Host " Ahead of $DefaultBranch by: $AheadBy `n Behind by: $BehindBy." - Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + If ($AheadBy -gt $MaxCommitsAhead) { - } + Write-Host " Skipped. Branch exceeds `"ahead by`" limit of $MaxCommitsAhead." - $ReportBranchList += "$ReportOnlyString $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + $ReportBranchList += ">>> Branch watch list <<< $BranchName exceeds maximum age but has outstanding commits that exceed maximum Ahead By limit. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." - $DeleteBranchCount++ + $WatchListCount++ - } Else { - - If ($AllowDataLoss) { + continue + + } + + If ($AheadBy -eq 0) { - Write-Host " $ReportOnlyString Delete branch $BranchName with data loss" + Write-Host " $ReportOnlyString Delete branch $BranchName" If (!$ReportOnly) { @@ -222,22 +223,51 @@ jobs: } - $ReportBranchList += "$ReportOnlyString !!! DATA LOSS !!! $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + $ReportBranchList += "$ReportOnlyString $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " $DeleteBranchCount++ - $DataLossCount++ - + } Else { - Write-Host " $ReportOnlyString Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." + If ($AllowDataLoss) { + + Write-Host " $ReportOnlyString Delete branch $BranchName with data loss" - $ReportBranchList += "$ReportOnlyString *** DATA LOSS BLOCKED *** $BranchName was marked for deletion with data loss but the data loss flag is disabled. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + If (!$ReportOnly) { - $DataLossBlockedCount++ + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + + } + + $ReportBranchList += "$ReportOnlyString !!! DATA LOSS !!! $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + + $DeleteBranchCount++ + $DataLossCount++ + + } Else { + + Write-Host " $ReportOnlyString Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." + + $ReportBranchList += "$ReportOnlyString *** DATA LOSS BLOCKED *** $BranchName was marked for deletion with data loss but the data loss flag is disabled. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + + $DataLossBlockedCount++ + + } # AllowDataLoss If - } # AllowDataLoss If + } # AheadBy If + + } Else { - } # AheadBy If + Write-Host " ERROR - One or more variables doesn't contain a valid data type. CompareData: $($CompareData). AheadBy: $($AheadBy -is [int]). BehindBy: $($BehindBy -is [int]). LastCommitDate: $($LastCommitDate -is [datetime]). Skipping." + Write-Host " AheadBy: $AheadBy" + Write-Host " BehindBy: $BehindBy" + Write-Host " LastCommitDate: $LastCommitDate" + Write-Host $($CompareData | ConvertTo-Json -Depth 1) + + $RetrieveBranchDataError = $True + $RetrieveBranchDataErrorCount++ + + } # Data retrieval check If } # Branch loop @@ -274,4 +304,12 @@ jobs: Write-Host "===========" Write-Host "$ReportOnlyString Branches deleted with data loss: $DataLossCount" Write-Host "$ReportOnlyString Total deleted branches: $DeleteBranchCount" + + # Forcing the workflow to fail at the end to bring attention to the fact that there was a failure to process + # one or more branches. + If ($RetrieveBranchDataError) { + + Throw "Failed to retrieve data for $RetrieveBranchDataErrorCount branches." + + } From 6a52326c007177c4307faf41b87e2fda7bea79b0 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 24 Mar 2025 16:25:00 -0700 Subject: [PATCH 40/92] Improve reporting --- .github/workflows/Shared-StaleBranch.yml | 318 ++++++++++++++++++++--- 1 file changed, 286 insertions(+), 32 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 504d4621621..34137f16bd4 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -22,7 +22,7 @@ on: jobs: stale-branch: - name: Removal stale branches + name: Stale branch removal runs-on: ubuntu-latest steps: - name: Process branches @@ -47,6 +47,7 @@ jobs: $RepoBranchSkipList = $env:RepoBranchSkipList | ConvertFrom-Json $ReportOnly = [System.Convert]::ToBoolean($env:ReportOnly) + # Function to get the total number of elements in an array. Supports nested arrays. Function Get-TotalElements { param( [Parameter(Mandatory=$true)] @@ -66,7 +67,7 @@ jobs: return $count } - + # Indicate in the script log if the workflow is in reporting mode or not. If in reporting mode, no changes will be made to the branches. If ($ReportOnly) { Write-Host "`n`nRUNNING IN REPORTING MODE`n`n" @@ -82,6 +83,7 @@ jobs: Write-Host "Default branch skip list: $DefaultSkipBranchList" Write-Host "Repo branch skip list: $RepoBranchSkipList" + # Create the branch skip list that is a combination of the central workflow list and the branch skip list that can be populated in each individual repo. $SkipBranchList = $DefaultSkipBranchList + $RepoBranchSkipList | Select-Object -Unique # WARNING - Setting $MaxCommitsAhead to anything other than 0 means that the workflow will delete branches with changes not in default branch. @@ -92,6 +94,7 @@ jobs: $MaxDaysBehind = 90 $DateLimit = (Get-Date).AddDays(-$MaxDaysBehind) + $ReportDate = Get-Date -Format "dddd MMMM dd, yyyy" # Create github HTTP authentication header $UserAgent = "officedocs" @@ -99,18 +102,24 @@ jobs: $GitHubHeaders.Add("Authorization","token $($AccessToken)") $GitHubHeaders.Add("User-Agent", $UserAgent) + # Retrieve repo data and API URLs. $RepoUrl = $GitHubData.event.repository.url $RepoData = Invoke-RestMethod -Headers $GitHubHeaders -Uri $RepoUrl -Method GET + # Set basic repo data and construct API query URLs. + $RepoName = $GitHubdata.event.repository.name + $OrgName = $GitHubData.event.organization.login $BranchesUrl = $RepoData.branches_url.Replace("{/branch}", "?per_page=100") - $DefaultBranch = $RepoData.default_branch $CompareUrl = $RepoData.compare_url.Replace("{base}...{head}", "$DefaultBranch...") + $GitHubGraphQlUrl = "https://api.github.com/graphql" + $BranchesHtmlUrl = "$($GitHubData.event.repository.html_url)/branches" + # Get the list of branches to process and set the initial branch count. $Branches = Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchesUrl -Method GET -FollowRelLink -MaximumFollowRelLink 50 -ResponseHeadersVariable ResponseHeaders - $StartBranchCount = Get-TotalElements -Array $Branches + # Initialize variables that'll be used so they're all zeroed out. $ReportBranchList = @() $DeleteBranchCount = 0 $WatchListCount = 0 @@ -138,15 +147,49 @@ jobs: ForEach ($Branch in $Page) { + # Reset variables $AheadBy = $BehindBy = $LastCommitDate = $CompareData = $Null - $ProtectedBranch = $True + # Set branch variables and URLs + $ProtectedBranch = $Branch.protected $BranchName = $Branch.name $CommitsUrl = $RepoData.commits_url.Replace("{/sha}", "?sha=$BranchName&per_page=1&page=1") $BranchDeleteUrl = $RepoData.url + "/git/refs/heads/$BranchName" + # GraphQL query to retrieve the account associated with the branch's first commit as seen by GitHub. + # May not be the actual creator of the branch in some edge cases but close enough. + $FirstCommitQuery = @" + query { + repository(owner: "$OrgName", name: "$RepoName") { + ref(qualifiedName: "refs/heads/$BranchName") { + target { + ... on Commit { + history(first: 1) { + edges { + node { + author { + user { + login + } + } + } + } + } + } + } + } + } + } + "@ + + # Create the body to send to GitHub's GraphQL API. Let ConvertTo-Json do some cleanup to make sure the JSON is valid. + $FirstCommitQueryBody = @{query = $FirstCommitQuery} | ConvertTo-Json + Write-Host "`nBranch name: $BranchName" + ####### Preliminary checks to see if the branch should be excluded. ####### + + # Check to see if the branch is on the skip list and, if so, go to the next branch. If ($SkipBranchList -contains $BranchName) { Write-Host " Skipped. Branch is on the branch skip list." @@ -155,10 +198,9 @@ jobs: } - $ProtectedBranch = $Branch.protected - Write-Host " Protected: $ProtectedBranch." + # Check to see if the branch is protect and, if so, go to the next branch. If ($ProtectedBranch) { Write-Host " Skipped. Branch is protected." @@ -167,10 +209,13 @@ jobs: } + # Get the last commit in the branch and then get the commit's date. This will be used to determine the last time there was activity in the branch. $LastCommitDate = (Invoke-RestMethod -Headers $GitHubHeaders -uri $CommitsUrl).commit.committer.date Write-Host " Last commit date: $LastCommitDate." + # If the date of last activity was after the maximum age cutoff, skip to the next branch. Only if the branch's last activity was before the cut off + # do we process anything else (ahead by, etc). If ($LastCommitDate -ge $DateLimit) { Write-Host " Skipped. Last commit date is after $DateLimit." @@ -179,12 +224,33 @@ jobs: } + ####### End of preliminary checks ####### + + ####### + # Try/Catch statement retrieves all the data that will be used in the upcoming conditions. If an exception occurs, the error will be returned and the branch will be skipped. Try { + # Get a diff between the branch and $DefaultBranch so we can get AheadBy/BehindBy. $CompareData = Invoke-RestMethod -Headers $GitHubHeaders -Uri "$CompareUrl$BranchName" -Method GET -ResponseHeadersVariable ResponseHeaders -ErrorAction Stop + # Get the branch's first commit using the GraphQL query created earlier to get the branch's likely creator. + $FirstCommitData = $(Invoke-RestMethod -Method POST -Uri $GitHubGraphQlUrl -Headers $GitHubHeaders -Body $FirstCommitQueryBody).data.repository.ref.target.history.edges.node | Select-Object -First 1 + # Force AheadBy/BehindBy type to [int] so we can test later on if we received valid data from GitHub. + # Get the branch's likely creator. $BehindBy = [int]$CompareData.behind_by $AheadBy = [int]$CompareData.ahead_by + $BranchCreator = $FirstCommitData.author.user.login + + # Create a reporting object that contains all the branch info collected. This will be used to create a table in the workflow summary that can be viewed by repo contributors. + $BranchReportObject = [PSCustomObject]@{ + BranchName = $BranchName + ProtectedBranch = $ProtectedBranch + AheadBy = $AheadBy + BehindBy = $BehindBy + DaysSinceLastCommit = $($(Get-Date) - $LastCommitDate).Days + ProcessingResult = $Null + BranchCreator = $BranchCreator + } } Catch { @@ -197,15 +263,22 @@ jobs: } + ####### + # From this point to the reporting section, the script will retrieve additional data to determine whether the branch should be excluded or not. Each if statement builds on the previous + # to see if a branch should be excluded or deleted. Only if every if statement is true, will the branch be deleted. + + # Check that valid data was returned. If (($CompareData) -and ($BehindBy -is [int]) -and ($AheadBy -is [int]) -and ($LastCommitDate -is [datetime])) { Write-Host " Ahead of $DefaultBranch by: $AheadBy `n Behind by: $BehindBy." + # Check to see if there are more commits in the branch than the allowed maximum ahead by commits. If so, update the reporting object and skip to the next branch. If ($AheadBy -gt $MaxCommitsAhead) { Write-Host " Skipped. Branch exceeds `"ahead by`" limit of $MaxCommitsAhead." - $ReportBranchList += ">>> Branch watch list <<< $BranchName exceeds maximum age but has outstanding commits that exceed maximum Ahead By limit. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + $BranchReportObject.ProcessingResult = "Watch" + $ReportBranchList += $BranchReportObject $WatchListCount++ @@ -213,33 +286,42 @@ jobs: } + # If the branch doesn't contain any commits not in $DefaultBranch, it's ok to delete. If ($AheadBy -eq 0) { Write-Host " $ReportOnlyString Delete branch $BranchName" + # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. If (!$ReportOnly) { Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null } - $ReportBranchList += "$ReportOnlyString $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + # Update the reporting object. + $BranchReportObject.ProcessingResult = "Deleted" + $ReportBranchList += $BranchReportObject $DeleteBranchCount++ } Else { + # There's a chance that allowed number of commits to delete is greater than 0. If that's true, then this is an extra check to confirm that + # the branch should actually be deleted. This is because deleting the branch will result in data loss. If ($AllowDataLoss) { Write-Host " $ReportOnlyString Delete branch $BranchName with data loss" + # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. If (!$ReportOnly) { Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null } - $ReportBranchList += "$ReportOnlyString !!! DATA LOSS !!! $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " + # Update the reporting object. + $BranchReportObject.ProcessingResult = "DataLoss-Deleted" + $ReportBranchList += $BranchReportObject $DeleteBranchCount++ $DataLossCount++ @@ -248,7 +330,9 @@ jobs: Write-Host " $ReportOnlyString Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." - $ReportBranchList += "$ReportOnlyString *** DATA LOSS BLOCKED *** $BranchName was marked for deletion with data loss but the data loss flag is disabled. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." + # Update the reporting object. + $BranchReportObject.ProcessingResult = "DataLoss-Blocked" + $ReportBranchList += $BranchReportObject $DataLossBlockedCount++ @@ -273,37 +357,207 @@ jobs: } # Result pages loop + ####### + # Branch processing has completed and, from this point on, reporting is generated. + + # Construct the markdown table header that'll be used in the workflow summary. + $TableHeaderRow1 = "| Branch name | Branch creator | Commits ahead by | Commits behind by | Days since last commit | Processing result |" + $TableHeaderRow2 = "|-------------|----------------|------------------|-------------------|------------------------|-------------------|" + + # Set job summary and create the "Deleted stale branches" section. + echo "# Stale branch results" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "**Summary generated**: $ReportDate" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "This summary shows the actions taken by this workflow during its run on the date above. If you want to see the current status of branches in this repository, see [Branches]($BranchesHtmlUrl)." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + + # Retrieve a list of branches again to get the total number of branches after processing the branches. $Branches = Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchesUrl -Method GET -FollowRelLink -MaximumFollowRelLink 50 -ResponseHeadersVariable ResponseHeaders - $EndBranchCount = Get-TotalElements -Array $Branches Write-Host "`n`n`n" - $ReportBranchList = $ReportBranchList | Sort-Object + # Sort the results of the branch report, create delete and watch branch lists, and get list counts. + $ReportBranchList = $ReportBranchList | Sort-Object -Property ProcessingResult, BranchName + $DeleteBranchList = $ReportBranchList | Where {$_.ProcessingResult -notmatch "Watch"} + $WatchBranchList = $ReportBranchList | Where {$_.ProcessingResult -match "Watch"} + $DeleteBranchListCount = $DeleteBranchList.Count + $WatchBranchListCount = $WatchBranchList.Count + + echo "## Deleted stale branches" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY - ForEach ($Item in $ReportBranchList) { + If ($DeleteBranchlistCount -gt 0) { + + echo "## Deleted stale branches" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "The following branches were deleted because they were over $MaxDaysBehind days behind the $DefaultBranch branch and contained $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + If ($ReportOnly) { - Write-Host $Item + echo "**REPORTING MODE**: Reporting mode is currently enabled. No branches were deleted during this run." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + } + + # Start to build the branch action taken table in the workflow summary. + echo $TableHeaderRow1 >> $env:GITHUB_STEP_SUMMARY + echo $TableHeaderRow2 >> $env:GITHUB_STEP_SUMMARY + + # Loop through every reporting object in the array where the processing result isn't "Watch". Add a row for each branch + # containing the branch data for writers to review. This is the table that shows what action was taken for each branch. + ForEach ($BranchReport in $DeleteBranchList) { + + $BN = $BranchReport.BranchName + $PB = $BranchReport.ProtectedBranch + $AB = $BranchReport.AheadBy + $BB = $BranchReport.BehindBy + $LC = $BranchReport.DaysSinceLastCommit + $PR = $BranchReport.ProcessingResult + $BC = $BranchReport.BranchCreator + + Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. " + echo "| $BN | $BC | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY + + } + + } Else { + + echo "No branches were deleted during this run." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + } - Write-Host "`nReport only mode: $ReportOnly" - Write-Host "Allow data loss: $AllowDataLoss" - Write-Host "Maximum commits ahead by limit: $MaxCommitsAhead" - Write-Host "Maximum days behind limit: $MaxDaysBehind" - Write-Host "Maximum branch age based on days behind limit: $DateLimit" - Write-Host "===========" - Write-Host "Default branch skip list: $DefaultSkipBranchList" - Write-Host "Repo branch skip list: $RepoBranchSkipList" - Write-Host "===========" - Write-Host "Total branches before run: $StartBranchCount" - Write-Host "Total branches after run: $EndBranchCount" - Write-Host "===========" - Write-Host "Watch list branches: $WatchListCount" - Write-Host "$ReportOnlyString Data loss blocked branches: $DataLossBlockedCount" - Write-Host "===========" - Write-Host "$ReportOnlyString Branches deleted with data loss: $DataLossCount" - Write-Host "$ReportOnlyString Total deleted branches: $DeleteBranchCount" + # Create a new section for the Watchlist branches + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "## Stale branch watch list" >> $env:GITHUB_STEP_SUMMARY + + If ($WatchBranchListCount -gt 0) { + + + echo "The following branches are over $MaxDaysBehind days behind the $DefaultBranch branch but were **not** deleted because they contained more than $MaxCommitsAhead commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY + echo "These branches should be reviewed and, if they're no longer needed, deleted. If these branches are still needed, they must be brought up to date with the $DefaultBranch branch. Select a branch name to view the differences between it and $($DefaultBranch)." >> $env:GITHUB_STEP_SUMMARY + echo "**Branches in this list may be deleted at a future date even if they contain commits not in the $DefaultBranch branch.**" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo $TableHeaderRow1 >> $env:GITHUB_STEP_SUMMARY + echo $TableHeaderRow2 >> $env:GITHUB_STEP_SUMMARY + + # Loop through every reporting object in the array where the processing result is "Watch". Add a row for each branch + # containing the branch data for writers to review. This is the table that shows writers which branches might be deleted in the future. + ForEach ($BranchReport in $WatchBranchList) { + + $BN = $BranchReport.BranchName + $PB = $BranchReport.ProtectedBranch + $AB = $BranchReport.AheadBy + $BB = $BranchReport.BehindBy + $LC = $BranchReport.DaysSinceLastCommit + $PR = $BranchReport.ProcessingResult + $BC = $BranchReport.BranchCreator + + $BranchDiffHtmlUrl = "$($GitHubData.event.repository.html_url)/compare/$DefaultBranch...$($BN)#files_bucket" + + Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. " + echo "| [$BN]($BranchDiffHtmlUrl) | $BC | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY + + } + + } Else { + + echo "No branches were added to the watch list during this run." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + } + + # The following output the workflow summary to both the script log and to the workflow summary. + + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "## Workflow overview" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $ReportOnlyMode = "Report only mode: $ReportOnly" + Write-Host $ReportOnlyMode + echo $ReportOnlyMode >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $AllowDataLossSetting = "Allow data loss: $AllowDataLoss" + Write-Host $AllowDataLossSetting + echo "$AllowDataLossSetting" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $MaximumCommitsAheadByLimit = "Maximum commits ahead by limit: $MaxCommitsAhead" + Write-Host $MaximumCommitsAheadByLimit + echo "$MaximumCommitsAheadByLimit" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $MaximumDaysBehindLimit = "Maximum days behind limit: $MaxDaysBehind" + Write-Host $MaximumDaysBehindLimit + echo "$MaximumDaysBehindLimit" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $MaximumBranchAgeBasedOnDaysBehindLimit = "Maximum branch age based on days behind limit: $DateLimit" + Write-Host $MaximumBranchAgeBasedOnDaysBehindLimit + echo "$MaximumBranchAgeBasedOnDaysBehindLimit" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $SeparatorLine = "===========" + Write-Host $SeparatorLine + echo $SeparatorLine >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $DefaultBranchSkipList = "Default branch skip list: $DefaultSkipBranchList" + Write-Host $DefaultBranchSkipList + echo $DefaultBranchSkipList >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $RepoBranchSkipListText = "Repo branch skip list: $RepoBranchSkipList" + Write-Host $RepoBranchSkipListText + echo "$RepoBranchSkipListText" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + Write-Host $SeparatorLine + echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $TotalBranchesBeforeRun = "Total branches before run: $StartBranchCount" + Write-Host $TotalBranchesBeforeRun + echo "$TotalBranchesBeforeRun" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $TotalBranchesAfterRun = "Total branches after run: $EndBranchCount" + Write-Host $TotalBranchesAfterRun + echo "$TotalBranchesAfterRun" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + Write-Host $SeparatorLine + echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $WatchListBranches = "Watch list branches: $WatchListCount" + Write-Host $WatchListBranches + echo "$WatchListBranches" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $DataLossBlockedBranches = "$ReportOnlyString Data loss blocked branches: $DataLossBlockedCount" + Write-Host $DataLossBlockedBranches + echo "$DataLossBlockedBranches" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + Write-Host $SeparatorLine + echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $BranchesDeletedWithDataLoss = "$ReportOnlyString Branches deleted with data loss: $DataLossCount" + Write-Host $BranchesDeletedWithDataLoss + echo "$BranchesDeletedWithDataLoss" >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + $TotalDeletedBranches = "$ReportOnlyString Total deleted branches: $DeleteBranchCount" + Write-Host $TotalDeletedBranches + echo "$TotalDeletedBranches" >> $env:GITHUB_STEP_SUMMARY # Forcing the workflow to fail at the end to bring attention to the fact that there was a failure to process # one or more branches. From a9e6104e5fe79e25e4a41943f0be1a14563bc734 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 25 Mar 2025 10:56:35 -0700 Subject: [PATCH 41/92] Remove duplicated heading --- .github/workflows/Shared-StaleBranch.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 34137f16bd4..14921cda3a7 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -391,8 +391,6 @@ jobs: If ($DeleteBranchlistCount -gt 0) { - echo "## Deleted stale branches" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY echo "The following branches were deleted because they were over $MaxDaysBehind days behind the $DefaultBranch branch and contained $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY From a1962ae36ef5e5a5ce73763e0588b442497c9a1b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 25 Mar 2025 11:57:32 -0700 Subject: [PATCH 42/92] Add check to run only if triggered from MicrosoftDocs repo --- .github/workflows/Shared-AutoLabelAssign.yml | 1 + .github/workflows/Shared-AutoLabelMsftContributor.yml | 1 + .github/workflows/Shared-BuildValidation.yml | 1 + .github/workflows/Shared-ExtractPayload.yml | 1 + .github/workflows/Shared-LiveMergeCheck.yml | 1 + .github/workflows/Shared-PrFileCount.yml | 1 + .github/workflows/Shared-ProtectedFiles.yml | 1 + .github/workflows/Shared-Stale.yml | 4 ++-- .github/workflows/Shared-StaleBranch.yml | 1 + .github/workflows/Shared-TierManagement.yml | 1 + 10 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index ea9db84ffb3..a3d41429252 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -29,6 +29,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script diff --git a/.github/workflows/Shared-AutoLabelMsftContributor.yml b/.github/workflows/Shared-AutoLabelMsftContributor.yml index db9e3215894..147c9192f5c 100644 --- a/.github/workflows/Shared-AutoLabelMsftContributor.yml +++ b/.github/workflows/Shared-AutoLabelMsftContributor.yml @@ -20,6 +20,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script diff --git a/.github/workflows/Shared-BuildValidation.yml b/.github/workflows/Shared-BuildValidation.yml index 59504e4720e..bd60ea2fdee 100644 --- a/.github/workflows/Shared-BuildValidation.yml +++ b/.github/workflows/Shared-BuildValidation.yml @@ -17,6 +17,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script diff --git a/.github/workflows/Shared-ExtractPayload.yml b/.github/workflows/Shared-ExtractPayload.yml index 4cec82312a5..6c2f25a52da 100644 --- a/.github/workflows/Shared-ExtractPayload.yml +++ b/.github/workflows/Shared-ExtractPayload.yml @@ -24,6 +24,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest outputs: JobPayload: ${{ steps.get-payload.outputs.WorkflowPayload }} diff --git a/.github/workflows/Shared-LiveMergeCheck.yml b/.github/workflows/Shared-LiveMergeCheck.yml index 649c7e1871a..c6f79311d32 100644 --- a/.github/workflows/Shared-LiveMergeCheck.yml +++ b/.github/workflows/Shared-LiveMergeCheck.yml @@ -18,6 +18,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script diff --git a/.github/workflows/Shared-PrFileCount.yml b/.github/workflows/Shared-PrFileCount.yml index e9365888225..3db88a09c82 100644 --- a/.github/workflows/Shared-PrFileCount.yml +++ b/.github/workflows/Shared-PrFileCount.yml @@ -18,6 +18,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index 3d05bd6d79e..dc6101154d2 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -18,6 +18,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index d1257dad99d..f0905a045b7 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -21,7 +21,7 @@ jobs: stale-private: name: Stale check - private repos runs-on: ubuntu-latest - if: inputs.RepoVisibility == 'private' + if: github.repository_owner == 'MicrosoftDocs' && inputs.RepoVisibility == 'private' steps: # - name: Get selected month @@ -117,7 +117,7 @@ jobs: # stale-public: # name: Stale check - public repos # runs-on: ubuntu-latest -# if: inputs.RepoVisibility == 'public' +# if: github.repository_owner == 'MicrosoftDocs' && inputs.RepoVisibility == 'public' # steps: # - uses: actions/stale@v9 # with: diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 14921cda3a7..f5b97aaa7df 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -23,6 +23,7 @@ jobs: stale-branch: name: Stale branch removal + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Process branches diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml index cd714d29eb9..6b6531b5236 100644 --- a/.github/workflows/Shared-TierManagement.yml +++ b/.github/workflows/Shared-TierManagement.yml @@ -23,6 +23,7 @@ on: jobs: build: name: Run Script + if: github.repository_owner == 'MicrosoftDocs' runs-on: ubuntu-latest steps: - name: Script From 4d7ab46d2ff2b7eaab920b802d06f975e7d3533e Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 27 Mar 2025 14:36:12 -0700 Subject: [PATCH 43/92] Delete .github/workflows/StaleBranch-Test.yml --- .github/workflows/StaleBranch-Test.yml | 167 ------------------------- 1 file changed, 167 deletions(-) delete mode 100644 .github/workflows/StaleBranch-Test.yml diff --git a/.github/workflows/StaleBranch-Test.yml b/.github/workflows/StaleBranch-Test.yml deleted file mode 100644 index 37a68f092bc..00000000000 --- a/.github/workflows/StaleBranch-Test.yml +++ /dev/null @@ -1,167 +0,0 @@ -name: (Scheduled) Stale branch removal - -permissions: - contents: write - -on: - # Commenting out schedule in MAX-CPUB-Test because it's actually running and impacting the production repo. If the workflow needs to be updated here - # and put into production, remove this comment and uncomment the schedule. - #schedule: - #- cron: "0 */6 * * *" - - workflow_dispatch: - - -jobs: - - stale-branch: - name: Removal stale branches - runs-on: ubuntu-latest - steps: - - name: Process branches - shell: pwsh - env: - SkipBranchList: '[ - "live", - "main", - "repo_sync_working_branch", - "asdf" - ]' - PayloadJson: ${{ toJSON(github) }} - AccessToken: ${{ secrets.GITHUB_TOKEN }} - run: | - - # Get GitHub data - $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 - $AccessToken = $env:AccessToken - $SkipBranchList = $env:SkipBranchList | ConvertFrom-Json - - - # WARNING - Setting $MaxAheadDefault to anything other than 0 means that the workflow will delete branches with changes not in default branch. - # !!! > 0 WILL RESULT IN DATA LOSS !!! - $MaxAheadDefault = 0 # This is the maximum number of commits a branch can be ahead of default branch. - $AllowDataLoss = $False # This flag must be set to $True to allow branches with commits not in default branch to be deleted. - # !!! > 0 WILL RESULT IN DATA LOSS !!! - - $MaxDaysBehind = 90 - $DateLimit = (Get-Date).AddDays(-$MaxDaysBehind) - - # Create github HTTP authentication header - $UserAgent = "officedocs" - $GitHubHeaders = @{} - $GitHubHeaders.Add("Authorization","token $($AccessToken)") - $GitHubHeaders.Add("User-Agent", $UserAgent) - - $RepoUrl = $GitHubData.event.repository.url - $RepoData = Invoke-RestMethod -Headers $GitHubHeaders -Uri $RepoUrl -Method GET - - $BranchesUrl = $RepoData.branches_url.Replace("{/branch}", "") - - $DefaultBranch = $RepoData.default_branch - $SyncBranch = "repo-sync-working-branch" - $CompareUrl = $RepoData.compare_url.Replace("{base}...{head}", "$DefaultBranch...") - - $Branches = Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchesUrl -Method GET -FollowRelLink -MaximumFollowRelLink 50 -ResponseHeadersVariable ResponseHeaders - - $ReportBranchList = @() - - ForEach ($Page in $Branches) { - - ForEach ($Branch in $Page) { - - $AheadBy = $BehindBy = $LastCommitDate = $CompareData = $Null - $ProtectedBranch = $True - - $BranchName = $Branch.name - $CommitsUrl = $RepoData.commits_url.Replace("{/sha}", "?sha=$BranchName&per_page=1&page=1") - - Write-Host "`nBranch name: $BranchName" - - If ($SkipBranchList -contains $BranchName) { - Write-Host " Skipped. Branch is on the branch skip list." - continue - } - - # $BranchData = Invoke-RestMethod -Headers $GitHubHeaders -Uri "$branchesurl/$BranchName" -Method GET -ResponseHeadersVariable ResponseHeaders - # $ProtectedBranch = $BranchData.protected - $ProtectedBranch = $Branch.protected - - Write-Host " Protected: $ProtectedBranch." - - If ($ProtectedBranch) { - Write-Host " Skipped. Branch is protected." - continue - } - - $LastCommitDate = (Invoke-RestMethod -Headers $GitHubHeaders -uri $CommitsUrl).commit.committer.date - - Write-Host " Last commit date: $LastCommitDate." - - If ($LastCommitDate -ge $DateLimit) { - Write-Host " Skipped. Last commit date is after $DateLimit." - continue - } - - $CompareData = Invoke-RestMethod -Headers $GitHubHeaders -Uri "$CompareUrl$BranchName" -Method GET -ResponseHeadersVariable ResponseHeaders - - $BehindBy = $CompareData.behind_by - $AheadBy = $CompareData.ahead_by - - Write-Host " Ahead of $DefaultBranch by: $AheadBy `n Behind by: $BehindBy." - - If ($AheadBy -gt $MaxAheadDefault) { - Write-Host " Skipped. Branch exceeds `"ahead by`" limit of $MaxAheadDefault." - - $ReportBranchList += ">>> Branch watch list <<< $BranchName exceeds maximum age but has outstanding commits that exceed maximum Ahead By limit. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." - - continue - } - - - If ($AheadBy -eq 0) { - - Write-Host " Delete branch $BranchName" - - $BranchDeleteUrl = $RepoData.url + "/git/refs/heads/$BranchName" - Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null - - $ReportBranchList += "$BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " - - $DeleteBranchCount++ - - } Else { - - If ($AllowDataLoss) { - - Write-Host " Delete branch $BranchName with data loss" - - $ReportBranchList += "!!! DATA LOSS !!! $BranchName deleted. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days). " - - $DeleteBranchCount++ - - } Else { - - Write-Host " Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." - - $ReportBranchList += "*** DATA LOSS BLOCKED *** $BranchName was marked for deletion with data loss but the data loss flag is disabled. Branch protected: $ProtectedBranch. Ahead by: $AheadBy. Behind by $BehindBy. Days since last commit: $($($(Get-Date) - $LastCommitDate).Days)." - - } - - } - - - - } - - - } - - Write-Host "`n`n`n" - - $ReportBranchList = $ReportBranchList | Sort-Object - - ForEach ($Item in $ReportBranchList) { - - Write-Host $Item - - } \ No newline at end of file From 4805a51717ce242b4f8923e21e59a38c4964641a Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 1 Apr 2025 10:59:39 -0700 Subject: [PATCH 44/92] Add StaleBranch workflow files --- .github/workflows/Shared-ProtectedFiles.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index dc6101154d2..f9e7504ca10 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -45,6 +45,7 @@ jobs: "PrFileCount.yml", "ProtectedFiles.yml", "Stale.yml", + "StaleBranch.yml", "TierManagement.yml", "workflow-status-report.yml", "Shared-AutoLabelAssign.yml", @@ -55,6 +56,7 @@ jobs: "Shared-PrFileCount.yml", "Shared-ProtectedFiles.yml", "Shared-Stale.yml", + "Shared-StaleBranch.yml", "Shared-TierManagement.yml" ]' ApproverList: '["user1"]' From 56052cb8ff68dbf918308c608d70e13a396d70fa Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 9 Apr 2025 11:56:56 -0700 Subject: [PATCH 45/92] Create Shared-Publish.yml --- .github/workflows/Shared-Publish.yml | 621 +++++++++++++++++++++++++++ 1 file changed, 621 insertions(+) create mode 100644 .github/workflows/Shared-Publish.yml diff --git a/.github/workflows/Shared-Publish.yml b/.github/workflows/Shared-Publish.yml new file mode 100644 index 00000000000..680323b665d --- /dev/null +++ b/.github/workflows/Shared-Publish.yml @@ -0,0 +1,621 @@ +name: (Scheduled) Publish to live + +permissions: + contents: write + pull-requests: write + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + EnableAutoPublish: + required: true + type: boolean + secrets: + AccessToken: + required: true + PrivateKey: + required: true + ClientId: + required: true + +jobs: + + auto-publish: + name: Publish to live + if: github.repository_owner == 'MicrosoftDocs' && contains(github.event.repository.topics, 'build') + runs-on: ubuntu-latest + steps: + - name: Process branches + shell: pwsh + env: + AccessToken: ${{ secrets.AccessToken }} + PayloadJson: ${{ inputs.PayloadJson }} + EnableAutoPublish: ${{ inputs.EnableAutoPublish }} + PrivateKey: ${{ secrets.PrivateKey }} + ClientId: ${{ secrets.ClientId }} + + run: | + + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $EnableAutoPublish = [System.Convert]::ToBoolean($env:EnableAutoPublish) + $AccessToken = $env:AccessToken + $PrivateKey = $env:PrivateKey + $ClientId = $env:ClientId + + # Date/Time calculations + $UtcNow = Get-Date -AsUTC + $PacificTz = [System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time") + $IndiaTz = [System.TimeZoneInfo]::FindSystemTimeZoneById("India Standard Time") + # Convert UTC to local times + $PacificNow = [System.TimeZoneInfo]::ConvertTimeFromUtc($UtcNow, $PacificTz) + $IndiaNow = [System.TimeZoneInfo]::ConvertTimeFromUtc($UtcNow, $IndiaTz) + # Format as MM/DD HH:mm (24 hour clock) + $PacificStamp = $PacificNow.ToString('MM/dd HH:mm') + $IndiaStamp = $IndiaNow.ToString('MM/dd HH:mm') + + # Create github HTTP authentication header + $StandardGitHubHeaders = @{} + $StandardGitHubHeaders.Add("Authorization","token $($AccessToken)") + $StandardGitHubHeaders.Add("User-Agent", "OfficeDocs") + + # Repo variables + $TargetBranch = "live" + $Repository = $GitHubdata.event.repository.name + $Organization = $GitHubData.event.organization.login + $DefaultBranch = $GitHubData.event.repository.default_branch + $RepoLabelUrl = $GitHubData.event.repository.labels_url + + # PR variables + $PrTitle = "[AutoPublish] $DefaultBranch to $TargetBranch" + $PrDescription = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`nBefore merging this PR, complete the following checks:`n`n- There are no more than 100 files in the PR. If there are more than 100 files, review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- There are no build warnings. If there are warnings, resolve them before merging the PR.`n- Regular PR criteria checks pass.`n`nIf you have questions, email marveldocs-admins." + + # Label variables + $AutoPublishLabelColor = "5319E7" + $AutoPublishLabelDescription = "PR was automatically created for publishing from $DefaultBranch to $TargetBranch." + $AutoPublishLabel = "AutoPublish" + $SignOffLabelColor = "46ce1c" + $SignOffLabelDescription = "The pull request is ready to be reviewed and merged by PubOps." + $SignOffLabel = "Sign off" + + ##################### + ##################### + # ConvertTo-JwtBase64 + + Function ConvertTo-JwtBase64 { + param( + [Parameter(Mandatory = $True)] + [string]$RawJson + ) + + # Convert UTF-8 bytes to Base64, then make it URL-safe for JWT + $Encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($RawJson)) + Return $Encoded.TrimEnd('=') -replace '\+','-' -replace '/','_' + } + + ##################### + ##################### + # New-GitHubAppJWT + + Function New-GitHubAppJWT { + param( + [Parameter(Mandatory = $True)] + [string]$ClientId, + + [Parameter(Mandatory = $True)] + [string]$PrivateKey, + + [int]$ExpiresInMinutes = 10 + ) + + Write-Host "Create JWT" + + # Build the header + $Header = @{ + alg = "RS256" + typ = "JWT" + } | ConvertTo-Json -Compress + + $HeaderEncoded = ConvertTo-JwtBase64 -RawJson $Header + + # Build the payload + $Now = [System.DateTimeOffset]::UtcNow + $Payload = @{ + iat = $now.AddSeconds(-10).ToUnixTimeSeconds() + exp = $now.AddMinutes($ExpiresInMinutes).ToUnixTimeSeconds() + iss = $ClientId + } | ConvertTo-Json -Compress + + $PayloadEncoded = ConvertTo-JwtBase64 -RawJson $Payload + + # Combine and sign + $Rsa = [System.Security.Cryptography.RSA]::Create() + $Rsa.ImportFromPem($PrivateKey) + + $Combined = "$HeaderEncoded.$PayloadEncoded" + $SignatureBytes = $Rsa.SignData( + [System.Text.Encoding]::UTF8.GetBytes($Combined), + [System.Security.Cryptography.HashAlgorithmName]::SHA256, + [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 + ) + + $SignatureEncoded = [Convert]::ToBase64String($SignatureBytes).TrimEnd('=') -replace '\+','-' -replace '/','_' + + Return "$HeaderEncoded.$PayloadEncoded.$SignatureEncoded" + } + + ##################### + ##################### + # Get-GitHubAppInstallationToken + + Function Get-GitHubAppInstallationToken { + param( + [Parameter(Mandatory = $true)] + [string]$ClientId, + + [Parameter(Mandatory = $true)] + [string]$PrivateKey, + + [Parameter(Mandatory = $true)] + [string]$Organization, + + [int]$TokenTTLMinutes = 10 + ) + + # Create the JWT + $Jwt = New-GitHubAppJWT -ClientId $ClientId -PrivateKey $PrivateKey -ExpiresInMinutes $TokenTTLMinutes + + # Prepare headers + $JwtHeaders = @{ + Authorization = "Bearer $Jwt" + Accept = "application/vnd.github+json" + "X-GitHub-Api-Version"= "2022-11-28" + } + + Write-Host "Request org installation ID" + + # 1) Retrieve the installation ID for the org + Try { + $InstallationInfo = Invoke-RestMethod ` + -Uri "https://api.github.com/orgs/$Organization/installation" ` + -Headers $JwtHeaders ` + -ErrorAction Stop + + $InstallationId = $InstallationInfo.id + } + Catch { + Write-Host "Failed to get installation ID from GitHub. $_" + Return $Null + } + + Write-Host "Get installation token" + + # 2) Use the installation ID to request an installation token + Try { + $TokenResponse = Invoke-RestMethod ` + -Uri "https://api.github.com/app/installations/$InstallationId/access_tokens" ` + -Headers $JwtHeaders ` + -Method Post ` + -ErrorAction Stop + + Return $TokenResponse.token + } + Catch { + Write-Host "Failed to get access token from GitHub. $_" + Return $Null + } + } + + ##################### + ##################### + # Test-RepoLabel + + Function Test-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $RepoUri + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $LabelUri = $RepoUri.Replace("{/name}","/$Name") + + # Check to see if the label we want exists in the repo + Try { + + Write-Host "Checking to see if label $Name exists in repo URL $LabelUri." + + $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $AppGitHubAccessHeaders -ErrorAction Stop + $LabelFound = $True + + } Catch { + + # OK if label doesn't exist. Just means we need to create it. + $LabelFound = $False + + } + + # Return boolean to calling statement + $LabelFound + + } + + ##################### + ##################### + # New-RepoLabel + + Function New-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $Color, + $Description, + $RepoUri + ) + + # Remove placeholder text from repo URL + $RepoUri = $RepoUri.Replace("{/name}","") + $Result = $Null + + # Construct the JSON statement that will be sent to GitHub as the body of the web request. Include the name of the label, its color, and description. + # Convert hash table to JSON + $Body = @{} + $Body.Add("name", $Name) + $Body.Add("color", $Color) + $Body.Add("description", $description) + $Body = $Body | ConvertTo-Json + + # Try to submit the request to GitHub API to create the label + Try { + + Write-Host "Creating label $Name with color $Color on repo $RepoUri." + + $Result = Invoke-RestMethod -Uri $RepoUri -Headers $AppGitHubAccessHeaders -Body $Body -Method POST + + } Catch { + + Write-Host "ERROR: Failed to create new label $Name on repo $RepoUri. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Test-PrLabel + + Function Test-Prlabel { + + [CmdletBinding()] + param( + + $LabelArray, + $IssueUrl + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $IssueLabelUrl = "$IssueUrl/labels" + $LabelHashTable = @{} + $LabelResults = $Null + + # Get list of labels on issue/PR + Try { + + Write-Host "Getting labels on issue $IssueLabelUrl." + + $LabelResults = Invoke-RestMethod -Uri $IssueLabelUrl -Headers $AppGitHubAccessHeaders -ErrorAction Stop + + } Catch { + + Write-Host "ERROR: Failed to get list of labels on $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + } + + ForEach ($Label in $LabelArray) { + + If ($LabelResults -ne $Null) { + + If ($LabelResults.name.Contains($Label)) { + + $LabelHashTable.Add($Label, $True) + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } + + # Return array of labels on Issue/PR + Return $LabelHashTable + + } + + ##################### + ##################### + # Set-PrLabel + + Function Set-PrLabel { + + param( + + $IssueUrl, + $LabelName + + ) + + # Construct label URL based on issue or pull request URL + $IssueLabelUrl = "$IssueUrl/labels" + + # Construct JSON statement that will be sent to GitHub as the body of the web request. Includes only the label name. GitHub expects an array even thought it's a single value + # Convert array to JSON + $Body = @() + $Body += $LabelName + $Body = ConvertTo-Json -InputObject $Body + + # Try to submit the request to GitHub API to apply they label to the issue or pull request + Try { + + Write-Host "Setting label $LabelName on URL $IssueLabelUrl." + + $Result = Invoke-RestMethod -Uri $IssueLabelUrl -Body $Body -Headers $AppGitHubAccessHeaders -Method POST + + + } Catch { + + Write-Host "ERROR: Failed to set label on URL $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Get-TzAbbrev + + # Derive a 2- or 3-letter abbreviation from the zone's name + Function Get-TzAbbrev { + param( + [System.TimeZoneInfo]$Tz, + [DateTime]$LocalTime + ) + # Choose the right long name (standard vs. daylight) + $FullName = If ($Tz.IsDaylightSavingTime($LocalTime)) { + $Tz.DaylightName # e.g. “Pacific Daylight Time” + } Else { + $Tz.StandardName # e.g. “Pacific Standard Time” + } + + # Collapse it to initials: “Pacific Daylight Time” to “PDT” + -Join (($fullName -split '\s+') | ForEach-Object { $_[0] }) + } + + ##################### + ##################### + # Test-TargetBranchExists + + Function Test-TargetBranchExists { + + $BranchResponse = $Null + $BranchExists = $Null + + Write-Host "Checking if $TargetBranch exists" + + Try { + + $BranchResponse = Invoke-RestMethod -uri "https://api.github.com/repos/$Organization/$Repository/git/ref/heads/$TargetBranch" -Headers $StandardGitHubHeaders -Method Get -ErrorAction Stop + $BranchExists = $True + + } Catch { + + Write-Host "Branch doesn't exist or an error occurred. Error: $_." + + $BranchExists = $False + + } + + Return $BranchExists + + } + + ##################### + ##################### + # Get-BranchDiff + + Function Get-BranchDiff { + + $CompareResponse = $Null + + Write-Host "Checking if $DefaultBranch is ahead of $TargetBranch" + + Try { + + $CompareResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/compare/$TargetBranch...$DefaultBranch" -Method GET -Headers $StandardGitHubHeaders -ErrorAction Stop + + } Catch { + + $CompareResponse = $Null + + } + + Return $CompareResponse + + } + + ##################### + ##################### + # Get-PublishPullRequest + + Function Get-PublishPullRequest { + + $PrResponse = $Null + + Write-Host "Checking if a pull request exists between $DefaultBranch and $TargetBranch" + + Try { + + $PrResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/pulls?head=$($Organization):$DefaultBranch&base=$TargetBranch&state=open" -Method GET -Headers $StandardGitHubHeaders -ErrorAction Stop + + } Catch { + + Write-Host "Error: $_" + $PrResponse = $Null + + } + + Return $PrResponse + + } + + ##################### + ##################### + # New-PullRequest + + Function New-PullRequest { + + $PrResponse = $Null + + Write-Host "Creating a new PR from $DefaultBranch to $TargetBranch." + + # Create github HTTP authentication header using GitHub app installation token + $AppGitHubAccessToken = Get-GitHubAppInstallationToken -ClientId $ClientId -PrivateKey $PrivateKey -Organization $Organization -TokenTTLMinutes 10 + $AppGitHubAccessHeaders = @{} + $AppGitHubAccessHeaders.Add("Authorization","token $($AppGitHubAccessToken)") + $AppGitHubAccessHeaders.Add("User-Agent", "OfficeDocs") + + $PrTitle = "$PrTitle - $PacificStamp $(Get-TzAbbrev $PacificTz $PacificNow) | $IndiaStamp $(Get-TzAbbrev $IndiaTz $IndiaNow)" + + $PrBody = @{ + title = $PrTitle + head = $DefaultBranch + base = "$TargetBranch" + body = $PrDescription + } | ConvertTo-Json + + Try { + + $PrResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/pulls" -Method POST -Headers $AppGitHubAccessHeaders -Body $PrBody -ErrorAction Stop + + Write-Host "Created pull request $($PrResponse.html_url)" + + $IssueUrl = $PrResponse.issue_url + + # Check if labels exist on the repo. + $AutoLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel + $SignOffLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel + + If (!$AutoLabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel -Color $AutoPublishLabelColor -Description $AutoPublishLabelDescription + + } + + If (!$SignOffLabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel -Color $SignOffLabelColor -Description $SignOffLabelDescription + + } + + # Check to see if the labels we're interested in are already added to the PR. + $LabelResultsArray = Test-Prlabel -LabelArray $AutoPublishLabel,$SignOffLabel -IssueUrl $IssueUrl + + # Only add the AutoPublishLabel label if it doesn't already exist on the PR + If (!$LabelResultsArray.$AutoPublishLabel) { + + Write-Host "Label $AutoPublishLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $AutoPublishLabel + + } + + # Only add the SignOffLabel label if it doesn't already exist on the PR + + If (!$LabelResultsArray.$SignOffLabel) { + + Write-Host "Label $SignOffLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $SignOffLabel + + } + + } Catch { + + $PrResponse = $Null + + } + + Return $PrResponse + + } + + ##################### + ##################### + # Main + + If ($EnableAutoPublish) { + + If (Test-TargetBranchExists) { + + $PrData = Get-PublishPullRequest + + write-host $($PrData | ConvertTo-Json -Depth 50) + + # Check to see if $PrData contains data. If yes, there's a PR. If not, no PR. + If ($PrData) { + + # Will add PR processing in the future. + + Write-Host "Pull request found. Exiting." + + } Else { + + Write-Host "No pull request found. Checking diff between $DefaultBranch and $TargetBranch." + + # Check to see if $DefaultBranch contains anything that isn't in $TargetBranch. + $BranchDiff = Get-BranchDiff + + write-host "$DefaultBranch is ahead of $TargetBranch by $($BranchDiff.ahead_by) commits." + + # If there are changes in $DefaultBranch that aren't in $TargetBranch, create a PR. + If ($BranchDiff.ahead_by -gt 0) { + + Write-Host "$DefaultBranch has changes ahead of $TargetBranch" + + $NewPrResponse = New-PullRequest + + } Else { + + Write-Host "$DefaultBranch has no changes ahead of $TargetBranch. Not creating PR." + + } + + } + + } Else { + + Write-Host "The branch $TargetBranch doesn't exist or an error occurred." + + } + + } Else { + + Write-Host "Auto publishing is disabled. Exiting." + + } From f1e82a74b775c26ff443e81b857b029c23934d7f Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 10 Apr 2025 16:25:34 -0700 Subject: [PATCH 46/92] Rename AutoPublish workflow --- .github/workflows/{Shared-Publish.yml => Shared-AutoPublish.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{Shared-Publish.yml => Shared-AutoPublish.yml} (100%) diff --git a/.github/workflows/Shared-Publish.yml b/.github/workflows/Shared-AutoPublish.yml similarity index 100% rename from .github/workflows/Shared-Publish.yml rename to .github/workflows/Shared-AutoPublish.yml From c8d036d88a6352d2c5be53d3d5a252c848257c73 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:27:06 -0700 Subject: [PATCH 47/92] Add AutoPublish workflows, update protected files approver list --- .github/workflows/Shared-ProtectedFiles.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-ProtectedFiles.yml b/.github/workflows/Shared-ProtectedFiles.yml index f9e7504ca10..0a7d6d51401 100644 --- a/.github/workflows/Shared-ProtectedFiles.yml +++ b/.github/workflows/Shared-ProtectedFiles.yml @@ -37,9 +37,9 @@ jobs: ".gitattributes", "AutoLabelAssign.yml", "AutoLabelMsftContributor.yml", + "AutoPublish.yml", "BackgroundTasks.yml", "BuildValidation.yml", - "compare-live-main.yml", "LiveMergeCheck.yml", "M365Endpoints.yml", "PrFileCount.yml", @@ -50,6 +50,7 @@ jobs: "workflow-status-report.yml", "Shared-AutoLabelAssign.yml", "Shared-AutoLabelMsftContributor.yml", + "Shared-AutoPublish.yml", "Shared-BuildValidation.yml", "Shared-ExtractPayload.yml", "Shared-LiveMergeCheck.yml", @@ -59,7 +60,7 @@ jobs: "Shared-StaleBranch.yml", "Shared-TierManagement.yml" ]' - ApproverList: '["user1"]' + ApproverList: '["dstrome", "garycentric"]' PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} From 9e6e1cc8b28ecad6e4b0c45c00a0cc326fedcbda Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:14:51 -0700 Subject: [PATCH 48/92] Add 'DeleteOnDayOfMonth' functionality --- .github/workflows/Shared-StaleBranch.yml | 168 +++++++++++++++++------ 1 file changed, 129 insertions(+), 39 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index f5b97aaa7df..895d9e7a13b 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -1,5 +1,10 @@ name: (Scheduled) Stale branch removal +# This workflow is designed to be run in the days up to, and including, a "deletion day", specified by 'DeleteOnDayOfMonth' in env:. +# On the days leading up to "deletion day", the workflow will report the branches to be deleted. This lets users see which branches will be deleted. On "deletion day", those branches are deleted. +# The workflow should not be configured to run after "deletion day" so that users can review the branches were deleted. +# Recommendation: configure cron to run on days 15-31,1 where 1 is what's configured in 'DeleteOnDayOfMonth'. If 'DeleteOnDayOfMonth' is set to something else, update cron to run the two weeks leading up to it. + permissions: contents: write @@ -29,6 +34,7 @@ jobs: - name: Process branches shell: pwsh env: + DeleteOnDayOfMonth: 1 # Delete branches only on this day of the month. Only a single positive integer is allowed. DefaultSkipBranchList: '[ "live", "main", @@ -47,6 +53,7 @@ jobs: $DefaultSkipBranchList = $env:DefaultSkipBranchList | ConvertFrom-Json $RepoBranchSkipList = $env:RepoBranchSkipList | ConvertFrom-Json $ReportOnly = [System.Convert]::ToBoolean($env:ReportOnly) + $DeleteOnDayOfMonth = $env:DeleteOnDayOfMonth # Function to get the total number of elements in an array. Supports nested arrays. Function Get-TotalElements { @@ -68,21 +75,6 @@ jobs: return $count } - # Indicate in the script log if the workflow is in reporting mode or not. If in reporting mode, no changes will be made to the branches. - If ($ReportOnly) { - - Write-Host "`n`nRUNNING IN REPORTING MODE`n`n" - - $ReportOnlyString = "REPORT ONLY -" - - } Else { - - $ReportOnlyString = $Null - - } - - Write-Host "Default branch skip list: $DefaultSkipBranchList" - Write-Host "Repo branch skip list: $RepoBranchSkipList" # Create the branch skip list that is a combination of the central workflow list and the branch skip list that can be populated in each individual repo. $SkipBranchList = $DefaultSkipBranchList + $RepoBranchSkipList | Select-Object -Unique @@ -94,9 +86,12 @@ jobs: # !!! > 0 WILL RESULT IN DATA LOSS !!! $MaxDaysBehind = 90 + $DateLimit = (Get-Date).AddDays(-$MaxDaysBehind) $ReportDate = Get-Date -Format "dddd MMMM dd, yyyy" - + $CurrentDay = (Get-Date).Day + $RunMonth = (Get-Date).AddMonths((Get-Date).Day -ge $DeleteOnDayOfMonth).ToString('MMMM') # If current day is greater or equal to $DeleteOnDayOfMonth, flip to next month ($True = 1). If not, stay on current month ($False = 0). + # Create github HTTP authentication header $UserAgent = "officedocs" $GitHubHeaders = @{} @@ -129,6 +124,34 @@ jobs: $RetrieveBranchDataErrorCount = 0 $RetrieveBranchDataError = $False + # Workflow will only delete branches on the date of the month specified by $DeleteOnDayOfMonth. On all other days the workflow runs, it will only generate + # a report of what would have been deleted on that date. + If ($CurrentDay -eq $DeleteOnDayOfMonth) { + + $DeletionRun = $True + + } Else { + + $DeletionRun = $False + + } + + # Indicate in the script log if the workflow is in reporting mode or not. If in reporting mode, no changes will be made to the branches. + If ($ReportOnly) { + + Write-Host "`n`nRUNNING IN REPORTING MODE`n`n" + + $ReportOnlyString = "REPORT ONLY -" + + } Else { + + $ReportOnlyString = $Null + + } + + Write-Host "Default branch skip list: $DefaultSkipBranchList" + Write-Host "Repo branch skip list: $RepoBranchSkipList" + # Block the script from running if Ahead by is greater than 0 when maximum branch age is less than 90. Doing this could result in loss of data that may still be # wanted. Do not change the values unless you accept the high risk of unwanted data loss. If (($MaxCommitsAhead -gt 0) -and ($MaxDaysBehind -lt 90)) { @@ -143,7 +166,14 @@ jobs: Throw "ERROR: You can't set MaxDaysBehind to less than 30. Branch churn too high." } - + + # Make sure $DeleteOnDayOfMonth is a valid month day. Not allowing above 28 so we don't have to deal with leap years or days with 30/31 days. + If (($DeleteOnDayOfMonth -notin 1..28)) { + + Throw "ERROR: DeleteOnDayOfMonth must be between 1 and 28." + + } + ForEach ($Page in $Branches) { ForEach ($Branch in $Page) { @@ -290,19 +320,31 @@ jobs: # If the branch doesn't contain any commits not in $DefaultBranch, it's ok to delete. If ($AheadBy -eq 0) { - Write-Host " $ReportOnlyString Delete branch $BranchName" + # Delete branch if this is a deletion run. Otherwise indicate that branch is pending deletion in a future run. + If ($DeletionRun) { + + Write-Host " $ReportOnlyString Delete branch $BranchName" + + # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. + If (!$ReportOnly) { + + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + + } - # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. - If (!$ReportOnly) { + $BranchReportObject.ProcessingResult = "Deleted" + + } Else { + + Write-Host " Pending delete branch $BranchName" - Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + $BranchReportObject.ProcessingResult = "PendingDelete" + } # Update the reporting object. - $BranchReportObject.ProcessingResult = "Deleted" $ReportBranchList += $BranchReportObject - $DeleteBranchCount++ } Else { @@ -311,30 +353,52 @@ jobs: # the branch should actually be deleted. This is because deleting the branch will result in data loss. If ($AllowDataLoss) { - Write-Host " $ReportOnlyString Delete branch $BranchName with data loss" + If ($DeletionRun) { - # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. - If (!$ReportOnly) { + Write-Host " $ReportOnlyString Delete branch $BranchName with data loss" - Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. + If (!$ReportOnly) { + + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + } + + $BranchReportObject.ProcessingResult = "DataLoss-Deleted" + + + } Else { + + Write-Host " Pending delete branch $BranchName with data loss" + + $BranchReportObject.ProcessingResult = "PendingDataLoss-Delete" + } # Update the reporting object. - $BranchReportObject.ProcessingResult = "DataLoss-Deleted" $ReportBranchList += $BranchReportObject - $DeleteBranchCount++ $DataLossCount++ } Else { - Write-Host " $ReportOnlyString Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." + If ($DeletionRun) { + + Write-Host " $ReportOnlyString Branch $BranchName was marked for deletion with data loss but data loss flag is disabled." + + $BranchReportObject.ProcessingResult = "DataLoss-Blocked" + + + } Else { + + Write-Host " Pending branch $BranchName was marked for deletion with data loss but data loss flag is disabled." + + $BranchReportObject.ProcessingResult = "PendingDataLoss-Blocked" + + } # Update the reporting object. - $BranchReportObject.ProcessingResult = "DataLoss-Blocked" $ReportBranchList += $BranchReportObject - $DataLossBlockedCount++ } # AllowDataLoss If @@ -392,12 +456,22 @@ jobs: If ($DeleteBranchlistCount -gt 0) { - echo "The following branches were deleted because they were over $MaxDaysBehind days behind the $DefaultBranch branch and contained $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + If ($DeletionRun) { + + echo "The following branches were deleted because they were over $MaxDaysBehind days behind the $DefaultBranch branch and contained $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY - If ($ReportOnly) { + If ($ReportOnly) { + + echo "**REPORTING MODE**: Reporting mode is currently enabled. No branches were deleted during this run." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + + } + + } Else { - echo "**REPORTING MODE**: Reporting mode is currently enabled. No branches were deleted during this run." >> $env:GITHUB_STEP_SUMMARY + echo "The following branches will be deleted on **$RunMonth $DeleteOnDayOfMonth** because they are over $MaxDaysBehind days behind the $DefaultBranch branch and contain $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY + echo "**If you don't want a branch to be deleted, merge $DefaultBranch into it before $RunMonth $DeleteOnDayOfMonth.**" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY } @@ -481,6 +555,11 @@ jobs: Write-Host $ReportOnlyMode echo $ReportOnlyMode >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY + + $DeletionRunMode = "Deletion run: $DeletionRun" + Write-Host $DeletionRunMode + echo $DeletionRunMode >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY $AllowDataLossSetting = "Allow data loss: $AllowDataLoss" Write-Host $AllowDataLossSetting @@ -540,7 +619,20 @@ jobs: echo "$WatchListBranches" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY - $DataLossBlockedBranches = "$ReportOnlyString Data loss blocked branches: $DataLossBlockedCount" + If ($DeletionRun) { + + $DataLossBlockedBranches = "$ReportOnlyString Data loss blocked branches: $DataLossBlockedCount" + $BranchesDeletedWithDataLoss = "$ReportOnlyString Branches deleted with data loss: $DataLossCount" + $TotalDeletedBranches = "$ReportOnlyString Total deleted branches: $DeleteBranchCount" + + } Else { + + $DataLossBlockedBranches = "Branches pending deletion (data loss blocked): $DataLossBlockedCount" + $BranchesDeletedWithDataLoss = "Branches pending deletion (data loss): $DataLossCount" + $TotalDeletedBranches = "Total branches pending deletion: $DeleteBranchCount" + + } + Write-Host $DataLossBlockedBranches echo "$DataLossBlockedBranches" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY @@ -549,12 +641,10 @@ jobs: echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY - $BranchesDeletedWithDataLoss = "$ReportOnlyString Branches deleted with data loss: $DataLossCount" Write-Host $BranchesDeletedWithDataLoss echo "$BranchesDeletedWithDataLoss" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY - $TotalDeletedBranches = "$ReportOnlyString Total deleted branches: $DeleteBranchCount" Write-Host $TotalDeletedBranches echo "$TotalDeletedBranches" >> $env:GITHUB_STEP_SUMMARY From 152d1fab49f493c19d3614e240b24abd0b0b6a7c Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:09:16 -0700 Subject: [PATCH 49/92] Add check for PubOps/non-PubOps repos --- .github/workflows/Shared-AutoPublish.yml | 161 ++++++++++++++++------- 1 file changed, 115 insertions(+), 46 deletions(-) diff --git a/.github/workflows/Shared-AutoPublish.yml b/.github/workflows/Shared-AutoPublish.yml index 680323b665d..c8732b95d99 100644 --- a/.github/workflows/Shared-AutoPublish.yml +++ b/.github/workflows/Shared-AutoPublish.yml @@ -36,6 +36,7 @@ jobs: EnableAutoPublish: ${{ inputs.EnableAutoPublish }} PrivateKey: ${{ secrets.PrivateKey }} ClientId: ${{ secrets.ClientId }} + IsPubOpsRepo: ${{ contains(github.event.repository.topics, 'pubops') }} run: | @@ -44,6 +45,7 @@ jobs: $AccessToken = $env:AccessToken $PrivateKey = $env:PrivateKey $ClientId = $env:ClientId + $IsPubOpsRepo = [System.Convert]::ToBoolean($env:IsPubOpsRepo) # Date/Time calculations $UtcNow = Get-Date -AsUTC @@ -70,7 +72,8 @@ jobs: # PR variables $PrTitle = "[AutoPublish] $DefaultBranch to $TargetBranch" - $PrDescription = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`nBefore merging this PR, complete the following checks:`n`n- There are no more than 100 files in the PR. If there are more than 100 files, review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- There are no build warnings. If there are warnings, resolve them before merging the PR.`n- Regular PR criteria checks pass.`n`nIf you have questions, email marveldocs-admins." + $PrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`nBefore merging this PR, complete the following checks:`n`n- There are no more than 100 files in the PR. If there are more than 100 files, review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- There are no build warnings. If there are warnings, resolve them before merging the PR.`n- Regular PR criteria checks pass.`n`nIf you have questions, email marveldocs-admins." + $PrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch.`n`nThis is repo isn't managed by PubOps.`n`n**A contributor with write access to this repo needs to merge this PR for changes in it to go live.**`n`nBefore you merge this PR:`n`n- Check the **Files changed** tab to ensure only changes you intend to publish are included in the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site`n`nIf you have questions, email marveldocs-admins." # Label variables $AutoPublishLabelColor = "5319E7" @@ -79,6 +82,9 @@ jobs: $SignOffLabelColor = "46ce1c" $SignOffLabelDescription = "The pull request is ready to be reviewed and merged by PubOps." $SignOffLabel = "Sign off" + $ManualMergeLabelColor = "FF6600" + $ManualMergeLabelDescription = "An AutoPublish pull request requires a human to merge it." + $ManualMergeLabel = "Manual merge required" ##################### ##################### @@ -229,7 +235,7 @@ jobs: Write-Host "Checking to see if label $Name exists in repo URL $LabelUri." - $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $AppGitHubAccessHeaders -ErrorAction Stop + $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $StandardGitHubHeaders -ErrorAction Stop $LabelFound = $True } Catch { @@ -276,7 +282,7 @@ jobs: Write-Host "Creating label $Name with color $Color on repo $RepoUri." - $Result = Invoke-RestMethod -Uri $RepoUri -Headers $AppGitHubAccessHeaders -Body $Body -Method POST + $Result = Invoke-RestMethod -Uri $RepoUri -Headers $StandardGitHubHeaders -Body $Body -Method POST } Catch { @@ -309,7 +315,7 @@ jobs: Write-Host "Getting labels on issue $IssueLabelUrl." - $LabelResults = Invoke-RestMethod -Uri $IssueLabelUrl -Headers $AppGitHubAccessHeaders -ErrorAction Stop + $LabelResults = Invoke-RestMethod -Uri $IssueLabelUrl -Headers $StandardGitHubHeaders -ErrorAction Stop } Catch { @@ -371,7 +377,7 @@ jobs: Write-Host "Setting label $LabelName on URL $IssueLabelUrl." - $Result = Invoke-RestMethod -Uri $IssueLabelUrl -Body $Body -Headers $AppGitHubAccessHeaders -Method POST + $Result = Invoke-RestMethod -Uri $IssueLabelUrl -Body $Body -Headers $StandardGitHubHeaders -Method POST } Catch { @@ -482,47 +488,42 @@ jobs: ##################### ##################### - # New-PullRequest + # Set-Labels - Function New-PullRequest { + Function Set-Labels { + + param( - $PrResponse = $Null + [Parameter(Mandatory = $True)] + [string]$IssueUrl + + ) - Write-Host "Creating a new PR from $DefaultBranch to $TargetBranch." + # Check if labels exist on the repo. + $AutoLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel - # Create github HTTP authentication header using GitHub app installation token - $AppGitHubAccessToken = Get-GitHubAppInstallationToken -ClientId $ClientId -PrivateKey $PrivateKey -Organization $Organization -TokenTTLMinutes 10 - $AppGitHubAccessHeaders = @{} - $AppGitHubAccessHeaders.Add("Authorization","token $($AppGitHubAccessToken)") - $AppGitHubAccessHeaders.Add("User-Agent", "OfficeDocs") - - $PrTitle = "$PrTitle - $PacificStamp $(Get-TzAbbrev $PacificTz $PacificNow) | $IndiaStamp $(Get-TzAbbrev $IndiaTz $IndiaNow)" + If (!$AutoLabelExists) { - $PrBody = @{ - title = $PrTitle - head = $DefaultBranch - base = "$TargetBranch" - body = $PrDescription - } | ConvertTo-Json + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel -Color $AutoPublishLabelColor -Description $AutoPublishLabelDescription - Try { + } - $PrResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/pulls" -Method POST -Headers $AppGitHubAccessHeaders -Body $PrBody -ErrorAction Stop + # Only add the AutoPublishLabel label if it doesn't already exist on the PR + If (!$LabelResultsArray.$AutoPublishLabel) { - Write-Host "Created pull request $($PrResponse.html_url)" + Write-Host "Label $AutoPublishLabel doesn't exist on $IssueUrl. Adding label." - $IssueUrl = $PrResponse.issue_url + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $AutoPublishLabel - # Check if labels exist on the repo. - $AutoLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel - $SignOffLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel + } - If (!$AutoLabelExists) { + If ($IsPubOpsRepo) { - # Create label on the repo if it doesn't exist. - New-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel -Color $AutoPublishLabelColor -Description $AutoPublishLabelDescription + Write-Host "PubOps repo. Checking and setting $SignOffLabel label." - } + $SignOffLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel If (!$SignOffLabelExists) { @@ -531,33 +532,94 @@ jobs: } - # Check to see if the labels we're interested in are already added to the PR. - $LabelResultsArray = Test-Prlabel -LabelArray $AutoPublishLabel,$SignOffLabel -IssueUrl $IssueUrl - # Only add the AutoPublishLabel label if it doesn't already exist on the PR - If (!$LabelResultsArray.$AutoPublishLabel) { + # Only add the SignOffLabel label if it doesn't already exist on the PR - Write-Host "Label $AutoPublishLabel doesn't exist on $IssueUrl. Adding label." + If (!$LabelResultsArray.$SignOffLabel) { + + Write-Host "Label $SignOffLabel doesn't exist on $IssueUrl. Adding label." # Add the label to the PR - Set-PrLabel -IssueUrl $IssueUrl -LabelName $AutoPublishLabel + Set-PrLabel -IssueUrl $IssueUrl -LabelName $SignOffLabel } - # Only add the SignOffLabel label if it doesn't already exist on the PR - If (!$LabelResultsArray.$SignOffLabel) { - Write-Host "Label $SignOffLabel doesn't exist on $IssueUrl. Adding label." + } Else { + + Write-Host "Non-PubOps repo. Checking and setting $ManualMergeLabel label." + + $ManualMergeLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $ManualMergeLabel + + If (!$ManualMergeLabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $ManualMergeLabel -Color $ManualMergeLabelColor -Description $ManualMergeLabelDescription + + } + + # Only add the ManualMergeLabel label if it doesn't already exist on the PR + + If (!$LabelResultsArray.$ManualMergeLabel) { + + Write-Host "Label $ManualMergeLabel doesn't exist on $IssueUrl. Adding label." # Add the label to the PR - Set-PrLabel -IssueUrl $IssueUrl -LabelName $SignOffLabel + Set-PrLabel -IssueUrl $IssueUrl -LabelName $ManualMergeLabel } + } + + } + + ##################### + ##################### + # New-PullRequest + + Function New-PullRequest { + + $PrResponse = $Null + + Write-Host "Creating a new PR from $DefaultBranch to $TargetBranch." + + # Create github HTTP authentication header using GitHub app installation token + $AppGitHubAccessToken = Get-GitHubAppInstallationToken -ClientId $ClientId -PrivateKey $PrivateKey -Organization $Organization -TokenTTLMinutes 10 + $AppGitHubAccessHeaders = @{} + $AppGitHubAccessHeaders.Add("Authorization","token $($AppGitHubAccessToken)") + $AppGitHubAccessHeaders.Add("User-Agent", "OfficeDocs") + + $PrTitle = "$PrTitle - $PacificStamp $(Get-TzAbbrev $PacificTz $PacificNow) | $IndiaStamp $(Get-TzAbbrev $IndiaTz $IndiaNow)" + + If ($IsPubOpsRepo) { + + $PrDescription = $PrDescriptionPubOps + + } Else { + + $PrDescription = $PrDescriptionNonPubOps + + } + + $PrBody = @{ + title = $PrTitle + head = $DefaultBranch + base = "$TargetBranch" + body = $PrDescription + } | ConvertTo-Json + + Try { + + $PrResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/pulls" -Method POST -Headers $AppGitHubAccessHeaders -Body $PrBody -ErrorAction Stop + + Write-Host "Created pull request $($PrResponse.html_url)" + } Catch { $PrResponse = $Null + + Write-Host "ERROR occurred during PR creation. Error: $_" } @@ -565,6 +627,7 @@ jobs: } + ##################### ##################### # Main @@ -575,8 +638,6 @@ jobs: $PrData = Get-PublishPullRequest - write-host $($PrData | ConvertTo-Json -Depth 50) - # Check to see if $PrData contains data. If yes, there's a PR. If not, no PR. If ($PrData) { @@ -597,9 +658,17 @@ jobs: If ($BranchDiff.ahead_by -gt 0) { Write-Host "$DefaultBranch has changes ahead of $TargetBranch" + Write-Host "PubOps repo: $IsPubOpsRepo" $NewPrResponse = New-PullRequest - + $IssueUrl = $NewPrResponse.issue_url + + If ($IssueUrl) { + + Set-Labels -IssueUrl $IssueUrl + + } + } Else { Write-Host "$DefaultBranch has no changes ahead of $TargetBranch. Not creating PR." From 84bc19e25616648d419b94546e718db0a960eb72 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 17 Apr 2025 17:13:48 -0700 Subject: [PATCH 50/92] Correct manual merge label color --- .github/workflows/Shared-AutoPublish.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Shared-AutoPublish.yml b/.github/workflows/Shared-AutoPublish.yml index c8732b95d99..c7ab88405ae 100644 --- a/.github/workflows/Shared-AutoPublish.yml +++ b/.github/workflows/Shared-AutoPublish.yml @@ -82,7 +82,7 @@ jobs: $SignOffLabelColor = "46ce1c" $SignOffLabelDescription = "The pull request is ready to be reviewed and merged by PubOps." $SignOffLabel = "Sign off" - $ManualMergeLabelColor = "FF6600" + $ManualMergeLabelColor = "f79d43" $ManualMergeLabelDescription = "An AutoPublish pull request requires a human to merge it." $ManualMergeLabel = "Manual merge required" From 04878a193251bbcfdb7cf5ca6e7ed9ae6aead53e Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:12:48 -0700 Subject: [PATCH 51/92] Add report/delete functionality --- .github/workflows/Shared-StaleBranch.yml | 161 +++++++++++++---------- 1 file changed, 90 insertions(+), 71 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 895d9e7a13b..307fc0c4715 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -75,6 +75,37 @@ jobs: return $count } + Function Get-NextDeletionDate { + + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + [ValidateRange(1,31)] + [Int]$DeletionDayOfMonth + ) + + $Today = Get-Date + + # Use this month if today’s date is on or before the deletion day; + # otherwise move to next month (AddMonths handles year rollover) + If ($Today.Day -le $DeletionDayOfMonth) { + $TargetMonth = $Today.Month + $TargetYear = $Today.Year + } Else { + $Next = $Today.AddMonths(1) + $TargetMonth = $Next.Month + $TargetYear = $Next.Year + } + + # Clamp to last day of month if necessary + $DaysInMonth = [DateTime]::DaysInMonth($TargetYear, $TargetMonth) + $TargetDay = [Math]::Min($DeletionDayOfMonth, $DaysInMonth) + + # Return midnight on the target day + Get-Date -Year $TargetYear -Month $TargetMonth -Day $TargetDay -Hour 0 -Minute 0 -Second 0 + } + + # Create the branch skip list that is a combination of the central workflow list and the branch skip list that can be populated in each individual repo. $SkipBranchList = $DefaultSkipBranchList + $RepoBranchSkipList | Select-Object -Unique @@ -87,11 +118,12 @@ jobs: $MaxDaysBehind = 90 - $DateLimit = (Get-Date).AddDays(-$MaxDaysBehind) - $ReportDate = Get-Date -Format "dddd MMMM dd, yyyy" - $CurrentDay = (Get-Date).Day - $RunMonth = (Get-Date).AddMonths((Get-Date).Day -ge $DeleteOnDayOfMonth).ToString('MMMM') # If current day is greater or equal to $DeleteOnDayOfMonth, flip to next month ($True = 1). If not, stay on current month ($False = 0). - + $DeletionDate = Get-NextDeletionDate -DeletionDayOfMonth $DeleteOnDayOfMonth + $DateLimit = $DeletionDate.AddDays(-$MaxDaysBehind) + $ReportDate = Get-Date -Format "dddd MMMM d, yyyy" + $CurrentDate = Get-Date + $FriendlyDeletionDate = $DeletionDate.ToString('MMMM d') + # Create github HTTP authentication header $UserAgent = "officedocs" $GitHubHeaders = @{} @@ -124,9 +156,9 @@ jobs: $RetrieveBranchDataErrorCount = 0 $RetrieveBranchDataError = $False - # Workflow will only delete branches on the date of the month specified by $DeleteOnDayOfMonth. On all other days the workflow runs, it will only generate + # Workflow will only delete branches on $DeletionDate. On all other days the workflow runs, it will only generate # a report of what would have been deleted on that date. - If ($CurrentDay -eq $DeleteOnDayOfMonth) { + If ($CurrentDate.Date -eq $DeletionDate.Date) { $DeletionRun = $True @@ -167,13 +199,6 @@ jobs: } - # Make sure $DeleteOnDayOfMonth is a valid month day. Not allowing above 28 so we don't have to deal with leap years or days with 30/31 days. - If (($DeleteOnDayOfMonth -notin 1..28)) { - - Throw "ERROR: DeleteOnDayOfMonth must be between 1 and 28." - - } - ForEach ($Page in $Branches) { ForEach ($Branch in $Page) { @@ -328,7 +353,7 @@ jobs: # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. If (!$ReportOnly) { - Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + #Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null } @@ -360,7 +385,7 @@ jobs: # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. If (!$ReportOnly) { - Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + #Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null } @@ -451,7 +476,16 @@ jobs: $DeleteBranchListCount = $DeleteBranchList.Count $WatchBranchListCount = $WatchBranchList.Count - echo "## Deleted stale branches" >> $env:GITHUB_STEP_SUMMARY + If ($DeletionRun) { + + echo "## Deleted stale branches" >> $env:GITHUB_STEP_SUMMARY + + } Else { + + echo "## Stale branches pending deletion" >> $env:GITHUB_STEP_SUMMARY + + } + echo "" >> $env:GITHUB_STEP_SUMMARY If ($DeleteBranchlistCount -gt 0) { @@ -470,8 +504,10 @@ jobs: } Else { - echo "The following branches will be deleted on **$RunMonth $DeleteOnDayOfMonth** because they are over $MaxDaysBehind days behind the $DefaultBranch branch and contain $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY - echo "**If you don't want a branch to be deleted, merge $DefaultBranch into it before $RunMonth $DeleteOnDayOfMonth.**" >> $env:GITHUB_STEP_SUMMARY + echo "The following branches will be deleted on **$FriendlyDeletionDate** because they will be over $MaxDaysBehind days behind the $DefaultBranch branch on that date. They also contain $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY + echo "> [!IMPORTANT]" >> $env:GITHUB_STEP_SUMMARY + echo "> **If you don't want a branch to be deleted, merge $DefaultBranch into it before $FriendlyDeletionDate.**" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY } @@ -500,7 +536,7 @@ jobs: } Else { - echo "No branches were deleted during this run." >> $env:GITHUB_STEP_SUMMARY + echo "No branches were deleted or were identified as pending deletion during this run." >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY } @@ -551,99 +587,82 @@ jobs: echo "## Workflow overview" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY - $ReportOnlyMode = "Report only mode: $ReportOnly" + $ReportOnlyMode = "**Report only mode**: $ReportOnly" Write-Host $ReportOnlyMode - echo $ReportOnlyMode >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$ReportOnlyMode\" >> $env:GITHUB_STEP_SUMMARY - $DeletionRunMode = "Deletion run: $DeletionRun" + $DeletionRunMode = "**Deletion run**: $DeletionRun" Write-Host $DeletionRunMode - echo $DeletionRunMode >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$DeletionRunMode\" >> $env:GITHUB_STEP_SUMMARY - $AllowDataLossSetting = "Allow data loss: $AllowDataLoss" + $AllowDataLossSetting = "**Allow data loss**: $AllowDataLoss" Write-Host $AllowDataLossSetting - echo "$AllowDataLossSetting" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$AllowDataLossSetting\" >> $env:GITHUB_STEP_SUMMARY - $MaximumCommitsAheadByLimit = "Maximum commits ahead by limit: $MaxCommitsAhead" + $MaximumCommitsAheadByLimit = "**Maximum commits ahead by limit**: $MaxCommitsAhead" Write-Host $MaximumCommitsAheadByLimit - echo "$MaximumCommitsAheadByLimit" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$MaximumCommitsAheadByLimit\" >> $env:GITHUB_STEP_SUMMARY - $MaximumDaysBehindLimit = "Maximum days behind limit: $MaxDaysBehind" + $MaximumDaysBehindLimit = "**Maximum days behind limit**: $MaxDaysBehind" Write-Host $MaximumDaysBehindLimit - echo "$MaximumDaysBehindLimit" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$MaximumDaysBehindLimit\" >> $env:GITHUB_STEP_SUMMARY - $MaximumBranchAgeBasedOnDaysBehindLimit = "Maximum branch age based on days behind limit: $DateLimit" + $MaximumBranchAgeBasedOnDaysBehindLimit = "**Maximum branch age based on days behind limit**: $DateLimit" Write-Host $MaximumBranchAgeBasedOnDaysBehindLimit - echo "$MaximumBranchAgeBasedOnDaysBehindLimit" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$MaximumBranchAgeBasedOnDaysBehindLimit\" >> $env:GITHUB_STEP_SUMMARY $SeparatorLine = "===========" Write-Host $SeparatorLine - echo $SeparatorLine >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$SeparatorLine\" >> $env:GITHUB_STEP_SUMMARY - $DefaultBranchSkipList = "Default branch skip list: $DefaultSkipBranchList" + $DefaultBranchSkipList = "**Default branch skip list**: $DefaultSkipBranchList" Write-Host $DefaultBranchSkipList - echo $DefaultBranchSkipList >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$DefaultBranchSkipList\" >> $env:GITHUB_STEP_SUMMARY - $RepoBranchSkipListText = "Repo branch skip list: $RepoBranchSkipList" + $RepoBranchSkipListText = "**Repo branch skip list**: $RepoBranchSkipList" Write-Host $RepoBranchSkipListText - echo "$RepoBranchSkipListText" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$RepoBranchSkipListText\" >> $env:GITHUB_STEP_SUMMARY Write-Host $SeparatorLine - echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$SeparatorLine\" >> $env:GITHUB_STEP_SUMMARY - $TotalBranchesBeforeRun = "Total branches before run: $StartBranchCount" + $TotalBranchesBeforeRun = "**Total branches before run**: $StartBranchCount" Write-Host $TotalBranchesBeforeRun - echo "$TotalBranchesBeforeRun" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$TotalBranchesBeforeRun\" >> $env:GITHUB_STEP_SUMMARY - $TotalBranchesAfterRun = "Total branches after run: $EndBranchCount" + $TotalBranchesAfterRun = "**Total branches after run**: $EndBranchCount" Write-Host $TotalBranchesAfterRun - echo "$TotalBranchesAfterRun" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$TotalBranchesAfterRun\" >> $env:GITHUB_STEP_SUMMARY Write-Host $SeparatorLine - echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$SeparatorLine\" >> $env:GITHUB_STEP_SUMMARY - $WatchListBranches = "Watch list branches: $WatchListCount" + $WatchListBranches = "**Watch list branches**: $WatchListCount" Write-Host $WatchListBranches - echo "$WatchListBranches" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$WatchListBranches\" >> $env:GITHUB_STEP_SUMMARY If ($DeletionRun) { - $DataLossBlockedBranches = "$ReportOnlyString Data loss blocked branches: $DataLossBlockedCount" - $BranchesDeletedWithDataLoss = "$ReportOnlyString Branches deleted with data loss: $DataLossCount" - $TotalDeletedBranches = "$ReportOnlyString Total deleted branches: $DeleteBranchCount" + $DataLossBlockedBranches = "**$ReportOnlyString Data loss blocked branches**: $DataLossBlockedCount" + $BranchesDeletedWithDataLoss = "**$ReportOnlyString Branches deleted with data loss**: $DataLossCount" + $TotalDeletedBranches = "**$ReportOnlyString Total deleted branches**: $DeleteBranchCount" } Else { - $DataLossBlockedBranches = "Branches pending deletion (data loss blocked): $DataLossBlockedCount" - $BranchesDeletedWithDataLoss = "Branches pending deletion (data loss): $DataLossCount" - $TotalDeletedBranches = "Total branches pending deletion: $DeleteBranchCount" + $DataLossBlockedBranches = "**Branches pending deletion (data loss blocked)**: $DataLossBlockedCount" + $BranchesDeletedWithDataLoss = "**Branches pending deletion (data loss)**: $DataLossCount" + $TotalDeletedBranches = "**Total branches pending deletion**: $DeleteBranchCount" } Write-Host $DataLossBlockedBranches - echo "$DataLossBlockedBranches" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$DataLossBlockedBranches\" >> $env:GITHUB_STEP_SUMMARY Write-Host $SeparatorLine - echo "$SeparatorLine" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$SeparatorLine\" >> $env:GITHUB_STEP_SUMMARY Write-Host $BranchesDeletedWithDataLoss - echo "$BranchesDeletedWithDataLoss" >> $env:GITHUB_STEP_SUMMARY - echo "" >> $env:GITHUB_STEP_SUMMARY + echo "$BranchesDeletedWithDataLoss\" >> $env:GITHUB_STEP_SUMMARY Write-Host $TotalDeletedBranches echo "$TotalDeletedBranches" >> $env:GITHUB_STEP_SUMMARY From 415d2078bdc8bf535e24c360f387a3cba89c04bd Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 22 Apr 2025 19:20:32 -0700 Subject: [PATCH 52/92] hyperlink BC --- .github/workflows/Shared-StaleBranch.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 307fc0c4715..aa2cddedd0b 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -142,6 +142,7 @@ jobs: $CompareUrl = $RepoData.compare_url.Replace("{base}...{head}", "$DefaultBranch...") $GitHubGraphQlUrl = "https://api.github.com/graphql" $BranchesHtmlUrl = "$($GitHubData.event.repository.html_url)/branches" + $GitHubBaseUrl = "https://github.com" # Get the list of branches to process and set the initial branch count. $Branches = Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchesUrl -Method GET -FollowRelLink -MaximumFollowRelLink 50 -ResponseHeadersVariable ResponseHeaders @@ -530,7 +531,7 @@ jobs: $BC = $BranchReport.BranchCreator Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. " - echo "| $BN | $BC | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY + echo "| $BN | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY } @@ -570,7 +571,7 @@ jobs: $BranchDiffHtmlUrl = "$($GitHubData.event.repository.html_url)/compare/$DefaultBranch...$($BN)#files_bucket" Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. " - echo "| [$BN]($BranchDiffHtmlUrl) | $BC | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY + echo "| [$BN]($BranchDiffHtmlUrl) | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY } From 56e3ae924f805b3f833d1be948d1a09967739372 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 23 Apr 2025 11:38:46 -0700 Subject: [PATCH 53/92] Add days since last commit as of deletion day to tables, center columns --- .github/workflows/Shared-StaleBranch.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index aa2cddedd0b..90e84941947 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -305,6 +305,7 @@ jobs: AheadBy = $AheadBy BehindBy = $BehindBy DaysSinceLastCommit = $($(Get-Date) - $LastCommitDate).Days + DaysSinceLastCommitOnDeleteDay = $($DeletionDate - $LastCommitDate).Days ProcessingResult = $Null BranchCreator = $BranchCreator } @@ -452,8 +453,8 @@ jobs: # Branch processing has completed and, from this point on, reporting is generated. # Construct the markdown table header that'll be used in the workflow summary. - $TableHeaderRow1 = "| Branch name | Branch creator | Commits ahead by | Commits behind by | Days since last commit | Processing result |" - $TableHeaderRow2 = "|-------------|----------------|------------------|-------------------|------------------------|-------------------|" + $TableHeaderRow1 = "| Branch name | Branch creator | Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) | Processing result |" + $TableHeaderRow2 = "|-------------|----------------|:----------------:|:-----------------:|:----------------------------------------------------:|-------------------|" # Set job summary and create the "Deleted stale branches" section. echo "# Stale branch results" >> $env:GITHUB_STEP_SUMMARY @@ -507,6 +508,10 @@ jobs: echo "The following branches will be deleted on **$FriendlyDeletionDate** because they will be over $MaxDaysBehind days behind the $DefaultBranch branch on that date. They also contain $MaxCommitsAhead or fewer commits not in the $DefaultBranch branch." >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY + echo "**Days since last commit** shows two values:" >> $env:GITHUB_STEP_SUMMARY + echo "- The number of days between the most recent commit and the date the workflow runs." >> $env:GITHUB_STEP_SUMMARY + echo "- The projected number of days between the most recent commit and the scheduled branch-deletion date, $FriendlyDeletionDate, shown in parentheses." >> $env:GITHUB_STEP_SUMMARY + echo "" >> $env:GITHUB_STEP_SUMMARY echo "> [!IMPORTANT]" >> $env:GITHUB_STEP_SUMMARY echo "> **If you don't want a branch to be deleted, merge $DefaultBranch into it before $FriendlyDeletionDate.**" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY @@ -527,11 +532,12 @@ jobs: $AB = $BranchReport.AheadBy $BB = $BranchReport.BehindBy $LC = $BranchReport.DaysSinceLastCommit + $DD = $BranchReport.DaysSinceLastCommitOnDeleteDay $PR = $BranchReport.ProcessingResult $BC = $BranchReport.BranchCreator - Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. " - echo "| $BN | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY + Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. On $FriendlyDeletionDate`: ($DD)." + echo "| $BN | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY } @@ -565,13 +571,14 @@ jobs: $AB = $BranchReport.AheadBy $BB = $BranchReport.BehindBy $LC = $BranchReport.DaysSinceLastCommit + $DD = $BranchReport.DaysSinceLastCommitOnDeleteDay $PR = $BranchReport.ProcessingResult $BC = $BranchReport.BranchCreator $BranchDiffHtmlUrl = "$($GitHubData.event.repository.html_url)/compare/$DefaultBranch...$($BN)#files_bucket" - Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. " - echo "| [$BN]($BranchDiffHtmlUrl) | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC | $PR |" >> $env:GITHUB_STEP_SUMMARY + Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. On $FriendlyDeletionDate`: ($DD)." + echo "| [$BN]($BranchDiffHtmlUrl) | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY } From f11c95316eeb4383d6eb0e4b22a7258281857c7a Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:03:07 -0700 Subject: [PATCH 54/92] Remove creator from delete table --- .github/workflows/Shared-StaleBranch.yml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 90e84941947..c42b53378ab 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -452,9 +452,12 @@ jobs: ####### # Branch processing has completed and, from this point on, reporting is generated. - # Construct the markdown table header that'll be used in the workflow summary. - $TableHeaderRow1 = "| Branch name | Branch creator | Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) | Processing result |" - $TableHeaderRow2 = "|-------------|----------------|:----------------:|:-----------------:|:----------------------------------------------------:|-------------------|" + # Construct the markdown table headers that'll be used in the workflow summary. + $DeleteTableHeaderRow1 = "| Branch name | Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) | Processing result |" + $DeleteTableHeaderRow2 = "|-------------|:----------------:|:-----------------:|:----------------------------------------------------:|-------------------|" + + $WatchTableHeaderRow1 = "| Branch name | Branch creator | Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) | Processing result |" + $WatchTableHeaderRow2 = "|-------------|----------------|:----------------:|:-----------------:|:----------------------------------------------------:|-------------------|" # Set job summary and create the "Deleted stale branches" section. echo "# Stale branch results" >> $env:GITHUB_STEP_SUMMARY @@ -519,8 +522,8 @@ jobs: } # Start to build the branch action taken table in the workflow summary. - echo $TableHeaderRow1 >> $env:GITHUB_STEP_SUMMARY - echo $TableHeaderRow2 >> $env:GITHUB_STEP_SUMMARY + echo $DeleteTableHeaderRow1 >> $env:GITHUB_STEP_SUMMARY + echo $DeleteTableHeaderRow2 >> $env:GITHUB_STEP_SUMMARY # Loop through every reporting object in the array where the processing result isn't "Watch". Add a row for each branch @@ -537,7 +540,7 @@ jobs: $BC = $BranchReport.BranchCreator Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. On $FriendlyDeletionDate`: ($DD)." - echo "| $BN | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY + echo "| $BN | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY } @@ -559,8 +562,8 @@ jobs: echo "These branches should be reviewed and, if they're no longer needed, deleted. If these branches are still needed, they must be brought up to date with the $DefaultBranch branch. Select a branch name to view the differences between it and $($DefaultBranch)." >> $env:GITHUB_STEP_SUMMARY echo "**Branches in this list may be deleted at a future date even if they contain commits not in the $DefaultBranch branch.**" >> $env:GITHUB_STEP_SUMMARY echo "" >> $env:GITHUB_STEP_SUMMARY - echo $TableHeaderRow1 >> $env:GITHUB_STEP_SUMMARY - echo $TableHeaderRow2 >> $env:GITHUB_STEP_SUMMARY + echo $WatchTableHeaderRow1 >> $env:GITHUB_STEP_SUMMARY + echo $WatchTableHeaderRow2 >> $env:GITHUB_STEP_SUMMARY # Loop through every reporting object in the array where the processing result is "Watch". Add a row for each branch # containing the branch data for writers to review. This is the table that shows writers which branches might be deleted in the future. From cc18f0d786c17540fae1450aaa426a2268698486 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 25 Apr 2025 11:17:13 -0700 Subject: [PATCH 55/92] Add link to view branch in delete table --- .github/workflows/Shared-StaleBranch.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index c42b53378ab..63b03d17dab 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -540,7 +540,18 @@ jobs: $BC = $BranchReport.BranchCreator Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. On $FriendlyDeletionDate`: ($DD)." - echo "| $BN | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY + + If ($DeletionRun) { + + echo "| $BN | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY + + } Else { + + $BranchViewUrl = "$($GitHubData.event.repository.html_url)/branches/all?query=$BN" + + echo "| [$BN]($BranchViewUrl) | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY + + } } From 169bff13c2d88ee6aa4c04393a8ee95101613c7b Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 28 Apr 2025 11:39:31 -0700 Subject: [PATCH 56/92] Uncomment Invoke delete commands --- .github/workflows/Shared-StaleBranch.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index 63b03d17dab..bf9349036cf 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -355,7 +355,7 @@ jobs: # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. If (!$ReportOnly) { - #Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null } @@ -387,7 +387,7 @@ jobs: # If the workflow is in reporting mode, don't delete the branch. If it isn't, delete it. If (!$ReportOnly) { - #Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null + Invoke-RestMethod -Headers $GitHubHeaders -Uri $BranchDeleteUrl -Method DELETE -ResponseHeadersVariable ResponseHeaders | Out-Null } From cdb306b07487f5a80fe7653e4f50d44a7f1aa152 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 29 Apr 2025 13:41:26 -0700 Subject: [PATCH 57/92] Retrieve PR info for watch branches if available --- .github/workflows/Shared-StaleBranch.yml | 134 ++++++++++++++++++++++- 1 file changed, 130 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Shared-StaleBranch.yml b/.github/workflows/Shared-StaleBranch.yml index bf9349036cf..3cb768da5d2 100644 --- a/.github/workflows/Shared-StaleBranch.yml +++ b/.github/workflows/Shared-StaleBranch.yml @@ -7,6 +7,7 @@ name: (Scheduled) Stale branch removal permissions: contents: write + pull-requests: read on: workflow_call: @@ -105,7 +106,129 @@ jobs: Get-Date -Year $TargetYear -Month $TargetMonth -Day $TargetDay -Hour 0 -Minute 0 -Second 0 } + Function Get-PrData { + [CmdletBinding()] + Param( + [Parameter(Mandatory)] + $Branch + ) + + $PrUrl = $($GitHubData.event.repository.pulls_url).Replace("{/number}", "?state=all&head=$($GitHubData.event.repository.owner.login):$Branch&base=$($GitHubData.event.repository.default_branch)") + + Try { + + Write-Host "PR URL: $PrUrl" + + $PrData = Invoke-RestMethod -Uri $PrUrl -Method Get -Headers $GitHubHeaders -ContentType "application/json" -ErrorAction Stop + + If ($PrData.count -gt 0) { + + If ($PrData[0].state -eq "open") { + + Write-Host "The branch $Branch has an open pull request. PR number: $($PrData[0].number)" + + + $ReturnData = $PrData[0] + + } Else { + + Write-Host "The branch $Branch has $($PrData.count) closed pull requests. Last PR number: $($PrData[0].number)" + + $ReturnData = $PrData[0] + + } + + } Else { + + Write-Host "No pull requests found for the branch $Branch." + + $ReturnData = $Null + } + + } Catch { + + Write-Host "ERROR: An error occurred while fetching pull request data: $_" + + $ReturnData = $Null + + } + + Return $ReturnData + + } + + Function Get-PrMarkdown { + + [CmdletBinding()] + Param( + $PrData + ) + + If ($PrData) { + + $PrNumber = $PrData.number + $PrHtmlUrl = $PrData.html_url + + If ($PrData.state -eq "open") { + + $PrIconText = "Open" + $PrIconUrl = "https://learn.microsoft.com/en-us/Office/media/internal/git-pull-request-16.svg" + + } Else { + + If ($PrData.merged_at -eq $Null) { + + $PrIconText = "Closed" + $PrIconUrl = "https://learn.microsoft.com/en-us/Office/media/internal/git-pull-request-closed-16.svg" + + } Else { + + $PrIconText = "Merged" + $PrIconUrl = "https://learn.microsoft.com/en-us/Office/media/internal/git-merge-16.svg" + + } + + } + + $PrMarkdown = "![$PrIconText]($PrIconUrl) [$PrNumber]($PrHtmlUrl)" + + } Else { + + $PrMarkdown = "-" + + } + + Return $PrMarkdown + + } + + Function Get-PrStaleStatus { + + [CmdletBinding()] + Param( + $PrData + ) + + $PrState = " " + $Labels = $PrData.labels + + If ($Labels.name -contains "Auto Closed") { + + $PrState = "Auto Closed" + + } Else { + + If ($Labels.name -contains "Inactive") { + + $PrState = "Inactive" + } + + } + + Return $PrState + + } # Create the branch skip list that is a combination of the central workflow list and the branch skip list that can be populated in each individual repo. $SkipBranchList = $DefaultSkipBranchList + $RepoBranchSkipList | Select-Object -Unique @@ -456,8 +579,8 @@ jobs: $DeleteTableHeaderRow1 = "| Branch name | Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) | Processing result |" $DeleteTableHeaderRow2 = "|-------------|:----------------:|:-----------------:|:----------------------------------------------------:|-------------------|" - $WatchTableHeaderRow1 = "| Branch name | Branch creator | Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) | Processing result |" - $WatchTableHeaderRow2 = "|-------------|----------------|:----------------:|:-----------------:|:----------------------------------------------------:|-------------------|" + $WatchTableHeaderRow1 = "| Branch name | Branch creator | Pull request |Commits ahead by | Commits behind by | Days since last commit
(on $FriendlyDeletionDate) |" + $WatchTableHeaderRow2 = "|-------------|----------------|:------------:|:---------------:|:-----------------:|:----------------------------------------------------:|" # Set job summary and create the "Deleted stale branches" section. echo "# Stale branch results" >> $env:GITHUB_STEP_SUMMARY @@ -586,13 +709,16 @@ jobs: $BB = $BranchReport.BehindBy $LC = $BranchReport.DaysSinceLastCommit $DD = $BranchReport.DaysSinceLastCommitOnDeleteDay - $PR = $BranchReport.ProcessingResult $BC = $BranchReport.BranchCreator + $PrData = Get-PrData -Branch $BN + $PrMarkdown = Get-PrMarkdown -PrData $PrData + $PrStaleState = Get-PrStaleStatus -PrData $PrData + $BranchDiffHtmlUrl = "$($GitHubData.event.repository.html_url)/compare/$DefaultBranch...$($BN)#files_bucket" Write-Host "$PR`: Branch name: $BN. Branch creator: $BC. Ahead by: $AB. Behind by: $BB. Days since last commit: $LC. On $FriendlyDeletionDate`: ($DD)." - echo "| [$BN]($BranchDiffHtmlUrl) | [$BC]($GitHubBaseUrl/$BC) | $AB | $BB | $LC
($DD) | $PR |" >> $env:GITHUB_STEP_SUMMARY + echo "| [$BN]($BranchDiffHtmlUrl) | [$BC]($GitHubBaseUrl/$BC) | $PrMarkdown
$PrStaleState | $AB | $BB | $LC
($DD) |" >> $env:GITHUB_STEP_SUMMARY } From 23f9ecf1ce89d1aba3f3fd3b51216b89c97114f0 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 27 May 2025 10:58:27 -0700 Subject: [PATCH 58/92] Create Shared-AutoPublishV2.yml --- .github/workflows/Shared-AutoPublishV2.yml | 918 +++++++++++++++++++++ 1 file changed, 918 insertions(+) create mode 100644 .github/workflows/Shared-AutoPublishV2.yml diff --git a/.github/workflows/Shared-AutoPublishV2.yml b/.github/workflows/Shared-AutoPublishV2.yml new file mode 100644 index 00000000000..53a344117e3 --- /dev/null +++ b/.github/workflows/Shared-AutoPublishV2.yml @@ -0,0 +1,918 @@ +name: (Scheduled) Publish to live V2 + +permissions: + contents: write + pull-requests: write + checks: read + +on: + workflow_call: + inputs: + PayloadJson: + required: true + type: string + EnableAutoPublish: + required: true + type: boolean + EnableAutoMerge: # Calling repo enabled or disabled - true | false. EnableAutoPublish on the repo also needs to be enabled. GlobalEnableAutoMerge also needs be enabled. + required: false + type: boolean + secrets: + AccessToken: + required: true + PrivateKey: + required: true + ClientId: + required: true + +jobs: + + auto-publish: + name: Publish to live V2 + if: github.repository_owner == 'MicrosoftDocs' && contains(github.event.repository.topics, 'build') + 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: Process branches + shell: pwsh + env: + GlobalEnableAutoMerge: true # Enables or disable merges for all repos - true | false. EnableAutoPublish on the repo also needs to be enabled. + MaxAllowedChangedFiles: 30 # The maximum allowed number of files that can be auto-merged without human review. + + AccessToken: ${{ secrets.AccessToken }} + AppGitHubAccessToken: ${{ steps.app-token.outputs.token }} + PayloadJson: ${{ inputs.PayloadJson }} + EnableAutoPublish: ${{ inputs.EnableAutoPublish }} + EnableAutoMerge: ${{ inputs.EnableAutoMerge }} + IsPubOpsRepo: ${{ contains(github.event.repository.topics, 'pubops') }} + + run: | + + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $EnableAutoPublish = [System.Convert]::ToBoolean($env:EnableAutoPublish) + $EnableAutoMerge = [System.Convert]::ToBoolean($env:EnableAutoMerge) + $GlobalEnableAutoMerge = [System.Convert]::ToBoolean($env:GlobalEnableAutoMerge) + $MaxAllowedChangedFiles = $env:MaxAllowedChangedFiles + $AccessToken = $env:AccessToken + $AppGitHubAccessToken = $env:AppGitHubAccessToken + $IsPubOpsRepo = [System.Convert]::ToBoolean($env:IsPubOpsRepo) + + # Date/Time calculations + $UtcNow = Get-Date -AsUTC + $PacificTz = [System.TimeZoneInfo]::FindSystemTimeZoneById("Pacific Standard Time") + $IndiaTz = [System.TimeZoneInfo]::FindSystemTimeZoneById("India Standard Time") + # Convert UTC to local times + $PacificNow = [System.TimeZoneInfo]::ConvertTimeFromUtc($UtcNow, $PacificTz) + $IndiaNow = [System.TimeZoneInfo]::ConvertTimeFromUtc($UtcNow, $IndiaTz) + # Format as MM/DD HH:mm (24 hour clock) + $PacificStamp = $PacificNow.ToString('MM/dd HH:mm') + $IndiaStamp = $IndiaNow.ToString('MM/dd HH:mm') + + # Create github HTTP authentication header + $StandardGitHubHeaders = @{} + $StandardGitHubHeaders.Add("Authorization","token $($AccessToken)") + $StandardGitHubHeaders.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") + + # Repo variables + $TargetBranch = "live" + $Repository = $GitHubdata.event.repository.name + $Organization = $GitHubData.event.organization.login + $DefaultBranch = $GitHubData.event.repository.default_branch + $RepoLabelUrl = $GitHubData.event.repository.labels_url + + # PR variables + $PrTitle = "[AutoPublish] $DefaultBranch to $TargetBranch" + $AutoMergeDisabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore merging this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, email marveldocs-admins." + $AutoMergeDisabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore you merge this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **A contributor with write access to this repo will need to merge this PR for changes in it to go live.`n`nIf you have questions, email marveldocs-admins." + $AutoMergeEnabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, email marveldocs-admins." + $AutoMergeEnabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **If a manual merge is required, a contributor with write access to this repo will need to merge this PR for changes in it to go live.**`n`nIf you have questions, email marveldocs-admins." + + # Label variables + $AutoPublishLabelColor = "5319E7" + $AutoPublishLabelDescription = "PR was automatically created for publishing from $DefaultBranch to $TargetBranch." + $AutoPublishLabel = "AutoPublish" + $AutoMergeLabelColor = "5319E7" + $AutoMergeLabelDescription = "PR was automatically merged by AutoPublish process." + $AutoMergeLabel = "AutoMerge" + $SignOffLabelColor = "46ce1c" + $SignOffLabelDescription = "The pull request is ready to be reviewed and merged by PubOps." + $SignOffLabel = "Sign off" + $ManualMergeLabelColor = "f79d43" + $ManualMergeLabelDescription = "An AutoPublish pull request requires a human to merge it." + $ManualMergeLabel = "Manual merge required" + + $PrLabelList = @($AutoPublishLabel, $SignOffLabel, $ManualMergeLabel, $AutoMergeLabel) + + ##################### + ##################### + # Test-RepoLabel + + Function Test-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $RepoUri + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $LabelUri = $RepoUri.Replace("{/name}","/$Name") + + # Check to see if the label we want exists in the repo + Try { + + Write-Host "Checking to see if label $Name exists in repo URL $LabelUri." + + $LabelResults = Invoke-WebRequest -UseBasicParsing -Uri $LabelUri -Headers $StandardGitHubHeaders -ErrorAction Stop + $LabelFound = $True + + } Catch { + + # OK if label doesn't exist. Just means we need to create it. + $LabelFound = $False + + } + + # Return boolean to calling statement + $LabelFound + + } + + ##################### + ##################### + # New-RepoLabel + + Function New-RepoLabel { + + [CmdletBinding()] + param( + + $Name, + $Color, + $Description, + $RepoUri + ) + + # Remove placeholder text from repo URL + $RepoUri = $RepoUri.Replace("{/name}","") + $Result = $Null + + # Construct the JSON statement that will be sent to GitHub as the body of the web request. Include the name of the label, its color, and description. + # Convert hash table to JSON + $Body = @{} + $Body.Add("name", $Name) + $Body.Add("color", $Color) + $Body.Add("description", $description) + $Body = $Body | ConvertTo-Json + + # Try to submit the request to GitHub API to create the label + Try { + + Write-Host "Creating label $Name with color $Color on repo $RepoUri." + + $Result = Invoke-RestMethod -Uri $RepoUri -Headers $StandardGitHubHeaders -Body $Body -Method POST + + } Catch { + + Write-Host "ERROR: Failed to create new label $Name on repo $RepoUri. Error: $($Error[0].Exception.Message)." + + } + + } + + ##################### + ##################### + # Test-PrLabel + + Function Test-Prlabel { + + [CmdletBinding()] + param( + + $LabelArray, + $IssueUrl + ) + + # Replace placeholder text in the URL retrieved from the GitHub API with the name of the label we're looking for + $IssueLabelUrl = "$IssueUrl/labels" + $LabelHashTable = @{} + $LabelResults = $Null + + # Get list of labels on issue/PR + Try { + + Write-Host "Getting labels on issue $IssueLabelUrl." + + $LabelResults = Invoke-RestMethod -Uri $IssueLabelUrl -Headers $StandardGitHubHeaders -ErrorAction Stop + + } Catch { + + Write-Host "ERROR: Failed to get list of labels on $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + } + + ForEach ($Label in $LabelArray) { + + If ($LabelResults -ne $Null) { + + If ($LabelResults.name.Contains($Label)) { + + $LabelHashTable.Add($Label, $True) + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } Else { + + $LabelHashTable.Add($Label, $False) + + } + + } + + # Return array of labels on Issue/PR + Return $LabelHashTable + + } + + ##################### + ##################### + # Set-PrLabel + + Function Set-PrLabel { + + param( + + $IssueUrl, + $LabelName + + ) + + # Construct label URL based on issue or pull request URL + $IssueLabelUrl = "$IssueUrl/labels" + + # Construct JSON statement that will be sent to GitHub as the body of the web request. Includes only the label name. GitHub expects an array even thought it's a single value + # Convert array to JSON + $Body = @() + $Body += $LabelName + $Body = ConvertTo-Json -InputObject $Body + + # Try to submit the request to GitHub API to apply they label to the issue or pull request + Try { + + Write-Host "Setting label $LabelName on URL $IssueLabelUrl." + + $Result = Invoke-RestMethod -Uri $IssueLabelUrl -Body $Body -Headers $StandardGitHubHeaders -Method POST + + + } Catch { + + Write-Host "ERROR: Failed to set label on URL $IssueLabelUrl. Error: $($Error[0].Exception.Message)." + + } + + } + + #################### + #################### + # Remove-PrLabel + + Function Remove-PrLabel { + + param( + + $IssueUrl, + $LabelName + + ) + + Write-Host "Remove `"$LabelName`" label from PR." + + Try { + + $LabelUrl = "$IssueUrl/labels/$LabelName" + + $Result = Invoke-RestMethod -Uri $LabelUrl -Headers $StandardGitHubHeaders -Method Delete -ErrorAction Stop + + Write-Host "Successfully removed `"$LabelName`" label." + + } Catch { + + Write-Host "ERROR: Failed to remove `"$LabelName`" label. Url: $LabelUrl. Error: $_" + + } + + } + + ##################### + ##################### + # Get-TzAbbrev + + # Derive a 2- or 3-letter abbreviation from the zone's name + Function Get-TzAbbrev { + param( + [System.TimeZoneInfo]$Tz, + [DateTime]$LocalTime + ) + # Choose the right long name (standard vs. daylight) + $FullName = If ($Tz.IsDaylightSavingTime($LocalTime)) { + $Tz.DaylightName # e.g. “Pacific Daylight Time” + } Else { + $Tz.StandardName # e.g. “Pacific Standard Time” + } + + # Collapse it to initials: “Pacific Daylight Time” to “PDT” + -Join (($fullName -split '\s+') | ForEach-Object { $_[0] }) + } + + ##################### + ##################### + # Test-TargetBranchExists + + Function Test-TargetBranchExists { + + $BranchResponse = $Null + $BranchExists = $Null + + Write-Host "Checking if $TargetBranch exists" + + Try { + + $BranchResponse = Invoke-RestMethod -uri "https://api.github.com/repos/$Organization/$Repository/git/ref/heads/$TargetBranch" -Headers $StandardGitHubHeaders -Method Get -ErrorAction Stop + $BranchExists = $True + + } Catch { + + Write-Host "Branch doesn't exist or an error occurred. Error: $_." + + $BranchExists = $False + + } + + Return $BranchExists + + } + + ##################### + ##################### + # Get-BranchDiff + + Function Get-BranchDiff { + + $CompareResponse = $Null + + Write-Host "Checking if $DefaultBranch is ahead of $TargetBranch" + + Try { + + $CompareResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/compare/$TargetBranch...$DefaultBranch" -Method GET -Headers $StandardGitHubHeaders -ErrorAction Stop + + } Catch { + + $CompareResponse = $Null + + } + + Return $CompareResponse + + } + + ##################### + ##################### + # Get-PrState + + Function Get-PrState { + + param( + + [Parameter(Mandatory = $True)] + [string]$NodeId + + ) + + $Query = @' + query ($Id: ID!) { + node(id: $Id) { + ... on PullRequest { + id + mergeable + commits(last: 1) { + nodes { + commit { + statusCheckRollup { state } + } + } + } + } + } + } + '@ + + $Body = @{ query = $query; variables = @{ Id = $NodeId } } | ConvertTo-Json -Depth 3 + + + $Result = Invoke-RestMethod -Uri https://api.github.com/graphql -Method POST -Headers $StandardGitHubHeaders -Body $Body + + Return $Result + + } + + ##################### + ##################### + # Get-PublishPullRequest + + Function Get-PublishPullRequest { + + $PrResponse = $Null + + Write-Host "Checking if a pull request exists between $DefaultBranch and $TargetBranch" + + Try { + + $PrResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/pulls?head=$($Organization):$DefaultBranch&base=$TargetBranch&state=open" -Method GET -Headers $StandardGitHubHeaders -ErrorAction Stop + + } Catch { + + Write-Host "Error: $_" + $PrResponse = $Null + + } + + Return $PrResponse + + } + + ##################### + ##################### + # Set-Labels + + Function Set-Labels { + + param( + + [Parameter(Mandatory = $True)] + [string]$IssueUrl, + [Parameter(Mandatory = $True)] + [boolean]$ManualMerge + + ) + + # Check if labels exist on the repo. Test for $SignOffLabel is lower in "IsPubOpsRepo" statement. + $AutoLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel + $AutoMergeLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoMergeLabel + $ManualMergeLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $ManualMergeLabel + + If (!$AutoLabelExists) { + + # Create AutoPublishLabel label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoPublishLabel -Color $AutoPublishLabelColor -Description $AutoPublishLabelDescription + + } + + If (!$AutoMergeLabelExists) { + + # Create AutoMergeLabel label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $AutoMergeLabel -Color $AutoMergeLabelColor -Description $AutoMergeLabelDescription + + } + + If (!$ManualMergeLabelExists) { + + # Create ManualMergeLabel label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $ManualMergeLabel -Color $ManualMergeLabelColor -Description $ManualMergeLabelDescription + + } + + + $LabelResultsArray = Test-Prlabel -LabelArray $PrLabelList -IssueUrl $IssueUrl + + # Only add the AutoPublishLabel label if it doesn't already exist on the PR + If (!$LabelResultsArray.$AutoPublishLabel) { + + Write-Host "Label $AutoPublishLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $AutoPublishLabel + + } + + If ($IsPubOpsRepo) { + + Write-Host "PubOps repo. Checking and setting $SignOffLabel label." + + $SignOffLabelExists = Test-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel + + If (!$SignOffLabelExists) { + + # Create label on the repo if it doesn't exist. + New-RepoLabel -RepoUri $RepoLabelUrl -Name $SignOffLabel -Color $SignOffLabelColor -Description $SignOffLabelDescription + + } + + # Only add the SignOffLabel label if it doesn't already exist on the PR + If (!$LabelResultsArray.$SignOffLabel) { + + Write-Host "Label $SignOffLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $SignOffLabel + + } + + } + + # Add either ManualMergeLabel or AutoMergeLabel depending on $ManualMerge true or false. + If ($ManualMerge) { + + Write-Host "Checking and setting $ManualMergeLabel label. GlobalEnableAutoMerge: $GlobalEnableAutoMerge. EnableAutoMerge: $EnableAutoMerge. ManualMerge: $ManualMerge." + + # Only add the ManualMergeLabel label if it doesn't already exist on the PR + If (!$LabelResultsArray.$ManualMergeLabel) { + + Write-Host "Label $ManualMergeLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $ManualMergeLabel + + } + + } Else { + + Write-Host "Checking and setting $AutoMergeLabel label. GlobalEnableAutoMerge: $GlobalEnableAutoMerge. EnableAutoMerge: $EnableAutoMerge. ManualMerge: $ManualMerge." + + # Only add the AutoMergeLabel label if it doesn't already exist on the PR + If (!$LabelResultsArray.$AutoMergeLabel) { + + Write-Host "Label $AutoMergeLabel doesn't exist on $IssueUrl. Adding label." + + # Add the label to the PR + Set-PrLabel -IssueUrl $IssueUrl -LabelName $AutoMergeLabel + + } + + } + + } + + ##################### + ##################### + # Enable-GitHubPRAutoMerge + + Function Enable-GitHubPRAutoMerge { + param( + [Parameter(Mandatory)] + [int] $PullNumber, + [int] $MaxRetries = 3 + + ) + + # Get the GraphQL node_id for the pull request + $Pr = Invoke-RestMethod "https://api.github.com/repos/$Organization/$Repository/pulls/$PullNumber" -Headers $AppGitHubAccessHeaders + $Node = $Pr.node_id + + # GraphQL to submit automerge request to GitHub + $Mutation = @" + mutation (`$id`: ID!, `$method`: PullRequestMergeMethod!) { + enablePullRequestAutoMerge( + input: {pullRequestId`: `$id, mergeMethod`: `$method} + ) { pullRequest { autoMergeRequest { enabledAt mergeMethod } } } + } + "@ + + # Create a custom object that'll be returned to the calling function + $Result = [PSCustomObject]@{ + PullNumber = $PullNumber + QueryResponse = $null + MutationResponse = $null + TimedOut = $false + ManualMergeRequired = $null + } + + $MutationResponse = $null + + # Exponential back-off: 1,2,4,8,16,32,etc seconds per $MaxRetries + For ($i = 0; $i -lt $MaxRetries -and $MutationResponse -eq $Null; $i++) { + + # Check mergeable state + $QueryResponse = Get-PrState -NodeId $Node + $State = $QueryResponse.data.node.mergeable + + # Add QueryResponse to result object to return to calling statement + $Result.QueryResponse = $QueryResponse + + Write-Host "PR #$PullNumber state: $State" + + Switch ($State) { + 'MERGEABLE' { + + $MutationBody = @{ query = $mutation; variables = @{ id = $Node; method = "MERGE" } } | ConvertTo-Json -Depth 3 + + Write-Host "Attempting to enable automerge for #$PullNumber" + + $MutationResponse = Invoke-RestMethod "https://api.github.com/graphql" -Method Post -Headers $AppGitHubAccessHeaders -Body $MutationBody + + # Add MutationResponse to result object to return to calling statement + $Result.MutationResponse = $MutationResponse + + } + 'CONFLICTING' { + + Write-Host "PR #$PullNumber has conflicts. Auto-merge not enabled." + + } + default { + + # Calculate exponential delay based on current attempt + $Delay = [math]::Pow(2, $i) + + Write-Host "mergeable is UNKNOWN. This is expected on the first attempt while GitHub evaluates PR. Retry in $Delay s..." + + Start-Sleep -Seconds $Delay + } + } + + Write-Host "Query response attempt $i`:`n $($QueryResponse | ConvertTo-Json -Depth 50)" + Write-Host "Mutation response attempt $i`:`n $($MutationResponse | ConvertTo-Json -Depth 50)" + + } + + If ($i -eq $MaxRetries) { + + $TimedOut = $Result.TimedOut = $true + + Write-Host "Timed out waiting for PR #$PullNumber to become mergeable." + + } + + # Check if auto-merge was enabled. If enabledAt is not null, it was enabled. + $AutoMergeEnabledAt = $MutationResponse.data.enablePullRequestAutoMerge.pullRequest.autoMergeRequest.enabledAt + + If ($AutoMergeEnabledAt) { + + Write-Host "Auto-merge for PR #$PrNumber was enabled at $AutoMergeEnabledAt." + + $Result.ManualMergeRequired = $False + + } Else { + + Write-Host "Auto-merge for PR #$PrNumber was not enabled. Manual merge required." + + $Result.ManualMergeRequired = $True + + If ($TimedOut) { + + Write-Host "PR #$PrNumber timed out waiting for mergeable state." + + } Else { + + If ($QueryResponse.data.node.mergeable -eq "CONFLICTING") { + + Write-Host "PR #$PrNumber has conflicts. Auto-merge not enabled." + + } Else { + + Write-Host "PR #$PrNumber wasn't mergeable for an unknown reason. Below is the response:" + Write-Host $($AutoMergeResult | ConvertTo-Json -Depth 20) + + } # QueryResponse.data.node.mergeable + + } # TimedOut + + } # If AutoMergeEnabledAt + + Return $Result + } + + ##################### + ##################### + # New-PullRequest + + Function New-PullRequest { + + $PrResponse = $Null + + Write-Host "Creating a new PR from $DefaultBranch to $TargetBranch." + + $PrTitle = "$PrTitle - $PacificStamp $(Get-TzAbbrev $PacificTz $PacificNow) | $IndiaStamp $(Get-TzAbbrev $IndiaTz $IndiaNow)" + + If ($IsPubOpsRepo) { + + If ($GlobalEnableAutoMerge -and $EnableAutoMerge) { + + $PrDescription = $AutoMergeEnabledPrDescriptionPubOps + + } Else { + + $PrDescription = $AutoMergeDisabledPrDescriptionPubOps + + } + + } Else { + + If ($GlobalEnableAutoMerge -and $EnableAutoMerge) { + + $PrDescription = $AutoMergeEnabledPrDescriptionNonPubOps + + } Else { + + $PrDescription = $AutoMergeDisabledPrDescriptionNonPubOps + + } + + } + + $PrBody = @{ + title = $PrTitle + head = $DefaultBranch + base = "$TargetBranch" + body = $PrDescription + } | ConvertTo-Json + + Try { + + $PrResponse = Invoke-RestMethod -Uri "https://api.github.com/repos/$Organization/$Repository/pulls" -Method POST -Headers $AppGitHubAccessHeaders -Body $PrBody -ErrorAction Stop + + Write-Host "Created pull request $($PrResponse.html_url)" + + + + } Catch { + + $PrResponse = $Null + + Write-Host "ERROR occurred during PR creation. Error: $_" + + } + + Return $PrResponse + + } + + + ##################### + ##################### + # Main + + If ($EnableAutoPublish) { + + If (Test-TargetBranchExists) { + + $PrData = Get-PublishPullRequest + + # Check to see if $PrData contains data. If yes, there's a PR. If not, no PR. + If ($PrData) { + + $Submitter = $PrData.user.login + $PrNumber = $PrData.number + $Node = $PrData.node_id + $IssueUrl = $PrData.issue_url + + If ($Submitter -eq "m365-skilling-repo-management[bot]") { + + Write-Host "PR #$PrNumber was opened by m365-skilling-repo-management[bot]." + + $LabelResultsArray = Test-Prlabel -LabelArray @($AutoPublishLabel, $ManualMergeLabel, $AutoMergeLabel) -IssueUrl $IssueUrl + + If ($LabelResultsArray.$AutoPublishLabel) { + + Write-Host "PR #$PrNumber created by AutoPublish." + + # Check PR status check state + $QueryResponse = Get-PrState -NodeId $Node + $CheckState = $QueryResponse.data.node.commits.nodes.commit.statusCheckRollup.state + + write-host $($QueryResponse | ConvertTo-json -depth 50) + Write-Host "Check state on PR #$PrNumber`: $CheckState." + + If ($CheckState -eq "FAILURE") { + + Write-Host "PR #$PrNumber status checks failed." + + If ($LabelResultsArray.$ManualMergeLabel) { + + Write-Host "$ManualMergeLabel already exists on PR #$PrNumber." + + } Else { + + If ($LabelResultsArray.$AutoMergeLabel) { + + Remove-PrLabel -IssueUrl $IssueUrl -LabelName $AutoMergeLabel + + } + + Write-Host "Adding $ManualMergeLabel to PR #$PrNumber." + + Set-PrLabel -IssueUrl $IssueUrl -LabelName $ManualMergeLabel + + } + + } + + } Else { + + Write-Host "PR #$PrNumber not created by AutoPublish. Exiting." + + } + + } Else { + + Write-Host "PR #$PrNumber was not opened by m365-skilling-repo-management[bot]. Exiting." + + } + + } Else { + + Write-Host "No pull request found. Checking diff between $DefaultBranch and $TargetBranch." + + # Check to see if $DefaultBranch contains anything that isn't in $TargetBranch. + $BranchDiff = Get-BranchDiff + + $AheadBy = $BranchDiff.ahead_by + $ManualMergeRequired = $False + + write-host "$DefaultBranch is ahead of $TargetBranch by $AheadBy commits." + + # If there are changes in $DefaultBranch that aren't in $TargetBranch, create a PR. + If ($AheadBy -gt 0) { + + Write-Host "$DefaultBranch has changes ahead of $TargetBranch" + Write-Host "PubOps repo: $IsPubOpsRepo" + + $NewPrResponse = New-PullRequest + + If ($NewPrResponse) { + + $PrNumber = $NewPrResponse.number + $ChangedFiles = $NewPrResponse.changed_files + + Write-Host "PR #$PrNumber created with $ChangedFiles changed files." + + If ($GlobalEnableAutoMerge -and $EnableAutoMerge) { + + If ($ChangedFiles -le $MaxAllowedChangedFiles) { + + $AutoMergeResult = Enable-GitHubPRAutoMerge -PullNumber $PrNumber -MaxRetries 6 + $ManualMergeRequired = $AutoMergeResult.ManualMergeRequired + + } Else { + + Write-Host "PR #$PrNumber has $ChangedFiles changed files but the maximum number of files allowed to auto merge is $MaxAllowedChangedFiles. Manual merge required." + + $ManualMergeRequired = $True + + } # If MaxAllowedChangedFiles + + } Else { + + Write-Host "Auto merge disabled. GlobalEnableAutoMerge: $GlobalEnableAutoMerge. EnableAutoMerge: $EnableAutoMerge." + + $ManualMergeRequired = $True + + } # If GlobalEnableAutoMerge/EnableAutoMerge + + $IssueUrl = $NewPrResponse.issue_url + + If ($IssueUrl) { + + Set-Labels -IssueUrl $IssueUrl -ManualMerge $ManualMergeRequired + + } + + } # If NewPrResponse + + } Else { + + Write-Host "$DefaultBranch has no changes ahead of $TargetBranch. Not creating PR." + + } + + } + + } Else { + + Write-Host "The branch $TargetBranch doesn't exist or an error occurred." + + } + + } Else { + + Write-Host "Auto publishing is disabled. Exiting." + + } From ba78b32c68ce1eb4b542c438468403baa8014d24 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 29 May 2025 21:42:55 -0700 Subject: [PATCH 59/92] Enable close issues --- .github/workflows/Shared-Stale.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-Stale.yml b/.github/workflows/Shared-Stale.yml index f0905a045b7..16fdb0a4043 100644 --- a/.github/workflows/Shared-Stale.yml +++ b/.github/workflows/Shared-Stale.yml @@ -60,14 +60,22 @@ jobs: repo-token: ${{ secrets.AccessToken }} debug-only: ${{ inputs.RunDebug }} operations-per-run: 1000 - days-before-issue-stale: -1 - days-before-issue-close: -1 + days-before-issue-stale: 90 + days-before-issue-close: 14 days-before-pr-stale: 90 days-before-pr-close: 14 # start-date: ${{ steps.get-month.outputs.SelectedMonth }} + stale-issue-label: Inactive + close-issue-label: Auto Closed stale-pr-label: Inactive close-pr-label: Auto Closed exempt-pr-labels: Keep open + stale-issue-message: > +

Inactive issue marked for closure

+

+ This issue has been inactive for over 90 days, and an Inactive label has been added to it. If this issue remains inactive with no new comments, it will be closed automatically in 14 days. + close-issue-message: > + This issue has been inactive for a further 14 days and is now being closed. stale-pr-message: >

Inactive PR marked for closure

From 49351cc238501788ad99d32524cf214b4e3f348f Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:01:07 -0700 Subject: [PATCH 60/92] Create AutoLabelMsftContributor-PrCloseMessage.md --- .../AutoLabelMsftContributor-PrCloseMessage.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md diff --git a/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md b/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md new file mode 100644 index 00000000000..3d4af71bc40 --- /dev/null +++ b/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md @@ -0,0 +1,14 @@ +Thank you for your contribution! To reduce the chances of pre-release content being released publicly, and to simplify the contribution workflow, Microsoft FTEs and vendors must submit contributions to **private repositories**. Private repositories typically have the same name as public repositories with the **-pr** suffix added. + +For example, if the public repository name is `contoso-help`, the private repository name is usually `contoso-help-pr`. + +Please re-submit your contribution to the private repository. If you can’t find the private repository to use, do one of the following: + +- **(Preferred)** Go to and paste the public Learn URL of the page you want to update into the field provided. +- Retrieve the article source URL from page source: + 1. Right-click on the article page and select **View source**. + 2. Find *original_content_git_url* and paste that URL into the address bar of a new tab. + 3. Click **Edit** and follow the steps to update the article. +- Install the **Microsoft Learn maintenance tool** and, while on the article you want to update, click the extension in the toolbar, then choose **MD** or **YML**. + +If you have questions or need help, please post a message to [G+E LMC Support (aka AskAnAdmin)](https://aka.ms/askanadmin) (Microsoft FTE or vendor login required). From 7496d094627f201737873d0833e357b813c36eec Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Sat, 7 Jun 2025 23:56:06 -0700 Subject: [PATCH 61/92] add additional info to message --- .../resources/AutoLabelMsftContributor-PrCloseMessage.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md b/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md index 3d4af71bc40..94e5277025a 100644 --- a/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md +++ b/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md @@ -4,11 +4,12 @@ For example, if the public repository name is `contoso-help`, the private reposi Please re-submit your contribution to the private repository. If you can’t find the private repository to use, do one of the following: -- **(Preferred)** Go to and paste the public Learn URL of the page you want to update into the field provided. +- **(Preferred)** Go to and paste the public **Learn** (not GitHub) URL of the page you want to update into the field provided. If you haven't used the Learn Editor before, check out the [Learn Editor documentation](https://learn.microsoft.com/en-us/help/platform/learn-editor-overview). If you see a 404, be sure to sign into Learn by clicking **Sign in** in the top-right corner of the page. +- Install the **Microsoft Learn maintenance tool** and, while on the article you want to update, click the extension in the toolbar, then choose **MD** or **YML**. - Retrieve the article source URL from page source: 1. Right-click on the article page and select **View source**. - 2. Find *original_content_git_url* and paste that URL into the address bar of a new tab. - 3. Click **Edit** and follow the steps to update the article. -- Install the **Microsoft Learn maintenance tool** and, while on the article you want to update, click the extension in the toolbar, then choose **MD** or **YML**. + 2. Find *original_content_git_url* and paste that URL into the address bar of a new tab. + 3. Switch the branch to **main**. + 4. Click **Edit** and follow the steps to update the article. If you have questions or need help, please post a message to [G+E LMC Support (aka AskAnAdmin)](https://aka.ms/askanadmin) (Microsoft FTE or vendor login required). From aeea2d29f9d24f443469be44c2b07d46bf95d258 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:45:07 -0700 Subject: [PATCH 62/92] add 'browser extension' clarification --- .../resources/AutoLabelMsftContributor-PrCloseMessage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md b/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md index 94e5277025a..e746324fd70 100644 --- a/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md +++ b/.github/workflows/resources/AutoLabelMsftContributor-PrCloseMessage.md @@ -5,7 +5,7 @@ For example, if the public repository name is `contoso-help`, the private reposi Please re-submit your contribution to the private repository. If you can’t find the private repository to use, do one of the following: - **(Preferred)** Go to and paste the public **Learn** (not GitHub) URL of the page you want to update into the field provided. If you haven't used the Learn Editor before, check out the [Learn Editor documentation](https://learn.microsoft.com/en-us/help/platform/learn-editor-overview). If you see a 404, be sure to sign into Learn by clicking **Sign in** in the top-right corner of the page. -- Install the **Microsoft Learn maintenance tool** and, while on the article you want to update, click the extension in the toolbar, then choose **MD** or **YML**. +- Install the **Microsoft Learn maintenance tool** browser extension and, while on the article you want to update, click the extension in the toolbar, then choose **MD** or **YML**. - Retrieve the article source URL from page source: 1. Right-click on the article page and select **View source**. 2. Find *original_content_git_url* and paste that URL into the address bar of a new tab. From 5ee90b57bb7fc21b7c822b4a66150a0638e7c784 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 23 Jun 2025 15:45:53 -0700 Subject: [PATCH 63/92] Add PR close functionality --- .../Shared-AutoLabelMsftContributor.yml | 289 ++++++++++-------- 1 file changed, 154 insertions(+), 135 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelMsftContributor.yml b/.github/workflows/Shared-AutoLabelMsftContributor.yml index 147c9192f5c..119458b782d 100644 --- a/.github/workflows/Shared-AutoLabelMsftContributor.yml +++ b/.github/workflows/Shared-AutoLabelMsftContributor.yml @@ -1,4 +1,4 @@ -name: Label Microsoft contrib +name: Label Microsoft contrib and close PR permissions: pull-requests: write @@ -23,13 +23,25 @@ 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 }} + AppGitHubAccessToken: ${{ steps.app-token.outputs.token }} AccessToken: ${{ secrets.AccessToken }} ClientId: ${{ secrets.ClientId }} PrivateKey: ${{ secrets.PrivateKey }} + IsPublicOnlyRepo: ${{ contains(github.event.repository.topics, 'publiconly') }} + run: | @@ -38,144 +50,21 @@ jobs: # Get payload data from GitHub. $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $GitRequestEvent = $GitHubData.event_name + $GitHubAction = $GithubData.event.action $AccessToken = $env:AccessToken + $AppGitHubAccessToken = $env:AppGitHubAccessToken $ClientId = $env:ClientId $PrivateKey = $env:PrivateKey + $IsPublicOnlyRepo = [System.Convert]::ToBoolean($env:IsPublicOnlyRepo) $RepoName = $GitHubData.event.repository.name $RepoUrl = $GitHubData.event.repository.url - $RepoTopicUrl = "$RepoUrl/topics" # 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/" - ########################################### - ########################################### - # GitHub app/installation token block start - ########################################### - ########################################### - - Function ConvertTo-JwtBase64 { - param( - [Parameter(Mandatory = $True)] - [string]$RawJson - ) - - # Convert UTF-8 bytes to Base64, then make it URL-safe for JWT - $Encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($RawJson)) - Return $Encoded.TrimEnd('=') -replace '\+','-' -replace '/','_' - } - - Function New-GitHubAppJWT { - param( - [Parameter(Mandatory = $True)] - [string]$ClientId, - - [Parameter(Mandatory = $True)] - [string]$PrivateKey, - - [int]$ExpiresInMinutes = 10 - ) - - Write-Host "Create JWT" - - # Build the header - $Header = @{ - alg = "RS256" - typ = "JWT" - } | ConvertTo-Json -Compress - - $HeaderEncoded = ConvertTo-JwtBase64 -RawJson $Header - - # Build the payload - $Now = [System.DateTimeOffset]::UtcNow - $Payload = @{ - iat = $now.AddSeconds(-10).ToUnixTimeSeconds() - exp = $now.AddMinutes($ExpiresInMinutes).ToUnixTimeSeconds() - iss = $ClientId - } | ConvertTo-Json -Compress - - $PayloadEncoded = ConvertTo-JwtBase64 -RawJson $Payload - - # Combine and sign - $Rsa = [System.Security.Cryptography.RSA]::Create() - $Rsa.ImportFromPem($PrivateKey) - - $Combined = "$HeaderEncoded.$PayloadEncoded" - $SignatureBytes = $Rsa.SignData( - [System.Text.Encoding]::UTF8.GetBytes($Combined), - [System.Security.Cryptography.HashAlgorithmName]::SHA256, - [System.Security.Cryptography.RSASignaturePadding]::Pkcs1 - ) - - $SignatureEncoded = [Convert]::ToBase64String($SignatureBytes).TrimEnd('=') -replace '\+','-' -replace '/','_' - - Return "$HeaderEncoded.$PayloadEncoded.$SignatureEncoded" - } - - Function Get-GitHubAppInstallationToken { - param( - [Parameter(Mandatory = $true)] - [string]$ClientId, - - [Parameter(Mandatory = $true)] - [string]$PrivateKey, - - [Parameter(Mandatory = $true)] - [string]$Organization, - - [int]$TokenTTLMinutes = 10 - ) - - # Create the JWT - $Jwt = New-GitHubAppJWT -ClientId $ClientId -PrivateKey $PrivateKey -ExpiresInMinutes $TokenTTLMinutes - - # Prepare headers - $JwtHeaders = @{ - Authorization = "Bearer $Jwt" - Accept = "application/vnd.github+json" - "X-GitHub-Api-Version"= "2022-11-28" - } - - Write-Host "Request org installation ID" - - # 1) Retrieve the installation ID for the org - Try { - $InstallationInfo = Invoke-RestMethod ` - -Uri "https://api.github.com/orgs/$Organization/installation" ` - -Headers $JwtHeaders ` - -ErrorAction Stop - - $InstallationId = $InstallationInfo.id - } - Catch { - Write-Error "Failed to get installation ID from GitHub. $_" - Return $Null - } - - Write-Host "Get installation token" - - # 2) Use the installation ID to request an installation token - Try { - $TokenResponse = Invoke-RestMethod ` - -Uri "https://api.github.com/app/installations/$InstallationId/access_tokens" ` - -Headers $JwtHeaders ` - -Method Post ` - -ErrorAction Stop - - Return $TokenResponse.token - } - Catch { - Write-Error "Failed to get access token from GitHub. $_" - Return $Null - } - } - - ######################################### - ######################################### - # GitHub app/installation token block end - ######################################### - ######################################### + $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" + $WorkflowsRef = "workflows-prod" ##################### ##################### @@ -298,9 +187,118 @@ jobs: ##################### ##################### - # Main + # Close-Pr + Function Close-Pr { + + param( + + $PrUrl, + $Headers + + ) + + $Body = @{ + + state = "closed" + + } | ConvertTo-Json + + Try { + + Write-Host "Closing PR URL: $PrUrl" + + Invoke-RestMethod -Uri $PrUrl -Headers $Headers -Method PATCH -Body $Body -ErrorAction Stop + + $PrClosed = $True + + } Catch { + + Write-Host "ERROR: Failed to close PR URL: $PrUrl. Error: $_" + + $PrClosed = $False + + } + + Return $PrClosed + + } + + ##################### + ##################### + # Set-PrMessage + + Function Set-PrMessage { + + Param( + $Message, + $CommentsUrl, + $Headers + ) + + $BodyHash = @{} + $BodyHash.body = $Message + $BodyJson = $BodyHash | ConvertTo-Json + $BodyJson + + Try { + + Write-Host "Setting message on comment URL: $CommentsUrl" + + $Result = Invoke-RestMethod -Uri $CommentsUrl -Body $BodyJson -Headers $Headers -Method POST -ErrorAction Stop + + $PostCommentSuccess = $True + + } Catch { + + Write-Host "ERROR: Failed to post message to comments URL: $CommentsUrl. Error: $_" + + + $PostCommentSuccess = $False + + } + + Return $PostCommentSuccess + + } + + ##################### + ##################### + # Get-PrMessage + + Function Get-PrMessage { + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $PrMessageFileName, + [Parameter(Mandatory=$True)] + $Headers + ) + + $PrMessageFile = "$WorkflowsResourcePath/$PrMessageFileName`?ref=$WorkflowsRef" + + Try { + + Write-Host "Getting PR message from $PrMessageFile" + + $PrMessageData = Invoke-RestMethod -Uri $PrMessageFile -Headers $Headers + $PrMessage = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($PrMessageData.content)); + + } Catch { + + Write-Host "Failed to get PR message $PrMessageFileName. Error: $_" + + $PrMessage = $Null + + } + + Return $PrMessage + + } + ##################### + ##################### + # Main Write-Host "Repo: $($GitHubData.event.repository.name)" Write-Host "Sender: $($GitHubData.event.sender.login)" @@ -316,16 +314,14 @@ jobs: # Create team read GitHub HTTP authentication header. Need a token that has access to org scope. Get-GitHubAppInstallationToken # requests an installation token from our GitHub app so that we can authenticate. GITHUB_TOKEN that is used to populate $AccessToken # doesn't have access to org scope so using our GitHub app's access. - $GitHubAccessToken = Get-GitHubAppInstallationToken -ClientId $ClientId -PrivateKey $PrivateKey -Organization MicrosoftDocs -TokenTTLMinutes 10 - $TeamReadGitHubHeaders = @{} - $TeamReadGitHubHeaders.Add("Authorization","token $($GitHubAccessToken)") + $TeamReadGitHubHeaders.Add("Authorization","token $($AppGitHubAccessToken)") $TeamReadGitHubHeaders.Add("User-Agent", "OfficeDocs") $TeamReadGitHubHeaders.Add("Accept","application/vnd.github.mercy-preview+json") # -and ($GitHubData.event.action -eq "opened") # Only process event types of 'pull_request_target' that are 'opened' (PR created) - If (($GitRequestEvent -eq "pull_request_target") ) { + If (($GitRequestEvent -eq "pull_request_target") -and ($GitHubAction -eq "opened")) { $Contributor = $GitHubData.event.pull_request.user.login $LabelName = "Microsoft submitter" @@ -390,7 +386,6 @@ jobs: Write-Host "Setting label on PR." - $LabelUrl = $GitHubData.event.pull_request.issue_url Write-Host "Using pull request URL $LabelUrl." @@ -409,6 +404,30 @@ jobs: } + If ($IsPublicOnlyRepo) { + + Write-Host "Repo is a public-only repo. Not closing PR." + + } Else { + + $PrUrl = $GitHubData.event.pull_request.url + $CommentsUrl = $GitHubData.event.pull_request.comments_url + + $CloseSuccess = Close-Pr -PrUrl $PrUrl -Headers $GitHubHeaders + + If ($CloseSuccess) { + + $PrMessage = Get-PrMessage -PrMessageFileName "AutoLabelMsftContributor-PrCloseMessage.md" -Headers $GitHubHeaders + + If ($PrMessage) { + + Set-PrMessage -Message $PrMessage -Headers $GitHubHeaders -CommentsUrl $CommentsUrl + + } + + } + + } } Else { From 09b71b72591ebeacd84cfb39033ce3c723d2655a Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 15 Jul 2025 14:07:58 -0700 Subject: [PATCH 64/92] disable read-only sign off globally --- .github/workflows/Shared-TierManagement.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml index 6b6531b5236..29040c54c85 100644 --- a/.github/workflows/Shared-TierManagement.yml +++ b/.github/workflows/Shared-TierManagement.yml @@ -42,7 +42,10 @@ jobs: $AccessToken = $env:AccessToken $EnableWriteSignOff = [bool][int]$env:EnableWriteSignOff - $EnableReadOnlySignoff = [bool][int]$env:EnableReadOnlySignoff + + # Disabling read-only sign off globally. Switch to TCP doesn't include a read-only sign off and all changes must go through DRI. + # $EnableReadOnlySignoff = [bool][int]$env:EnableReadOnlySignoff + $EnableReadOnlySignoff = $False $DefaultBranch = $GitHubData.event.repository.default_branch $GitHubState = $GitHubData.event.issue.state From 17abd642e535dcdf87b9ac87c46f468005fab1e4 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 11 Aug 2025 11:49:36 -0700 Subject: [PATCH 65/92] Update Shared-TierManagement.yml --- .github/workflows/Shared-TierManagement.yml | 136 +++++++++++++++++++- 1 file changed, 131 insertions(+), 5 deletions(-) diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml index 29040c54c85..9b1914b2446 100644 --- a/.github/workflows/Shared-TierManagement.yml +++ b/.github/workflows/Shared-TierManagement.yml @@ -2,7 +2,7 @@ name: Tier management permissions: pull-requests: write - contents: read + contents: write on: workflow_call: @@ -21,9 +21,9 @@ on: required: true jobs: - build: - name: Run Script - if: github.repository_owner == 'MicrosoftDocs' + tier-management: + name: Tier management + if: github.repository_owner == 'MicrosoftDocs' && github.event_name == 'issue_comment' runs-on: ubuntu-latest steps: - name: Script @@ -447,4 +447,130 @@ jobs: } # PR event and action check - + set-draft: + name: Set PR as draft + if: github.repository_owner == 'MicrosoftDocs' && github.event_name == 'pull_request_target' + runs-on: ubuntu-latest + steps: + - name: Script + shell: pwsh + env: + PayloadJson: ${{ inputs.PayloadJson }} + AccessToken: ${{ secrets.AccessToken }} + + run: | + + # Get GitHub data and event + $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 + $GitRequestEvent = $GitHubData.event_name + + $AccessToken = $env:AccessToken + + $DefaultBranch = $GitHubData.event.repository.default_branch + $GitHubSender = $GitHubData.event.sender.login + $PrUrl = $GitHubData.event.pull_request.url + $CommentsUrl = $GitHubData.event.pull_request.comments_url + $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$GitHubSender/permission" ) + + $DraftMessage = "

Pull request set to Draft

Hi @{0}.

To avoid accidentally publishing the changes in this pull request prematurely, its state has been changed to Draft.

When you're ready for the changes in this pull request to be published live, select the Ready for review button at the bottom of the page.

If you have questions, please post a message to https://aka.ms/askanadmin." + + # Create github HTTP authentication header + $GitHubHeaders = @{} + $GitHubHeaders.Add("Authorization","token $($AccessToken)") + $GitHubHeaders.Add("User-Agent", "OfficeDocs") + + ##################### + ##################### + # Set-PrMessage + + Function Set-PrMessage { + + [cmdletbinding()] + Param( + [Parameter(Mandatory=$True)] + $Message + ) + + $BodyHash = @{} + $BodyHash.body = $Message + $BodyJson = $BodyHash | ConvertTo-Json + $BodyJson + + Try { + + $Result = Invoke-WebRequest -UseBasicParsing -Uri $CommentsUrl -Body $BodyJson -Headers $GitHubHeaders -Method POST -ErrorAction Stop + + $PostCommentSuccess = $True + + } Catch { + + $PostCommentSuccess = $False + + } + + Return $PostCommentSuccess + + } + + + ##################### + ##################### + # Main + + + # 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 + + Write-Host "User $GitHubSender permission level: $UserPermission." + + # If user has triage or above, do nothing, otherwise switch PR to draft. + If (($UserPermission -like "write*") -or ($UserPermission -eq "maintain") -or ($UserPermission -eq "triage") -or ($UserPermission -eq "admin")) { + + Write-Host "User has $UserPermission access. Not switching PR to draft." + + } Else { + + Write-Host "PR URL: $PrUrl" + + # REST: get PR node_id + $PrData = Invoke-RestMethod -Method Get -Headers $GitHubHeaders -Uri $PrUrl + $PrNodeId = $PrData.node_id + + Write-Host "Setting PR $($PrData.number) to draft. Node ID: $PrNodeId." + + $Body = @{ + query = @' + mutation($id: ID!){ + convertPullRequestToDraft(input:{pullRequestId:$id}) { + pullRequest { number url isDraft } + } + } + '@ + variables = @{ id = $PrNodeId } + } | ConvertTo-Json -Depth 5 + + + Try { + + $Resp = Invoke-RestMethod -Method Post -Uri "https://api.github.com/graphql" -Headers $GitHubHeaders -ContentType 'application/json' -Body $Body + + # Show errors if any, otherwise show the PR + If ($Resp.errors) { + Write-Error ("GraphQL error(s): " + ($Resp.errors | ConvertTo-Json -Depth 10)) + } Else { + $Resp.data.convertPullRequestToDraft.pullRequest | ConvertTo-Json -Depth 5 + } + + $DraftMessage = $DraftMessage -f $GitHubSender + + Set-PrMessage -Message $DraftMessage + + } Catch { + + Write-Host "ERROR: Failed to set PR to draft. Error: $_" + + } + + + } From c5857a83fbc6977482d8cfaf1d6c9f4721fcb5de Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 11 Aug 2025 22:48:59 -0700 Subject: [PATCH 66/92] Skip running for m365-skilling-repo-management[bot] --- .github/workflows/Shared-TierManagement.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml index 9b1914b2446..a365a81200f 100644 --- a/.github/workflows/Shared-TierManagement.yml +++ b/.github/workflows/Shared-TierManagement.yml @@ -449,7 +449,7 @@ jobs: set-draft: name: Set PR as draft - if: github.repository_owner == 'MicrosoftDocs' && github.event_name == 'pull_request_target' + if: github.repository_owner == 'MicrosoftDocs' && github.event_name == 'pull_request_target' && github.event.pull_request.user.login != 'm365-skilling-repo-management[bot]' runs-on: ubuntu-latest steps: - name: Script From 7461394fc5642655c2c6c6b963d920eca5888108 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:08:01 -0700 Subject: [PATCH 67/92] Update set as draft workflow to run only if user isn't a bot --- .github/workflows/Shared-TierManagement.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml index a365a81200f..eaf42f57a2e 100644 --- a/.github/workflows/Shared-TierManagement.yml +++ b/.github/workflows/Shared-TierManagement.yml @@ -449,7 +449,7 @@ jobs: set-draft: name: Set PR as draft - if: github.repository_owner == 'MicrosoftDocs' && github.event_name == 'pull_request_target' && github.event.pull_request.user.login != 'm365-skilling-repo-management[bot]' + if: github.repository_owner == 'MicrosoftDocs' && github.event_name == 'pull_request_target' && !contains(github.event.pull_request.user.login, '[bot]') runs-on: ubuntu-latest steps: - name: Script From 2b6dc1bcab13a2c0e492fb103553b7812490ffd3 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:10:17 -0700 Subject: [PATCH 68/92] Update contact info to ask an admin --- .github/workflows/Shared-AutoPublishV2.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/Shared-AutoPublishV2.yml b/.github/workflows/Shared-AutoPublishV2.yml index 53a344117e3..0b11f611725 100644 --- a/.github/workflows/Shared-AutoPublishV2.yml +++ b/.github/workflows/Shared-AutoPublishV2.yml @@ -95,10 +95,10 @@ jobs: # PR variables $PrTitle = "[AutoPublish] $DefaultBranch to $TargetBranch" - $AutoMergeDisabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore merging this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, email marveldocs-admins." - $AutoMergeDisabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore you merge this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **A contributor with write access to this repo will need to merge this PR for changes in it to go live.`n`nIf you have questions, email marveldocs-admins." - $AutoMergeEnabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, email marveldocs-admins." - $AutoMergeEnabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **If a manual merge is required, a contributor with write access to this repo will need to merge this PR for changes in it to go live.**`n`nIf you have questions, email marveldocs-admins." + $AutoMergeDisabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore merging this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, post a message to https://aka.ms/askanadmin." + $AutoMergeDisabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore you merge this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **A contributor with write access to this repo will need to merge this PR for changes in it to go live.`n`nIf you have questions, post a message to https://aka.ms/askanadmin." + $AutoMergeEnabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, post a message to https://aka.ms/askanadmin." + $AutoMergeEnabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **If a manual merge is required, a contributor with write access to this repo will need to merge this PR for changes in it to go live.**`n`nIf you have questions, post a message to https://aka.ms/askanadmin." # Label variables $AutoPublishLabelColor = "5319E7" 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 69/92] 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 70/92] 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 71/92] 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 72/92] 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 73/92] 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 74/92] 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 75/92] 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." From 8d245172b96aa76ceff5805f3adc63e786202a44 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 6 Oct 2025 12:21:34 -0700 Subject: [PATCH 76/92] Handle null reviewer array and missing file service --- .github/workflows/Shared-AutoLabelAssign.yml | 74 +++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 58ebbcd8888..6358d52d90e 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -145,14 +145,16 @@ jobs: # Initialize variables. $FileData = @{} + $FileName = $File.filename $MetadataFound = $False + # Check to see whether current file is markdown or YAML. Those are the only files we care about. Ignore the rest. - $IsContentFile = ($File.filename.EndsWith(".md") -or $File.filename.EndsWith(".yml")) -and !$File.filename.ToLower().Contains("/toc.") + $IsContentFile = ($FileName.EndsWith(".md") -or $FileName.EndsWith(".yml")) -and !$FileName.ToLower().Contains("/toc.") # Only process content files. If ($IsContentFile) { - Write-Host "Processing file $($File.filename)." + Write-Host "Processing file $FileName." # Retrieve file contents from GitHub. File contents is returned in Base 64 so after contents is retrieved, convert from Base 64 to plain text. $FileContentsBase64 = Invoke-RestMethod -Method GET -Uri $File.contents_url -Headers $GitHubHeaders @@ -217,11 +219,12 @@ jobs: Technology = $Technology Tier = $Tier Author = $Author + FileName = $FileName } # If any metadata or author data was found, add it to the $FileArray output array. If ($MetadataFound) { - Write-Host "Metadata or author data found on $($File.filename). Adding to output array." + Write-Host "Metadata or author data found on $FileName. Adding to output array." $FileArray += $FileData } } @@ -254,6 +257,7 @@ jobs: If ($File.Tier -ne $Null) {$MetadataArray += $File.Tier.SubString(0,1).ToUpper() + $File.Tier.SubString(1).ToLower()} If ($File.Author -ne $Null) {$AuthorArray += $File.Author} + } # Because there might be multiple files in the $MetaDataArray and $AuthorArrays with the same metadata and author data, duplicate values might have been added to the output arrays. @@ -1073,14 +1077,22 @@ jobs: $ReviewerArray = @() ForEach ($File in $FileMetadataArray) { - - $FileReviewers = $Null - $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + If ($File.Service -ne $Null) { - If ($FileReviewers -ne $Null) { - - $ReviewerArray += $FileReviewers + $FileReviewers = $Null + + $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + + If ($FileReviewers -ne $Null) { + + $ReviewerArray += $FileReviewers + + } + + } Else { + + Write-Host "No service found for file $($File.FileName)." } @@ -1088,37 +1100,45 @@ jobs: $ReviewerArray = $ReviewerArray | Select-Object -Unique - Write-Host "Checking org membership." + If ($ReviewerArray.Count -gt 0) { - $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray - $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } + Write-Host "Checking org membership." - $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts + $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray + $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } - If ($ValidatedReviewerAccounts.Length -gt 0) { + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts - $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ValidatedReviewerAccounts + If ($ValidatedReviewerAccounts.Length -gt 0) { - If ($MissingReviewers) { - - Write-Host "Additional reviewers found. Setting reviewers and posting PR comment." + $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ValidatedReviewerAccounts - $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") + If ($MissingReviewers) { + + Write-Host "Additional reviewers found. Setting reviewers and posting PR comment." - Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts - Set-PrConversationMessage -Message $ReviewerMessage + $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") - } Else { + 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." + Write-Host "No additional reviewers to add. Not setting reviewers or posting PR comment." + + } + } Else { + + Write-Host "No valid reviewers to assign." + Write-Host $TestedReviewerAccounts + } } Else { - - Write-Host "No valid reviewers to assign." - Write-Host $TestedReviewerAccounts - + + Write-Host "No reviewers matched any services or subservices in PR articles." + } From 32eab1b60dbf2b10b8d7ff217cec610b99032e9d Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Mon, 6 Oct 2025 22:07:39 -0700 Subject: [PATCH 77/92] switching pr message and mapping file to prod --- .github/workflows/Shared-AutoLabelAssign.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 6358d52d90e..ed571391aa7 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -112,11 +112,11 @@ jobs: $ServiceToGitHubUserMapRepo = "officedocs-pr" $ServiceToGitHubUserMapFilePath = "/contents/.github/workflows/resources/service-subservice-to-github-user-map.csv" - $ServiceToGitHubUserMapRef = "reviewer-test" + $ServiceToGitHubUserMapRef = "main" $ServiceToGitHubUserMapUrl = $GitHubApiUrl + $ServiceToGitHubUserMapRepo + $ServiceToGitHubUserMapFilePath + "?ref=$ServiceToGitHubUserMapRef" $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" - $WorkflowsRef = "workflows-test" + $WorkflowsRef = "workflows-prod" # 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/" @@ -1197,4 +1197,5 @@ jobs: Write-Host "Event action not ready_for_review, opened, reopened, or synchronize." + } # PR event and action check From cc8b9d53313aabcf67b87f5113e826c3b6bc6cfb Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Tue, 7 Oct 2025 14:06:25 -0700 Subject: [PATCH 78/92] Hook up auto reviewer enable/disable toggle --- .github/workflows/Shared-AutoLabelAssign.yml | 116 ++++++++++--------- 1 file changed, 64 insertions(+), 52 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index ed571391aa7..899eae67802 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -1056,104 +1056,116 @@ jobs: } - # Auto user assignment can be disabled on repos using the $DataTableName table. + # Auto user assignment can be disabled on repos in the calling workflow. If ($AutoAssignUsers -eq $True) { - # 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)) { + ###################################### Auto reviewer assignment ###################################### - # 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 + If ($AutoAssignReviewers -eq $True) { + + # 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)) { + + # 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 + + # Only add reviewers if the submitter can't sign off on their own PR. + If ($UserPermission -eq "read") { - # Only add reviewers if the submitter can't sign off on their own PR. - If ($UserPermission -eq "read") { + # Get the service/subservice to GitHub account map + $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable + $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap - # Get the service/subservice to GitHub account map - $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable - $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap + $ReviewerArray = @() - $ReviewerArray = @() + ForEach ($File in $FileMetadataArray) { - ForEach ($File in $FileMetadataArray) { + If ($File.Service -ne $Null) { - If ($File.Service -ne $Null) { + $FileReviewers = $Null - $FileReviewers = $Null + $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService - $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + If ($FileReviewers -ne $Null) { + + $ReviewerArray += $FileReviewers + + } - If ($FileReviewers -ne $Null) { + } Else { - $ReviewerArray += $FileReviewers + Write-Host "No service found for file $($File.FileName)." } - - } Else { - - Write-Host "No service found for file $($File.FileName)." } - - } - $ReviewerArray = $ReviewerArray | Select-Object -Unique + $ReviewerArray = $ReviewerArray | Select-Object -Unique - If ($ReviewerArray.Count -gt 0) { + If ($ReviewerArray.Count -gt 0) { - Write-Host "Checking org membership." + Write-Host "Checking org membership." - $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray - $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } + $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray + $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } - $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts - If ($ValidatedReviewerAccounts.Length -gt 0) { + If ($ValidatedReviewerAccounts.Length -gt 0) { - $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ValidatedReviewerAccounts + $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ValidatedReviewerAccounts - If ($MissingReviewers) { - - Write-Host "Additional reviewers found. Setting reviewers and posting PR comment." + If ($MissingReviewers) { + + Write-Host "Additional reviewers found. Setting reviewers and posting PR comment." + + $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") - $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + 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 "No additional reviewers to add. Not setting reviewers or posting PR comment." + } - } + } Else { + + Write-Host "No valid reviewers to assign." + Write-Host $TestedReviewerAccounts + + } } Else { - - Write-Host "No valid reviewers to assign." - Write-Host $TestedReviewerAccounts + Write-Host "No reviewers matched any services or subservices in PR articles." + } + } Else { - - Write-Host "No reviewers matched any services or subservices in PR articles." - + + Write-Host "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." + } - } Else { - Write-Host "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." + Write-Host "Not adding PR reviewers. PR action: $GitHubAction. Draft state: $IsPrDraft." } } Else { - Write-Host "Not adding PR reviewers. PR action: $GitHubAction. Draft state: $IsPrDraft." + Write-Host "Auto reviewer assignment disabled." } + ###################################### Auto user assignment ###################################### + # Don't add assignments to PRs in excluded branches listed in $ExcludedBranchList If (!$ExcludedBranchList.Contains($TargetBranch)) { From 9d12539ca429a01113b0908b33b8b644069e4662 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 8 Oct 2025 15:01:58 -0700 Subject: [PATCH 79/92] Scope to build repos only --- .github/workflows/Shared-AutoLabelAssign.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 899eae67802..ac78085d5ce 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -36,7 +36,7 @@ on: jobs: build: name: Run Script - if: github.repository_owner == 'MicrosoftDocs' + if: github.repository_owner == 'MicrosoftDocs' && contains(github.event.repository.topics, 'build') runs-on: ubuntu-latest steps: @@ -1210,4 +1210,5 @@ jobs: Write-Host "Event action not ready_for_review, opened, reopened, or synchronize." + } # PR event and action check From 415199839b4aaa0e9037028959d22f967b8e9755 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 9 Oct 2025 17:22:07 -0700 Subject: [PATCH 80/92] Add logging functionality --- .github/workflows/Shared-AutoLabelAssign.yml | 131 ++++++++++++++++++- 1 file changed, 128 insertions(+), 3 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index ac78085d5ce..c0ae921d092 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -62,6 +62,10 @@ jobs: run: | # Get runspace info $RepoRoot = $env:RUNNER_WORKSPACE + $RepoName = $env:GITHUB_REPOSITORY + $WorkflowName = $env:GITHUB_WORKFLOW -replace '[\\/:*?"<>|\s]', '_' + $WorkflowRunId = $env:GITHUB_RUN_ID + $WorkflowRunAttempt = $env:GITHUB_RUN_ATTEMPT $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $AccessToken = $env:AccessToken $GitHubApiUrl = "https://api.github.com/repos/MicrosoftDocs/" @@ -71,7 +75,8 @@ jobs: $PrCreator = $GitHubData.event.pull_request.user.login $TargetBranch = $GitHubData.event.pull_request.base.ref $GitHubState = $GitHubData.event.pull_request.state - $GitHubAction = $GithubData.event.action + $GitHubAction = $GitHubData.event.action + $PrNumber = $GitHubData.event.pull_request.number $IsPrDraft = $GitHubData.event.pull_request.draft $GitHubSender = $GitHubData.event.sender.login $GitHubRepoName = $GitHubData.event.repository.name @@ -79,6 +84,7 @@ jobs: $PrFileListUrl = "$($GitHubData.event.pull_request.url)/files" $IssueUrl = $GitHubData.event.pull_request.issue_url $PrUrl = $GitHubData.event.pull_request.url + $PrHtmlUrl = $GitHubData.event.pull_request.html_url $CommentsUrl = $GitHubData.event.pull_request.comments_url $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$PrCreator/permission" ) @@ -110,11 +116,41 @@ jobs: $LabelColor = "BFDADC" $LabelDescription = "" + # Central workflow logging parameters + $LoggingRepoName = "officedocs-pr" + $LoggingBranch = "logging" + $LoggingRootDir = ".github/workflow-logs" + $Year = Get-Date -Format yyyy + $Month = Get-Date -Format MM + $LoggingFilePath = "$LoggingRootDir/$RepoName/$Year/$Month/PR$PrNumber/workflow-runs/$WorkflowName/${WorkflowRunId}_${WorkflowRunAttempt}.csv" + + $LoggingData = [pscustomobject][ordered]@{ + RepoName = $RepoName + PrNumber = $PrNumber + PrUrl = $PrHtmlUrl + PrCreator = $PrCreator + WorkflowSender = $GitHubSender + PrIsDraft = $IsPrDraft + InitialReviewerList = $Null + ValidatedReviewerList = $Null + ReviewStatus = $Null + ReviewDetails = $Null + ServiceSubService = $Null + PrEvent = $GitRequestEvent + PrAction = $GitHubAction + PrState = $GitHubState + WorkflowName = $WorkflowName + WorkflowRunId = $WorkflowRunId + TimeStamp = (Get-Date).ToString("o") + } + + # Path to GE Taxonomy service/subservice to Content Lead GitHub account mapping file. $ServiceToGitHubUserMapRepo = "officedocs-pr" $ServiceToGitHubUserMapFilePath = "/contents/.github/workflows/resources/service-subservice-to-github-user-map.csv" $ServiceToGitHubUserMapRef = "main" $ServiceToGitHubUserMapUrl = $GitHubApiUrl + $ServiceToGitHubUserMapRepo + $ServiceToGitHubUserMapFilePath + "?ref=$ServiceToGitHubUserMapRef" + # Path to central workflow resources location. $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" $WorkflowsRef = "workflows-prod" @@ -963,6 +999,59 @@ jobs: } + ##################### + ##################### + # Send-WorkflowLog + + Function Send-WorkflowLog { + [CmdletBinding()] + Param( + + [Parameter(Mandatory=$true)] + [PSCustomObject[]]$LogData + + ) + + + # Convert to CSV + $CsvContent = $LogData | ConvertTo-Csv -NoTypeInformation | Out-String + $Base64Content = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($CsvContent)) + + $FileUrl = "https://api.github.com/repos/MicrosoftDocs/$LoggingRepoName/contents/$LoggingFilePath" + + Try { + $ExistingFile = Invoke-RestMethod -Uri $FileUrl -Headers $AppGitHubAccessHeaders -Method Get -StatusCodeVariable StatusCode + $Sha = $ExistingFile.sha + } Catch { + $Sha = $null + } + + # Create or update file + $Body = @{ + message = "Log from $RepoName workflow $WorkflowName (run $WorkflowRunId)" + content = $Base64Content + branch = $LoggingBranch + } + + If ($Sha) { + $Body.sha = $Sha + } + + Try { + + $Response = Invoke-RestMethod -Uri $FileUrl -Headers $AppGitHubAccessHeaders -Method Put -Body ($Body | ConvertTo-Json) + + } Catch { + + Write-Host "ERROR: Failed to post workflow log." + + } + + Write-Host "Log pushed to $LoggingFilePath" + Return $Response + } + + ##################### ##################### # Main @@ -1079,6 +1168,7 @@ jobs: $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap $ReviewerArray = @() + $ServiceSubServiceArray = @() # Used in logging ForEach ($File in $FileMetadataArray) { @@ -1088,6 +1178,8 @@ jobs: $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + $ServiceSubServiceArray += "$($File.Service)\$($File.Subservice)" + If ($FileReviewers -ne $Null) { $ReviewerArray += $FileReviewers @@ -1123,25 +1215,46 @@ jobs: $ReviewerMessage = $AtMentionedGitHubAccounts + $(Get-PrMessage "AutoLabelAssign-ContentLeadReviewNotice") - Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts - Set-PrConversationMessage -Message $ReviewerMessage + Try { + + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + Set-PrConversationMessage -Message $ReviewerMessage + + $LoggingData.ReviewStatus = "ReviewerAddSuccess" + + } Catch { + + $LoggingData.ReviewStatus = "ReviewerAddFailed" + $LoggingData.ReviewDetails = $_ + + } } Else { Write-Host "No additional reviewers to add. Not setting reviewers or posting PR comment." + $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewDetails = "No additional reviewers to add." + } } Else { Write-Host "No valid reviewers to assign." Write-Host $TestedReviewerAccounts + + $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewDetails = "No valid reviewers to assign." } } Else { Write-Host "No reviewers matched any services or subservices in PR articles." + + $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewDetails = "No reviewers matched any services or subservices." + } @@ -1149,15 +1262,27 @@ jobs: } Else { Write-Host "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." + + $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewDetails = "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." } } Else { Write-Host "Not adding PR reviewers. PR action: $GitHubAction. Draft state: $IsPrDraft." + + $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewDetails = "Not adding reviewers because PR is draft." } + $LoggingData.ServiceSubService = $($ServiceSubServiceArray | Select-Object -Unique) -Join ";" + $LoggingData.InitialReviewerList = $ReviewerArray -Join ";" + $Loggingdata.ValidatedReviewerList = $ValidatedReviewerAccounts -Join ";" + + Send-WorkflowLog -LogData $LoggingData + } Else { Write-Host "Auto reviewer assignment disabled." From 6af8d7aa55fcb3751aa5e0ee074785b4d373560d Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 10 Oct 2025 14:25:14 -0700 Subject: [PATCH 81/92] ignore bots, improve enums, fix perm check --- .github/workflows/Shared-AutoLabelAssign.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index c0ae921d092..6bfb799c55f 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -1150,7 +1150,7 @@ jobs: ###################################### Auto reviewer assignment ###################################### - If ($AutoAssignReviewers -eq $True) { + If (($AutoAssignReviewers -eq $True) -and ($PrCreator -notlike "*\[bot\]*")) { # 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. @@ -1161,7 +1161,7 @@ jobs: $UserPermission = $(Invoke-RestMethod -Method GET -Headers $GitHubHeaders -Uri $UserPermissionUrl).role_name # Only add reviewers if the submitter can't sign off on their own PR. - If ($UserPermission -eq "read") { + If (($UserPermission -eq "read") -or ($UserPermission -eq "") -or ($UserPermission -eq $Null)) { # Get the service/subservice to GitHub account map $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable @@ -1233,7 +1233,7 @@ jobs: Write-Host "No additional reviewers to add. Not setting reviewers or posting PR comment." - $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewStatus = "NoAdditionalReviewers" $LoggingData.ReviewDetails = "No additional reviewers to add." } @@ -1243,7 +1243,7 @@ jobs: Write-Host "No valid reviewers to assign." Write-Host $TestedReviewerAccounts - $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewStatus = "NoValidReviewers" $LoggingData.ReviewDetails = "No valid reviewers to assign." } @@ -1252,7 +1252,7 @@ jobs: Write-Host "No reviewers matched any services or subservices in PR articles." - $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewStatus = "NoMatchedSvcSubSvc" $LoggingData.ReviewDetails = "No reviewers matched any services or subservices." @@ -1263,7 +1263,7 @@ jobs: Write-Host "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." - $LoggingData.ReviewStatus = "ReviewerAddSkipped" + $LoggingData.ReviewStatus = "NotReadOnly" $LoggingData.ReviewDetails = "Not adding reviewer because submitter can sign off PR. User permission: $UserPermission." } @@ -1272,8 +1272,8 @@ jobs: Write-Host "Not adding PR reviewers. PR action: $GitHubAction. Draft state: $IsPrDraft." - $LoggingData.ReviewStatus = "ReviewerAddSkipped" - $LoggingData.ReviewDetails = "Not adding reviewers because PR is draft." + $LoggingData.ReviewStatus = "PrDraftOrOpened" + $LoggingData.ReviewDetails = "Not adding reviewers because PR is draft or just opened." } @@ -1285,7 +1285,7 @@ jobs: } Else { - Write-Host "Auto reviewer assignment disabled." + Write-Host "Auto reviewer assignment disabled or PR creator is bot account. Assignment enabled: $AutoAssignReviewers. PR creator: $PrCreator." } From bbbc9288550eee6ccf89541e5711b5cc6fbdaa3c Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 15 Oct 2025 11:54:23 -0700 Subject: [PATCH 82/92] Clarify sign off label requirements for reviewers --- .../resources/AutoLabelAssign-ContentLeadReviewNotice.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md b/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md index d02afe25bb8..0485d762e9a 100644 --- a/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md +++ b/.github/workflows/resources/AutoLabelAssign-ContentLeadReviewNotice.md @@ -1,7 +1,7 @@ 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: +Before this PR can be merged, a Content Lead needs to add the **Sign off** label to it. Only **one** of the listed reviewers needs to add the **Sign off** label. 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. @@ -15,6 +15,8 @@ After you've confirmed the above, do the following to sign off: 1. Select **Sign off** from the label list. 1. Click away from the label list. +![image](https://learn.microsoft.com/en-us/office/media/internal/sign-off-label.jpg) + 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. From da83c98cafc899b0cc9412af1d244b40ac1f1631 Mon Sep 17 00:00:00 2001 From: "Gary Moore (SC-ALT)" <186231600+garymoore-sc@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:42:59 -0700 Subject: [PATCH 83/92] Replace Ask An Admin with TC > General --- .../resources/PrFileCountCheck-PrivateWarningMessage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md b/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md index 0c56c433765..e45ae93c74c 100644 --- a/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md +++ b/.github/workflows/resources/PrFileCountCheck-PrivateWarningMessage.md @@ -23,6 +23,6 @@ If you can't confirm the changes in this pull request are correct, click **Close ### Need help? -If you need help, post a message to https://aka.ms/askanadmin. +If you need help, post a message to [Trusted Content > General](https://teams.microsoft.com/l/channel/19%3A0rVEQfEKaFLYpOAxHoDvnc62RJNe1tKQ2gRFYLrm5kc1%40thread.tacv2/General?groupId=42b99b14-8a2d-4fe4-ae7e-76cfe6221f4a&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47). \* You must have write or triage access to this repo to add labels. From 7ce00338dd1867930e3e2597045df997886c8af5 Mon Sep 17 00:00:00 2001 From: Gary Moore <5432776+garycentric@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:55:40 -0700 Subject: [PATCH 84/92] Update Ask An Admin to TC > General --- .../workflows/resources/PrFileCountCheck-PrivateBlockMessage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md b/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md index 702e76f3c4c..88958bf147d 100644 --- a/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md +++ b/.github/workflows/resources/PrFileCountCheck-PrivateBlockMessage.md @@ -23,6 +23,6 @@ If you want to abandon these changes and not merge this PR, click **Close pull r ### Need help? -If you need help, post a message to https://aka.ms/askanadmin. +If you need help, post a message to [TC > General](https://teams.microsoft.com/l/channel/19%3A0rVEQfEKaFLYpOAxHoDvnc62RJNe1tKQ2gRFYLrm5kc1%40thread.tacv2/General?groupId=42b99b14-8a2d-4fe4-ae7e-76cfe6221f4a&tenantId=72f988bf-86f1-41af-91ab-2d7cd011db47). \* You must have write or triage access to this repo to add labels. From 15c9ee7b7561a95a66a66187c90174e8bdee1dd7 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:36:04 -0700 Subject: [PATCH 85/92] Refactored to use github teams as reviewer lists --- .github/workflows/Shared-AutoLabelAssign.yml | 179 ++++++++++--------- 1 file changed, 90 insertions(+), 89 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 6bfb799c55f..e430be8370b 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -51,6 +51,20 @@ jobs: - name: Script shell: pwsh env: + GlobalExcludedReviewerList: '[ + "dstrome", + "sc-dstrome", + "garycentric", + "garymoore-sc", + "serdarsoysal" + "cx-ktsuji", + "dawntanner", + "pamgreen-msft", + "pebaum", + "SerdarSoysal", + "vinaypamnani-msft" + ]' + PayloadJson: ${{ inputs.PayloadJson }} AccessToken: ${{ secrets.AccessToken }} AppGitHubAccessToken: ${{ steps.app-token.outputs.token }} @@ -59,6 +73,7 @@ jobs: AutoLabel: ${{ inputs.AutoLabel }} ExcludedUserList: ${{ inputs.ExcludedUserList }} ExcludedBranchList: ${{ inputs.ExcludedBranchList }} + run: | # Get runspace info $RepoRoot = $env:RUNNER_WORKSPACE @@ -68,6 +83,7 @@ jobs: $WorkflowRunAttempt = $env:GITHUB_RUN_ATTEMPT $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $AccessToken = $env:AccessToken + $GlobalExcludedReviewerList = $env:GlobalExcludedReviewerList $GitHubApiUrl = "https://api.github.com/repos/MicrosoftDocs/" $GitRequestEvent = $GitHubData.event_name @@ -144,19 +160,16 @@ jobs: TimeStamp = (Get-Date).ToString("o") } - # Path to GE Taxonomy service/subservice to Content Lead GitHub account mapping file. - $ServiceToGitHubUserMapRepo = "officedocs-pr" - $ServiceToGitHubUserMapFilePath = "/contents/.github/workflows/resources/service-subservice-to-github-user-map.csv" - $ServiceToGitHubUserMapRef = "main" - $ServiceToGitHubUserMapUrl = $GitHubApiUrl + $ServiceToGitHubUserMapRepo + $ServiceToGitHubUserMapFilePath + "?ref=$ServiceToGitHubUserMapRef" + # Path to GE Taxonomy service/subservice to Content Lead GitHub team mapping file. + $ServiceToGitHubTeamMapRepo = "officedocs-pr" + $ServiceToGitHubTeamMapFilePath = "/contents/.github/workflows/resources/service-subservice-to-github-team-map.csv" + $ServiceToGitHubTeamMapRef = "main" + $ServiceToGitHubTeamMapUrl = $GitHubApiUrl + $ServiceToGitHubTeamMapRepo + $ServiceToGitHubTeamMapFilePath + "?ref=$ServiceToGitHubTeamMapRef" # Path to central workflow resources location. $WorkflowsResourcePath = "https://api.github.com/repos/MicrosoftDocs/microsoft-365-docs/contents/.github/workflows/resources" $WorkflowsRef = "workflows-prod" - # 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 @@ -614,20 +627,20 @@ jobs: ##################### ##################### - # Get-ServiceGitHubAccountMappingTable + # Get-ServiceGitHubTeamMappingTable - Function Get-ServiceGitHubAccountMappingTable { + Function Get-ServiceGitHubTeamMappingTable { Try { - Write-Host "Getting service/subservice to GitHub user mapping from $ServiceToGitHubUserMapUrl" + Write-Host "Getting service/subservice to GitHub team mapping from $ServiceToGitHubTeamMapUrl" - $GitHubMappingTableData = Invoke-RestMethod -Uri $ServiceToGitHubUserMapUrl -Headers $AppGitHubAccessHeaders + $GitHubMappingTableData = Invoke-RestMethod -Uri $ServiceToGitHubTeamMapUrl -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" + Write-Host "Failed to get service/subservice to GitHub user mapping from $ServiceToGitHubTeamMapUrl" $GitHubMappingTable = $Null @@ -670,7 +683,7 @@ jobs: Product = $Row.Product Service = $Service SubService = $SubService - ContentLeadGitHubAccounts = $Row.ContentLeadGitHubAccounts + GitHubTeam = $Row.GitHubTeam } $ExpandedRows += $NewRow } @@ -682,9 +695,9 @@ jobs: ##################### ##################### - # Get-ContentLeadAccounts + # Get-GitHubReviewerTeams - Function Get-ContentLeadAccounts { + Function Get-GitHubReviewerTeams { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] @@ -698,7 +711,7 @@ jobs: ) - $AccountsList = @() + $GitHubTeam = @() Write-Host "Article service: $Service" Write-Host "Article subservice: $SubService" @@ -726,7 +739,7 @@ jobs: Write-Host "SubService found" # Found exact match with SubService - $AccountsList += $ExactMatch.contentleadgithubaccounts + $GitHubTeam += $ExactMatch.GitHubTeam } Else { @@ -740,10 +753,10 @@ jobs: If ($HasSubServices) { - Write-Host "Service has subservices. Returning all accounts for all subservices." + Write-Host "Service has subservices. Returning all teams for all subservices." - # Multiple subservices exist, return all accounts for this service's subservices - $AccountsList += $HasSubServices.contentleadgithubaccounts + # Multiple subservices exist, return all teams for this service's subservices + $GitHubTeam += $HasSubServices.GitHubTeam } } } @@ -758,7 +771,7 @@ jobs: Write-Host "Service with no subservice row found" # Found exact match without SubService - $AccountsList += $ExactMatch.contentleadgithubaccounts + $GitHubTeam += $ExactMatch.GitHubTeam } Else { @@ -772,37 +785,37 @@ jobs: If ($HasSubServices.Count -gt 1) { - Write-Host "Service has subservices. Returning all accounts for all subservices." + Write-Host "Service has subservices. Returning all teams for all subservices." - # Multiple subservices exist, return all accounts - $AccountsList += $HasSubServices.contentleadgithubaccounts + # Multiple subservices exist, return all teams + $GitHubTeam += $HasSubServices.GitHubTeam } } } } - Write-Host "Found accounts" - $AccountsList | ForEach-Object { Write-Host $_ } + Write-Host "Found teams" + $GitHubTeam | ForEach-Object { Write-Host $_ } - # Process the accounts list - $AllAccounts = @() + # Process the teams list + $AllTeams = @() - ForEach ($AccountString In $AccountsList) { - If (![string]::IsNullOrWhiteSpace($AccountString)) { + ForEach ($TeamString In $GitHubTeam) { + If (![string]::IsNullOrWhiteSpace($TeamString)) { # Remove leading semicolon if present - $CleanedString = $AccountString.TrimStart(';') + $CleanedString = $TeamString.TrimStart(';') # Split by semicolon and add to array - $Accounts = $CleanedString -split ';' | Where-Object { + $Teams = $CleanedString -split ';' | Where-Object { ![string]::IsNullOrWhiteSpace($_) } - $AllAccounts += $Accounts + $AllTeams += $Teams } } # Return deduplicated array - Return $AllAccounts | Select-Object -Unique + Return $AllTeams | Select-Object -Unique } @@ -902,58 +915,53 @@ jobs: ##################### ##################### - # Test-OrgMembership + # Get-TeamMembership - Function Test-OrgMembership { + Function Get-TeamMembership { [CmdletBinding()] Param( [Parameter(Mandatory = $true)] - [string[]]$GitHubReviewers + [string[]]$ReviewerTeams ) - $ReviewerHashTable = @{} + $ReviewerList = @() - 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 + ForEach ($Team in $ReviewerTeams) { + + $ReviewerTeamUrl = "https://api.github.com/orgs/MicrosoftDocs/teams/$Team/members?per_page=100" 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 "Getting membership information for $Team." - Write-Host "$Reviewer is an active member of the MicrosoftDocs `"everyone`" team." + $TeamResult = Invoke-RestMethod -Uri $ReviewerTeamUrl -Headers $AppGitHubAccessHeaders -FollowRelLink -ErrorAction Stop - $ReviewerHashTable.Add($Reviewer, $True) - - } Else { + ForEach ($Page in $TeamResult) { - Write-Host "$Reviewer isn't an active member of the MicrosoftDocs `"everyone`" team." - - $ReviewerHashTable.Add($Reviewer, $False) - + ForEach ($Member in $Page) { + + If ($GlobalExcludedReviewerList -notcontains $Member.login) { + + $ReviewerList += $Member.login + + } + + } } - + } Catch { - - Write-Host "ERROR: Failed to look up membership info for $Reviewer. Error: $_" + + Write-Host "ERROR: Failed to look up membership info for $Team. Error: $_" - # If the call fails, set to false to avoid breaking add reviewer call later in workflow. - $ReviewerHashTable.Add($Reviewer, $False) - } - + } + + Write-Host "Reviewers found: $($ReviewerList | Sort-Object -Unique)" - Return $ReviewerHashTable - + Return $ReviewerList | Sort-Object -Unique } @@ -1164,25 +1172,25 @@ jobs: If (($UserPermission -eq "read") -or ($UserPermission -eq "") -or ($UserPermission -eq $Null)) { # Get the service/subservice to GitHub account map - $ServiceToAccountMap = Get-ServiceGitHubAccountMappingTable - $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToAccountMap + $ServiceToTeamMap = Get-ServiceGitHubTeamMappingTable + $ExpandedServiceSubServiceRows = Expand-ServiceSubServiceRows -CsvContent $ServiceToTeamMap - $ReviewerArray = @() + $ReviewerTeamArray = @() $ServiceSubServiceArray = @() # Used in logging ForEach ($File in $FileMetadataArray) { If ($File.Service -ne $Null) { - $FileReviewers = $Null + $ReviewerTeams = $Null - $FileReviewers = Get-ContentLeadAccounts -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService + $ReviewerTeams = Get-GitHubReviewerTeams -DataArray $ExpandedServiceSubServiceRows -Service $File.Service -SubService $File.SubService $ServiceSubServiceArray += "$($File.Service)\$($File.Subservice)" - If ($FileReviewers -ne $Null) { + If ($ReviewerTeams -ne $Null) { - $ReviewerArray += $FileReviewers + $ReviewerTeamArray += $ReviewerTeams } @@ -1194,20 +1202,17 @@ jobs: } - $ReviewerArray = $ReviewerArray | Select-Object -Unique + $ReviewerTeamArray = $ReviewerTeamArray | Select-Object -Unique - If ($ReviewerArray.Count -gt 0) { + If ($ReviewerTeamArray.Count -gt 0) { - Write-Host "Checking org membership." + $ReviewerAccounts = Get-TeamMembership -ReviewerTeams $ReviewerTeamArray - $TestedReviewerAccounts = Test-OrgMembership -GitHubReviewers $ReviewerArray - $ValidatedReviewerAccounts = $TestedReviewerAccounts.keys | Where-Object { $TestedReviewerAccounts[$_] } + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ReviewerAccounts - $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ValidatedReviewerAccounts + If ($ReviewerAccounts.Length -gt 0) { - If ($ValidatedReviewerAccounts.Length -gt 0) { - - $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ValidatedReviewerAccounts + $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ReviewerAccounts If ($MissingReviewers) { @@ -1217,7 +1222,7 @@ jobs: Try { - Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ValidatedReviewerAccounts + Set-PrReviewer -PrUrl $PrUrl -GitHubReviewers $ReviewerAccounts Set-PrConversationMessage -Message $ReviewerMessage $LoggingData.ReviewStatus = "ReviewerAddSuccess" @@ -1278,8 +1283,8 @@ jobs: } $LoggingData.ServiceSubService = $($ServiceSubServiceArray | Select-Object -Unique) -Join ";" - $LoggingData.InitialReviewerList = $ReviewerArray -Join ";" - $Loggingdata.ValidatedReviewerList = $ValidatedReviewerAccounts -Join ";" + $LoggingData.InitialReviewerList = $ReviewerTeamArray -Join ";" + $Loggingdata.ValidatedReviewerList = $ReviewerAccounts -Join ";" Send-WorkflowLog -LogData $LoggingData @@ -1304,8 +1309,6 @@ jobs: } - - } Else { Write-Host "Target branch $TargetBranch is an excluded branch. Not adding author assignments." @@ -1334,6 +1337,4 @@ jobs: Write-Host "Event action not ready_for_review, opened, reopened, or synchronize." - - } # PR event and action check From a539d4e83846df5614d0eba55e8f315c38fedb2a Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:06:32 -0700 Subject: [PATCH 86/92] Fix exclusion list --- .github/workflows/Shared-AutoLabelAssign.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index e430be8370b..8e9a2b74a75 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -56,7 +56,7 @@ jobs: "sc-dstrome", "garycentric", "garymoore-sc", - "serdarsoysal" + "serdarsoysal", "cx-ktsuji", "dawntanner", "pamgreen-msft", @@ -83,7 +83,7 @@ jobs: $WorkflowRunAttempt = $env:GITHUB_RUN_ATTEMPT $GitHubData = $env:PayloadJson | ConvertFrom-Json -Depth 50 $AccessToken = $env:AccessToken - $GlobalExcludedReviewerList = $env:GlobalExcludedReviewerList + $GlobalExcludedReviewerList = $env:GlobalExcludedReviewerList | ConvertFrom-Json $GitHubApiUrl = "https://api.github.com/repos/MicrosoftDocs/" $GitRequestEvent = $GitHubData.event_name From edf7c80f0550af80800f71d1b2cf7bbbca1217ef Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Wed, 12 Nov 2025 16:39:50 -0800 Subject: [PATCH 87/92] Add support for docfx author, ms.service, ms.subservice --- .github/workflows/Shared-AutoLabelAssign.yml | 258 +++++++++++++++++-- 1 file changed, 242 insertions(+), 16 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 8e9a2b74a75..6609132be43 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -39,7 +39,10 @@ jobs: if: github.repository_owner == 'MicrosoftDocs' && contains(github.event.repository.topics, 'build') runs-on: ubuntu-latest steps: - + - name: Check out repo + id: checkout-repo + uses: actions/checkout@v4 + - name: Create App Token id: app-token uses: actions/create-github-app-token@v1 @@ -76,7 +79,7 @@ jobs: run: | # Get runspace info - $RepoRoot = $env:RUNNER_WORKSPACE + $RepoRoot = $env:GITHUB_WORKSPACE $RepoName = $env:GITHUB_REPOSITORY $WorkflowName = $env:GITHUB_WORKFLOW -replace '[\\/:*?"<>|\s]', '_' $WorkflowRunId = $env:GITHUB_RUN_ID @@ -175,6 +178,129 @@ jobs: Write-Host "Repository URL: $RepoUrl" + ##################### + ##################### + # Convert-DocFxMetadataPatternToRegex + + Function Convert-DocFxMetadataPatternToRegex { + Param ( + [string]$Pattern + ) + + $Clean = ($Pattern -replace '\\', '/') -replace '^\./', '' + $Regex = [Regex]::Escape($Clean) -replace '/\\\*\\\*/', '/(?:[^/]+/)*' ` + -replace '/\\\*\\\*$', '/.*' ` + -replace '\\\*\\\*', '.*' ` + -replace '\\\*', '[^/]*' ` + -replace '\\\?', '[^/]' + + # Both absolute (starts with /) and relative patterns should anchor to repo root + Return $Clean.StartsWith('/') ? "^$($Regex.TrimStart('/'))$" : "^$Regex$" + + } + + ##################### + ##################### + # Get-DocFxFileMetadataFromPattern + + Function Get-DocFxFileMetadataFromPattern { + + [CmdletBinding()] + Param ( + [object]$PatternTable, + + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + If ($PatternTable -ne $Null) { + + If ($PatternTable -isnot [hashtable]) { + + $Ordered = [ordered]@{} + $PatternTable.PSObject.Properties | ForEach-Object { $Ordered[$_.Name] = $_.Value } + $PatternTable = $Ordered + + } + + $NormPath = ($FilePath -replace '\\', '/') -replace '^\./', '' + + Foreach ($Pattern in $PatternTable.Keys) { + + If ($NormPath -match (Convert-DocFxMetadataPatternToRegex -Pattern $Pattern)) { + + Return $PatternTable[$Pattern] + + } + + } + + } Else { + + Write-Host "No data for specified attribute found in DocFx fileMetadata config." + + } + + Return $null + + } + + ##################### + ##################### + # Get-DocFxConfig + + Function Get-DocFxConfig { + + [CmdletBinding()] + Param ( + [Parameter(Mandatory = $true)] + [string]$FilePath + ) + + If (-not $RepoRoot) { + + Throw 'Repository root could not be determined.' + + } + + $FullPath = (Resolve-Path -LiteralPath $FilePath -ErrorAction Stop).Path + $RepoRoot = (Resolve-Path -LiteralPath $RepoRoot -ErrorAction Stop).Path.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + + # Confirm the file is inside the repo + If (-not $FullPath.StartsWith($RepoRoot, [StringComparison]::OrdinalIgnoreCase)) { + + Throw 'FilePath is not located underneath the repository root.' + + } + + # Walk up the tree + $CurrentDir = Split-Path -Path $FullPath -Parent + + While ($CurrentDir) { + + $Candidate = Join-Path -Path $CurrentDir -ChildPath 'docfx.json' + + If (Test-Path -LiteralPath $Candidate -PathType Leaf) { + + Write-Host "Getting DocFx config from $Candidate" + + Return (Get-Content -LiteralPath $Candidate -Raw) | ConvertFrom-Json -AsHashtable + } + + If ($CurrentDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) -eq $RepoRoot) { + + Break + + } + + # Get the parent of the current folder + $CurrentDir = Split-Path -Path $CurrentDir -Parent + + } + + Return $null + } + ##################### ##################### # Get-FileMetadata @@ -209,32 +335,130 @@ jobs: $FileContentsBase64 = Invoke-RestMethod -Method GET -Uri $File.contents_url -Headers $GitHubHeaders $FileContents = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($FileContentsBase64.content)) - # Check to see if the file contents contains a string that matches the $AuthorRegex regex pattern. If yes, add value to $Author, if not assign $Null. + # Retrieve DocFx configuration that applies to current file. Retrieving for each file is inefficient but most PRs only have a few files in them. + # Pre-processing to be more efficient could actually use more cycles. + $DocFxConfig = Get-DocFxConfig -FilePath $FileName + + # Check to see if the file contents contains a string that matches the $AuthorRegex regex pattern. If yes, add value to $Author, check + # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, return null. If ($FileContents -match $AuthorRegex) { + $Author = $Matches[2] $MetadataFound = $True + Write-Host "Found author $Author." + } Else { - $Author = $Null + + Write-Host "Author not found in article. Checking DocFx fileMetadata for $FileName." + + $Author = Get-DocFxFileMetadataFromPattern -PatternTable $DocFxConfig.build.fileMetadata.author -FilePath $FileName + + If ($Author -ne $Null) { + + Write-Host "Author $Author found in DocFx fileMetadata." + $MetadataFound = $True + + } Else { + + $Author = $DocFxConfig.build.globalMetadata.author + + If ($Author -ne $Null) { + + Write-Host "Author $Author found in DocFx globalMetadata." + $MetadataFound = $True + + } Else { + + Write-Host "Author not found in DocFx globalMetadata. Returning null." + $Author = $Null + + } + + } + } - # Check to see if file contents contains a string that matches the $ServiceRegex regex pattern. If yes, add value to $Service. Then check SubService regex pattern. - # If value isn't matched, assign $Null and don't check SubService. - If ($FileContents -match $ServiceRegex) { + # Check to see if file contents contains a string that matches the $ServiceRegex regex pattern. If yes, add value to $Service, check + # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, return null. + + If ($FileContents -match $ServiceRegex) { + $Service = $Matches[2] $MetadataFound = $True + Write-Host "Found service $Service." - If ($FileContents -match $SubServiceRegex) - { - $SubService = $Matches[2] - $MetadataFound = $True - Write-Host "Found sub service $SubService." - } Else { - $SubService = $Null - } + } Else { - $Service = $Null + + Write-Host "Service not found in article. Checking DocFx fileMetadata for $FileName." + + $Service = Get-DocFxFileMetadataFromPattern -PatternTable $DocFxConfig.build.fileMetadata.'ms.service' -FilePath $FileName + + If ($Service -ne $Null) { + + Write-Host "Service $Service found in DocFx fileMetadata." + $MetadataFound = $True + + + } Else { + + $Service = $DocFxConfig.build.globalMetadata.'ms.service' + + If ($Service -ne $Null) { + + Write-Host "Service $Service found in DocFx globalMetadata." + $MetadataFound = $True + + } Else { + + Write-Host "Service not found in DocFx globalMetadata. Returning null." + $Service = $Null + + } + + } + } + + # Check to see if file contents contains a string that matches the $SubServiceRegex regex pattern. If yes, add value to $SubService, check + # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, return null. + If ($FileContents -match $SubServiceRegex) { + + $SubService = $Matches[2] + $MetadataFound = $True + + Write-Host "Found sub service $SubService." + + } Else { + + Write-Host "SubService not found in article. Checking DocFx fileMetadata for $FileName." + + $SubService = Get-DocFxFileMetadataFromPattern -PatternTable $DocFxConfig.build.fileMetadata.'ms.subservice' -FilePath $FileName + + If ($SubService -ne $Null) { + + Write-Host "SubService $SubService found in DocFx fileMetadata." + + } Else { + + $SubService = $DocFxConfig.build.globalMetadata.'ms.subservice' + + If ($SubService -ne $Null) { + + Write-Host "SubService $SubService found in DocFx globalMetadata." + + } Else { + + Write-Host "SubService not found in DocFx globalMetadata. Returning null." + $SubService = $Null + + } + + } + + } + # Check to see if file contents contains a string that matches the $ProdRegex regex pattern. If yes, add value to $Product. Then check TechnologyRegex regex pattern. # If value isn't matched, assign $Null and don't check Technology. If ($FileContents -match $ProdRegex) { @@ -1060,6 +1284,7 @@ jobs: } + ##################### ##################### # Main @@ -1338,3 +1563,4 @@ jobs: Write-Host "Event action not ready_for_review, opened, reopened, or synchronize." } # PR event and action check + From b5ea40bdf04f6d5a82c149f397ed1cadd5ef3428 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 13 Nov 2025 15:35:50 -0800 Subject: [PATCH 88/92] Update actions/checkout to target the head repo/branch --- .github/workflows/Shared-AutoLabelAssign.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 6609132be43..5e5a6b57eb7 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -42,6 +42,10 @@ jobs: - name: Check out repo id: checkout-repo uses: actions/checkout@v4 + with: + repository: ${{ fromJson(inputs.PayloadJson).event.pull_request.head.repo.full_name }} + ref: ${{ fromJson(inputs.PayloadJson).event.pull_request.head.ref }} + fetch-depth: 0 - name: Create App Token id: app-token From 5cbe61225367d4e49c44efbefd949827590d36dd Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 13 Nov 2025 16:30:34 -0800 Subject: [PATCH 89/92] Fix checkout target --- .github/workflows/Shared-AutoLabelAssign.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 5e5a6b57eb7..1fdfd17abd9 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -43,9 +43,7 @@ jobs: id: checkout-repo uses: actions/checkout@v4 with: - repository: ${{ fromJson(inputs.PayloadJson).event.pull_request.head.repo.full_name }} - ref: ${{ fromJson(inputs.PayloadJson).event.pull_request.head.ref }} - fetch-depth: 0 + ref: ${{ fromJson(inputs.PayloadJson).event.pull_request.head.sha }} - name: Create App Token id: app-token From e91b03a1226263272ee9ce7d57d87e63509fd507 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:00:41 -0800 Subject: [PATCH 90/92] Various fixes --- .github/workflows/Shared-AutoLabelAssign.yml | 114 +++++++++++++------ 1 file changed, 78 insertions(+), 36 deletions(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 1fdfd17abd9..9c737a1d494 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -127,7 +127,7 @@ jobs: $AppGitHubAccessHeaders.Add("User-Agent", "OfficeDocs") # Regex for string matches - $AuthorRegex = "(?m)^(author:\s{0,3})([\w|\-]{1,39})" + $AuthorRegex = "(?m)^(author:\s{0,3})([\w-]{1,39})(?=\s*(?:#|$))" $ServiceRegex = "(ms\.service:\s{0,3})([\w|\-|\.]{1,60})" $SubServiceRegex = "(ms\.subservice:\s{0,3})([\w|\-|\.]{1,60})" $TechnologyRegex = "(ms\.technology:\s{0,3})([\w|\-|\.]{1,60})" @@ -265,41 +265,63 @@ jobs: } - $FullPath = (Resolve-Path -LiteralPath $FilePath -ErrorAction Stop).Path - $RepoRoot = (Resolve-Path -LiteralPath $RepoRoot -ErrorAction Stop).Path.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) + Try { - # Confirm the file is inside the repo - If (-not $FullPath.StartsWith($RepoRoot, [StringComparison]::OrdinalIgnoreCase)) { + $FullPath = (Resolve-Path -LiteralPath $FilePath -ErrorAction Stop).Path + $RepoRoot = (Resolve-Path -LiteralPath $RepoRoot -ErrorAction Stop).Path.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) - Throw 'FilePath is not located underneath the repository root.' + $FileResolved = $True + + } Catch { + + Write-Host "Unable to resolve $FilePath. File may have been deleted." + $FileResolved = $False } - # Walk up the tree - $CurrentDir = Split-Path -Path $FullPath -Parent + If ($FileResolved) { - While ($CurrentDir) { + # Confirm the file is inside the repo + If (-not $FullPath.StartsWith($RepoRoot, [StringComparison]::OrdinalIgnoreCase)) { - $Candidate = Join-Path -Path $CurrentDir -ChildPath 'docfx.json' + Throw 'FilePath is not located underneath the repository root.' - If (Test-Path -LiteralPath $Candidate -PathType Leaf) { - - Write-Host "Getting DocFx config from $Candidate" - - Return (Get-Content -LiteralPath $Candidate -Raw) | ConvertFrom-Json -AsHashtable } - If ($CurrentDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) -eq $RepoRoot) { + # Walk up the tree + $CurrentDir = Split-Path -Path $FullPath -Parent + + While ($CurrentDir) { + + $Candidate = Join-Path -Path $CurrentDir -ChildPath 'docfx.json' + + If (Test-Path -LiteralPath $Candidate -PathType Leaf) { + + Write-Host "Getting DocFx config from $Candidate" + + Return (Get-Content -LiteralPath $Candidate -Raw) | ConvertFrom-Json -AsHashtable + } - Break + If ($CurrentDir.TrimEnd([IO.Path]::DirectorySeparatorChar, [IO.Path]::AltDirectorySeparatorChar) -eq $RepoRoot) { + + Break + + } + + # Get the parent of the current folder + $CurrentDir = Split-Path -Path $CurrentDir -Parent } - # Get the parent of the current folder - $CurrentDir = Split-Path -Path $CurrentDir -Parent + } + If ($FileResolved) { + + Write-Host "ERROR: File path $FullPath and repo root $RepoRoot were resolved but no docfx.json file was found." + } + # Only reached if $FilePath isn't resolved or if While loop falls through without finding docfx.json. Return $null } @@ -341,16 +363,17 @@ jobs: # Pre-processing to be more efficient could actually use more cycles. $DocFxConfig = Get-DocFxConfig -FilePath $FileName - # Check to see if the file contents contains a string that matches the $AuthorRegex regex pattern. If yes, add value to $Author, check - # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, return null. + # Check to see if the file contents contains a string that matches the $AuthorRegex regex pattern. If yes, add value to $Author. If not, check + # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, or if DocFx couldn't be + # found, return null. If ($FileContents -match $AuthorRegex) { $Author = $Matches[2] $MetadataFound = $True - Write-Host "Found author $Author." + Write-Host "Found author $Author in file." - } Else { + } ElseIf ($DocFxConfig -ne $Null) { Write-Host "Author not found in article. Checking DocFx fileMetadata for $FileName." @@ -379,19 +402,25 @@ jobs: } + } Else { + + Write-Host "WARNING: Unable to retrieve author information from article file and unable to retrieve from docfx.json because it couldn't be found. This is expected if file was deleted." + $Author = $Null + } - # Check to see if file contents contains a string that matches the $ServiceRegex regex pattern. If yes, add value to $Service, check - # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, return null. + # Check to see if file contents contains a string that matches the $ServiceRegex regex pattern. If yes, add value to $Service. If not, check + # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, or if DocFx couldn't be + # found, return null. If ($FileContents -match $ServiceRegex) { $Service = $Matches[2] $MetadataFound = $True - Write-Host "Found service $Service." + Write-Host "Found service $Service in file." - } Else { + } ElseIf ($DocFxConfig -ne $Null) { Write-Host "Service not found in article. Checking DocFx fileMetadata for $FileName." @@ -421,18 +450,24 @@ jobs: } + } Else { + + Write-Host "WARNING: Unable to retrieve service information from article file and unable to retrieve from docfx.json because it couldn't be found. This is expected if file was deleted." + $Service = $Null + } - # Check to see if file contents contains a string that matches the $SubServiceRegex regex pattern. If yes, add value to $SubService, check - # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, return null. + # Check to see if file contents contains a string that matches the $SubServiceRegex regex pattern. If yes, add value to $SubService. If not, check + # fileMetadata and globalMetadata in that order in DocFx. If there's a match in either of those, return value. If not, or if DocFx couldn't be + # found, return null. If ($FileContents -match $SubServiceRegex) { $SubService = $Matches[2] $MetadataFound = $True - Write-Host "Found sub service $SubService." + Write-Host "Found sub service $SubService in file." - } Else { + } ElseIf ($DocFxConfig -ne $Null) { Write-Host "SubService not found in article. Checking DocFx fileMetadata for $FileName." @@ -459,7 +494,12 @@ jobs: } - } + } Else { + + Write-Host "WARNING: Unable to retrieve subservice information from article file and unable to retrieve from docfx.json because it couldn't be found. This is expected if file was deleted." + $SubService = $Null + + } # Check to see if file contents contains a string that matches the $ProdRegex regex pattern. If yes, add value to $Product. Then check TechnologyRegex regex pattern. # If value isn't matched, assign $Null and don't check Technology. @@ -1185,7 +1225,7 @@ jobs: } - Write-Host "Reviewers found: $($ReviewerList | Sort-Object -Unique)" + Write-Host "Reviewers found in teams: $($ReviewerList | Sort-Object -Unique)" Return $ReviewerList | Sort-Object -Unique @@ -1292,6 +1332,7 @@ jobs: # Main Write-Host "Repo: $GitHubRepoName" + Write-Host "PR number: $PrNumber" Write-Host "Sender: $GitHubSender" Write-Host "Request event: $GitRequestEvent" Write-Host "GitHub action: $GitHubAction" @@ -1299,6 +1340,7 @@ jobs: Write-Host "Default branch: $DefaultBranch" Write-Host "Target branch: $TargetBranch" Write-Host "PR files URL: $PrFileListUrl" + Write-Host "PR HTML URL: $PrHtmlUrl" Write-Host "Auto assign users on $GitHubRepoName`: $AutoAssignUsers" Write-Host "Auto label on $GitHubRepoName`: $AutoLabel" @@ -1394,7 +1436,7 @@ jobs: # 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 - + $UserPermission = "read" # Only add reviewers if the submitter can't sign off on their own PR. If (($UserPermission -eq "read") -or ($UserPermission -eq "") -or ($UserPermission -eq $Null)) { @@ -1435,10 +1477,10 @@ jobs: $ReviewerAccounts = Get-TeamMembership -ReviewerTeams $ReviewerTeamArray - $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ReviewerAccounts - If ($ReviewerAccounts.Length -gt 0) { + $AtMentionedGitHubAccounts = Add-AtPrefix -GitHubReviewers $ReviewerAccounts + $MissingReviewers = Compare-PRIndividualReviewers -GitHubReviewers $ReviewerAccounts If ($MissingReviewers) { From 3060b4058ed9c60a426e2641c95ef00027fa21d3 Mon Sep 17 00:00:00 2001 From: David Strome <21028455+dstrome@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:05:42 -0800 Subject: [PATCH 91/92] remove test statement --- .github/workflows/Shared-AutoLabelAssign.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/Shared-AutoLabelAssign.yml b/.github/workflows/Shared-AutoLabelAssign.yml index 9c737a1d494..e8b6aa20770 100644 --- a/.github/workflows/Shared-AutoLabelAssign.yml +++ b/.github/workflows/Shared-AutoLabelAssign.yml @@ -1436,7 +1436,7 @@ jobs: # 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 - $UserPermission = "read" + # Only add reviewers if the submitter can't sign off on their own PR. If (($UserPermission -eq "read") -or ($UserPermission -eq "") -or ($UserPermission -eq $Null)) { @@ -1608,3 +1608,4 @@ jobs: } # PR event and action check + From 3c35cdf7bf1162ff70bd9fc6198ca983d7c73781 Mon Sep 17 00:00:00 2001 From: Gary Moore <5432776+garycentric@users.noreply.github.com> Date: Mon, 26 Jan 2026 14:15:45 -0800 Subject: [PATCH 92/92] Replace aka.ms/AskAnAdmin with aka.ms/tcphelp --- .github/workflows/Shared-AutoPublishV2.yml | 8 ++++---- .github/workflows/Shared-TierManagement.yml | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/Shared-AutoPublishV2.yml b/.github/workflows/Shared-AutoPublishV2.yml index 0b11f611725..7de678ed531 100644 --- a/.github/workflows/Shared-AutoPublishV2.yml +++ b/.github/workflows/Shared-AutoPublishV2.yml @@ -95,10 +95,10 @@ jobs: # PR variables $PrTitle = "[AutoPublish] $DefaultBranch to $TargetBranch" - $AutoMergeDisabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore merging this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, post a message to https://aka.ms/askanadmin." - $AutoMergeDisabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore you merge this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **A contributor with write access to this repo will need to merge this PR for changes in it to go live.`n`nIf you have questions, post a message to https://aka.ms/askanadmin." - $AutoMergeEnabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, post a message to https://aka.ms/askanadmin." - $AutoMergeEnabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **If a manual merge is required, a contributor with write access to this repo will need to merge this PR for changes in it to go live.**`n`nIf you have questions, post a message to https://aka.ms/askanadmin." + $AutoMergeDisabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore merging this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, post a message to https://aka.ms/tcphelp." + $AutoMergeDisabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is disabled.** This PR must be merged manually.`n`nBefore you merge this PR, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **A contributor with write access to this repo will need to merge this PR for changes in it to go live.`n`nIf you have questions, post a message to https://aka.ms/tcphelp." + $AutoMergeEnabledPrDescriptionPubOps = "@MicrosoftDocs/marveldocs-pubops`n`nThis is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`nIf you have questions, post a message to https://aka.ms/tcphelp." + $AutoMergeEnabledPrDescriptionNonPubOps = "This is an automated pull request to publish changes from the $DefaultBranch branch to the $TargetBranch branch. Merging this PR will publish the changes to the live learn.microsoft.com site.`n`n**Auto-merge is enabled.** The repo will attempt to automatically merge this PR when all required checks pass. If the PR is automatically merged, no further action is required to publish the contents of this PR.`n`nIn the event the repo can't automatically merge this PR, the **Manual merge required** label will be added to the PR. If this happens, complete the following checks before merging this PR:`n`n- If there are more than $MaxAllowedChangedFiles files, the repo will not automatically merge the PR. Review the changes for any obvious mistakes such as mass-deleted files. If the changes appear normal (bulk changes, general updates to many files, etc), merge the PR. If you're not sure, do not merge the PR and investigate further.`n- If there are warnings or failing checks, resolve them before merging the PR.`n- View the changes on https://review.learn.microsoft.com to confirm the changes look correct on the site.`n`nAfter you've completed these steps, manually merge the PR.`n`n**Note**: This is repo isn't managed by PubOps. **If a manual merge is required, a contributor with write access to this repo will need to merge this PR for changes in it to go live.**`n`nIf you have questions, post a message to https://aka.ms/tcphelp." # Label variables $AutoPublishLabelColor = "5319E7" diff --git a/.github/workflows/Shared-TierManagement.yml b/.github/workflows/Shared-TierManagement.yml index eaf42f57a2e..121a2266cbb 100644 --- a/.github/workflows/Shared-TierManagement.yml +++ b/.github/workflows/Shared-TierManagement.yml @@ -72,9 +72,9 @@ jobs: $SignOffLabelColor = "46ce1c" $SignOffLabelDescription = "The pull request is ready to be reviewed and merged by PubOps." $SignOffLabel = "Sign off" - $InvalidTargetBranchString = "Hi @{1}

The target (base) branch of this PR, **{2}**, doesn't support the **{0}** command. The **{0}** command can only be used to merge pull requests into the default branch **{3}** or into branches beginning with **release-**.

Please retarget your PR to either the default branch **{3}** or to a release branch.

If you have questions, please post a message to https://aka.ms/askanadmin." # Variable substitution happens in script. - $Tier3LabelMissingString = "hi @{1},

The **{0}** command can't be used on this PR because the none of the files included in it are classified as Tier3/Selfserve. At least one file needs to be classified as Tier3/Selfserve. Please work with the owner of the article(s) (found in the **ms.author** metadata field in the article) to review and merge this PR.

If you're the owner of the article(s) and your alias is specified in **ms.author**, work with the Magic content team that owns the content set to classify the article(s) as Tier3/Selfserve.

If you have questions, please post a message to https://aka.ms/askanadmin." -f $SignOffString, $CommentUser - $PrSubmittedToReviewString = "Hi @{0},

This PR has been sent to our publishing team for review to ensure content quality and completeness. A member of our publishing team will contact you within {1} business hours.

If you have questions, please post a message to https://aka.ms/askanadmin." -f $CommentUser, $ReviewResponseHours + $InvalidTargetBranchString = "Hi @{1}

The target (base) branch of this PR, **{2}**, doesn't support the **{0}** command. The **{0}** command can only be used to merge pull requests into the default branch **{3}** or into branches beginning with **release-**.

Please retarget your PR to either the default branch **{3}** or to a release branch.

If you have questions, please post a message to https://aka.ms/tcphelp." # Variable substitution happens in script. + $Tier3LabelMissingString = "hi @{1},

The **{0}** command can't be used on this PR because the none of the files included in it are classified as Tier3/Selfserve. At least one file needs to be classified as Tier3/Selfserve. Please work with the owner of the article(s) (found in the **ms.author** metadata field in the article) to review and merge this PR.

If you're the owner of the article(s) and your alias is specified in **ms.author**, work with the Magic content team that owns the content set to classify the article(s) as Tier3/Selfserve.

If you have questions, please post a message to https://aka.ms/tcphelp." -f $SignOffString, $CommentUser + $PrSubmittedToReviewString = "Hi @{0},

This PR has been sent to our publishing team for review to ensure content quality and completeness. A member of our publishing team will contact you within {1} business hours.

If you have questions, please post a message to https://aka.ms/tcphelp." -f $CommentUser, $ReviewResponseHours $SignOffRegex = "(?m)^\s*$SignOffString\s*$" @@ -472,7 +472,7 @@ jobs: $CommentsUrl = $GitHubData.event.pull_request.comments_url $UserPermissionUrl = $GitHubData.event.repository.collaborators_url.Replace("{/collaborator}", "/$GitHubSender/permission" ) - $DraftMessage = "

Pull request set to Draft

Hi @{0}.

To avoid accidentally publishing the changes in this pull request prematurely, its state has been changed to Draft.

When you're ready for the changes in this pull request to be published live, select the Ready for review button at the bottom of the page.

If you have questions, please post a message to https://aka.ms/askanadmin." + $DraftMessage = "

Pull request set to Draft

Hi @{0}.

To avoid accidentally publishing the changes in this pull request prematurely, its state has been changed to Draft.

When you're ready for the changes in this pull request to be published live, select the Ready for review button at the bottom of the page.

If you have questions, please post a message to https://aka.ms/tcphelp." # Create github HTTP authentication header $GitHubHeaders = @{}