From efd3f02819307bec66dc1034ac0dc82902a628af Mon Sep 17 00:00:00 2001 From: Daniel Jurek Date: Fri, 21 Aug 2026 14:50:11 -0700 Subject: [PATCH] Allow specific PR authors to skip CODEOWNERS validation --- .../templates/steps/verify-codeowners.yml | 21 +- .../scripts/Set-VerifyCodeownersSkip.ps1 | 188 +++++++++++++++--- 2 files changed, 175 insertions(+), 34 deletions(-) diff --git a/eng/common/pipelines/templates/steps/verify-codeowners.yml b/eng/common/pipelines/templates/steps/verify-codeowners.yml index 01eba1029b77..c8bd1a4ea79c 100644 --- a/eng/common/pipelines/templates/steps/verify-codeowners.yml +++ b/eng/common/pipelines/templates/steps/verify-codeowners.yml @@ -25,12 +25,19 @@ parameters: - data - functions - datamovement - # Comma separated list of emails allowed to skip codeowners validation. User - # must queue the pipeline manually with the 'Skip.VerifyCodeowners' variable - # set to 'true'. + # Comma separated list of Microsoft emails allowed to skip codeowners + # validation on manually queued builds. Build.RequestedForEmail is empty on + # pull request builds, so use AllowSkipAliases for those. - name: AllowSkipEmails type: string default: '' + # Comma separated list of GitHub aliases (logins) allowed to skip codeowners + # validation on pull request builds. Matched against the GitHub login of the + # pull request author. Pull requests opened by a coding agent report the agent + # as the author, so they are not exempt. + - name: AllowSkipAliases + type: string + default: '' steps: - ${{ if and(eq(variables['Build.Reason'], 'PullRequest'), eq(parameters.EnablePrValidation, true)) }}: @@ -41,9 +48,10 @@ steps: pwsh: true filePath: $(Build.SourcesDirectory)/eng/common/scripts/Set-VerifyCodeownersSkip.ps1 arguments: >- - -RequestedForEmail '$(Build.RequestedForEmail)' - -SkipVerifyCodeowners '$(Skip.VerifyCodeowners)' - -AdditionalSkipEmails '${{ parameters.AllowSkipEmails }}' + -BuildReason '$(Build.Reason)' + -Repo '${{ parameters.Repo }}' + -PullRequestNumber '$(System.PullRequest.PullRequestNumber)' + -AdditionalSkipAliases '${{ parameters.AllowSkipAliases }}' workingDirectory: $(Build.SourcesDirectory) - template: /eng/common/pipelines/templates/steps/install-azsdk-cli.yml @@ -85,6 +93,7 @@ steps: arguments: >- -RequestedForEmail '$(Build.RequestedForEmail)' -SkipVerifyCodeowners '$(Skip.VerifyCodeowners)' + -BuildReason '$(Build.Reason)' -AdditionalSkipEmails '${{ parameters.AllowSkipEmails }}' workingDirectory: $(Build.SourcesDirectory) diff --git a/eng/common/scripts/Set-VerifyCodeownersSkip.ps1 b/eng/common/scripts/Set-VerifyCodeownersSkip.ps1 index c339fc2efe66..6cb0491e4e19 100644 --- a/eng/common/scripts/Set-VerifyCodeownersSkip.ps1 +++ b/eng/common/scripts/Set-VerifyCodeownersSkip.ps1 @@ -4,24 +4,49 @@ Evaluates whether codeowners verification should be skipped and sets a pipeline variable with the result. .DESCRIPTION -Codeowners verification can be skipped when the 'Skip.VerifyCodeowners' variable -is set to 'true' and the person who requested the build is a member of an allowed -set of emails. The allowed set is the union of a default list and any additional -emails passed via the AdditionalSkipEmails parameter. - -The result is written to an Azure DevOps pipeline variable (default name -'ShouldSkipVerifyCodeowners') so that later steps can gate on a single variable. +Verification is skipped only for an allow listed person. Who that is gets +established differently depending on how the build started: + - Pull request builds: the GitHub login of the pull request author, resolved + from the GitHub API. 'Build.RequestedForEmail' is empty here, because the + build is queued by the 'GitHub' app service principal rather than by a user. + - Manually queued builds: 'Build.RequestedForEmail', and only when the + 'Skip.VerifyCodeowners' queue time variable is also 'true'. That variable + cannot be supplied on a pull request build, so it is ignored there. + +Matching on the pull request author, rather than on whoever pushed most +recently, keeps the decision a property of the pull request. Agent opened pull +requests report the agent (for example 'Copilot') as the author, so they are +never exempt. + +If the author cannot be resolved, verification runs. A GitHub outage or an +exhausted rate limit never blocks a build, and never silently grants a skip. .PARAMETER RequestedForEmail The email of the person who requested the build (e.g. Build.RequestedForEmail). +Only populated for manually queued builds. .PARAMETER SkipVerifyCodeowners -The value of the 'Skip.VerifyCodeowners' pipeline variable. Verification is only -eligible to be skipped when this is 'true'. +The 'Skip.VerifyCodeowners' pipeline variable. Ignored on pull request builds, +where queue time variables cannot be supplied. + +.PARAMETER BuildReason +The 'Build.Reason' pipeline variable. + +.PARAMETER Repo +The GitHub repository in '/' form (e.g. Build.Repository.Name). +Used to resolve the pull request author. + +.PARAMETER PullRequestNumber +The pull request number (e.g. System.PullRequest.PullRequestNumber). Used to +resolve the pull request author. .PARAMETER AdditionalSkipEmails -Additional emails allowed to skip verification, in addition to the -default set. Accepts a comma-, semicolon-, or whitespace-separated list. +Emails allowed to skip verification, in addition to the default set. Accepts a +comma-, semicolon-, or whitespace-separated list. + +.PARAMETER AdditionalSkipAliases +GitHub aliases (logins) allowed to skip verification, in addition to the default +set. Accepts a comma-, semicolon-, or whitespace-separated list. .PARAMETER OutputVariableName The name of the pipeline variable to set with the boolean result. @@ -30,19 +55,19 @@ The name of the pipeline variable to set with the boolean result. param ( [string] $RequestedForEmail = '', [string] $SkipVerifyCodeowners = $env:SKIP_VERIFYCODEOWNERS, + [string] $BuildReason = $env:BUILD_REASON, + [string] $Repo = $env:BUILD_REPOSITORY_NAME, + [string] $PullRequestNumber = $env:SYSTEM_PULLREQUEST_PULLREQUESTNUMBER, [string] $AdditionalSkipEmails = '', + [string] $AdditionalSkipAliases = '', [string] $OutputVariableName = 'ShouldSkipVerifyCodeowners' ) Set-StrictMode -Version 4 $ErrorActionPreference = 'Stop' -if ($SkipVerifyCodeowners -ne 'true') { - Write-Host "Skip.VerifyCodeowners is not set to 'true' (value: '$SkipVerifyCodeowners'). Verification will run." - Write-Host "##vso[task.setvariable variable=$OutputVariableName]false" - return -} - +# Microsoft emails allowed to skip verification. Only usable on manually queued +# builds, where Build.RequestedForEmail is populated. $defaultSkipEmails = @( 'bebroder@microsoft.com', 'mharder@microsoft.com', @@ -51,23 +76,130 @@ $defaultSkipEmails = @( 'raychen@microsoft.com' ) -$extraSkipEmails = @($AdditionalSkipEmails -split '[,;\s]+' | Where-Object { $_ }) +# GitHub aliases (logins) allowed to skip verification. Used on pull request +# builds, where the GitHub login of the user who triggered the build is the only +# identity available. These are the same people as $defaultSkipEmails above; keep +# the two lists in sync. +$defaultSkipAliases = @( + 'benbp', + 'mikeharder', + 'danieljurek', + 'chidozieononiwu', + 'raych1' +) + +function Get-NormalizedSet { + param ( + [string[]] $Default, + [string] $Additional + ) + + $extra = @($Additional -split '[,;\s]+' | Where-Object { $_ }) + + return @($Default + $extra) | + ForEach-Object { $_.Trim().ToLowerInvariant() } | + Where-Object { $_ } | + Select-Object -Unique +} + +# Returns the GitHub login of the pull request author, or an empty string if it +# cannot be determined. +function Get-PullRequestAuthor { + param ( + [string] $Repo, + [string] $PullRequestNumber + ) + + $uri = "https://api.github.com/repos/$Repo/pulls/$PullRequestNumber" + + # The request is unauthenticated and subject to GitHub's anonymous per IP + # rate limit. Retries are kept low, because every attempt spends budget. + try { + $pullRequest = Invoke-RestMethod -Uri $uri -MaximumRetryCount 3 -RetryIntervalSec 2 + $author = "$($pullRequest.user.login)" + + if ($pullRequest.user.type -eq 'Bot') { + Write-Host "Pull request $PullRequestNumber was opened by the '$author' app rather than by a person, so it is attributed to the app and not to whoever dispatched it. Verification will run." + } + + return $author + } catch { + Write-Host "Failed to resolve the author of '$uri': $_" + return '' + } +} + +function Set-Result { + param ( + [bool] $ShouldSkip + ) + + $value = $ShouldSkip.ToString().ToLowerInvariant() + Write-Host "Setting $OutputVariableName to $value" + Write-Host "##vso[task.setvariable variable=$OutputVariableName]$value" +} + +$isPullRequest = $BuildReason -eq 'PullRequest' + +Write-Host "Build.Reason: $BuildReason" + +# Skip.VerifyCodeowners is a queue-time variable, which cannot be supplied on a +# pull request build, so it only applies outside of pull requests. There, +# skipping must be explicitly requested. +if (!$isPullRequest) { + Write-Host "Skip.VerifyCodeowners: $SkipVerifyCodeowners" + + if ($SkipVerifyCodeowners -ne 'true') { + Write-Host "This is not a pull request build and Skip.VerifyCodeowners is not set to 'true' (value: '$SkipVerifyCodeowners'). Verification will run." + Set-Result -ShouldSkip $false + return + } +} + +$allowedSkipAliases = Get-NormalizedSet -Default $defaultSkipAliases -Additional $AdditionalSkipAliases + +if ($isPullRequest) { + # Build.RequestedForEmail is empty on GitHub pull request builds, so match on + # the PR author's GitHub login instead. + $pullRequestAuthor = Get-PullRequestAuthor -Repo $Repo -PullRequestNumber $PullRequestNumber + + $normalizedAlias = $pullRequestAuthor.Trim().ToLowerInvariant() -$allowedSkipEmails = @($defaultSkipEmails + $extraSkipEmails) | - ForEach-Object { $_.Trim().ToLowerInvariant() } | - Where-Object { $_ } | - Select-Object -Unique + if (!$normalizedAlias) { + Write-Host "Could not determine the pull request author. Codeowners verification will run." + Set-Result -ShouldSkip $false + return + } + Write-Host "Pull request author: $normalizedAlias" + + if ($allowedSkipAliases -notcontains $normalizedAlias) { + Write-Host "The GitHub alias '$normalizedAlias' is not in the allowed skip list." + Set-Result -ShouldSkip $false + return + } + + Write-Host "The GitHub alias '$normalizedAlias' is allowed to skip verification." + Set-Result -ShouldSkip $true + return +} + +$allowedSkipEmails = Get-NormalizedSet -Default $defaultSkipEmails -Additional $AdditionalSkipEmails $normalizedEmail = $RequestedForEmail.Trim().ToLowerInvariant() -$shouldSkip = $allowedSkipEmails -contains $normalizedEmail +if (!$normalizedEmail) { + Write-Host "Could not determine the email of the person who requested the build. Verification will run." + Set-Result -ShouldSkip $false + return +} -if ($shouldSkip) { - Write-Host "Skipping codeowners verification. Skip.VerifyCodeowners is set and '$normalizedEmail' is an allowed email." -} else { - Write-Host "Skip.VerifyCodeowners is set but '$normalizedEmail' is not an allowed email. Verification will run." +if ($allowedSkipEmails -notcontains $normalizedEmail) { + Write-Host "The email '$normalizedEmail' is not in the allowed skip list." + Set-Result -ShouldSkip $false + return } -Write-Host "##vso[task.setvariable variable=$OutputVariableName]$($shouldSkip.ToString().ToLowerInvariant())" +Write-Host "The email '$normalizedEmail' is allowed to skip verification." +Set-Result -ShouldSkip $true return