diff --git a/dev-infra/pr/merge/task.ts b/dev-infra/pr/merge/task.ts index 5a177847386..7b9a76ead2b 100644 --- a/dev-infra/pr/merge/task.ts +++ b/dev-infra/pr/merge/task.ts @@ -16,9 +16,6 @@ import {isPullRequest, loadAndValidatePullRequest,} from './pull-request'; import {GithubApiMergeStrategy} from './strategies/api-merge'; import {AutosquashMergeStrategy} from './strategies/autosquash-merge'; -/** Github OAuth scopes required for the merge task. */ -const REQUIRED_SCOPES = ['repo']; - /** Describes the status of a pull request merge. */ export const enum MergeStatus { UNKNOWN_GIT_ERROR, @@ -56,8 +53,19 @@ export class PullRequestMergeTask { * @param force Whether non-critical pull request failures should be ignored. */ async merge(prNumber: number, force = false): Promise { - // Assert the authenticated GitClient has access on the required scopes. - const hasOauthScopes = await this.git.hasOauthScopes(...REQUIRED_SCOPES); + // Check whether the given Github token has sufficient permissions for writing + // to the configured repository. If the repository is not private, only the + // reduced `public_repo` OAuth scope is sufficient for performing merges. + const hasOauthScopes = await this.git.hasOauthScopes((scopes, missing) => { + if (!scopes.includes('repo')) { + if (this.config.remote.private) { + missing.push('repo'); + } else if (!scopes.includes('public_repo')) { + missing.push('public_repo'); + } + } + }); + if (hasOauthScopes !== true) { return { status: MergeStatus.GITHUB_ERROR, diff --git a/dev-infra/utils/config.ts b/dev-infra/utils/config.ts index bcf6a82d60f..bd08ee68e44 100644 --- a/dev-infra/utils/config.ts +++ b/dev-infra/utils/config.ts @@ -21,6 +21,8 @@ export interface GitClientConfig { name: string; /** If SSH protocol should be used for git interactions. */ useSsh?: boolean; + /** Whether the specified repository is private. */ + private?: boolean; } /** diff --git a/dev-infra/utils/git/index.ts b/dev-infra/utils/git/index.ts index 3a15b5a0982..88c626dc20a 100644 --- a/dev-infra/utils/git/index.ts +++ b/dev-infra/utils/git/index.ts @@ -21,6 +21,9 @@ type RateLimitResponseWithOAuthScopeHeader = Octokit.Response void; + /** Error for failed Git commands. */ export class GitCommandError extends Error { constructor(client: GitClient, public args: string[]) { @@ -155,14 +158,11 @@ export class GitClient { * Assert the GitClient instance is using a token with permissions for the all of the * provided OAuth scopes. */ - async hasOauthScopes(...requestedScopes: string[]): Promise { - const missingScopes: string[] = []; + async hasOauthScopes(testFn: OAuthScopeTestFunction): Promise { const scopes = await this.getAuthScopesForToken(); - requestedScopes.forEach(scope => { - if (!scopes.includes(scope)) { - missingScopes.push(scope); - } - }); + const missingScopes: string[] = []; + // Test Github OAuth scopes and collect missing ones. + testFn(scopes, missingScopes); // If no missing scopes are found, return true to indicate all OAuth Scopes are available. if (missingScopes.length === 0) { return true;