diff --git a/dev-infra/pr/merge/config.ts b/dev-infra/pr/merge/config.ts index 861b46bbc1..73222436ef 100644 --- a/dev-infra/pr/merge/config.ts +++ b/dev-infra/pr/merge/config.ts @@ -53,6 +53,8 @@ export interface MergeConfig { claSignedLabel: string|RegExp; /** Pattern that matches labels which imply a merge ready pull request. */ mergeReadyLabel: string|RegExp; + /** Label that is applied when special attention from the caretaker is required. */ + caretakerNoteLabel?: string|RegExp; /** Label which can be applied to fixup commit messages in the merge script. */ commitMessageFixupLabel: string|RegExp; /** diff --git a/dev-infra/pr/merge/index.ts b/dev-infra/pr/merge/index.ts index fcd3c16fdd..1b43aaf32b 100644 --- a/dev-infra/pr/merge/index.ts +++ b/dev-infra/pr/merge/index.ts @@ -90,7 +90,7 @@ export async function mergePullRequest( /** * Handles the merge result by printing console messages, exiting the process * based on the result, or by restarting the merge if force mode has been enabled. - * @returns Whether the merge was successful or not. + * @returns Whether the merge completed without errors or not. */ async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) { const {failure, status} = result; @@ -98,7 +98,7 @@ export async function mergePullRequest( switch (status) { case MergeStatus.SUCCESS: - info(green(`Successfully merged the pull request: ${prNumber}`)); + info(green(`Successfully merged the pull request: #${prNumber}`)); return true; case MergeStatus.DIRTY_WORKING_DIR: error( @@ -114,6 +114,9 @@ export async function mergePullRequest( error(red('An error related to interacting with Github has been discovered.')); error(failure!.message); return false; + case MergeStatus.USER_ABORTED: + info(`Merge of pull request has been aborted manually: #${prNumber}`); + return true; case MergeStatus.FAILED: error(yellow(`Could not merge the specified pull request.`)); error(red(failure!.message)); diff --git a/dev-infra/pr/merge/messages.ts b/dev-infra/pr/merge/messages.ts new file mode 100644 index 0000000000..dbcb0aac66 --- /dev/null +++ b/dev-infra/pr/merge/messages.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {red} from '../../utils/console'; + +import {PullRequest} from './pull-request'; + +export function getCaretakerNotePromptMessage(pullRequest: PullRequest): string { + return red('Pull request has a caretaker note applied. Please make sure you read it.') + + `\nQuick link to PR: ${pullRequest.url}`; +} diff --git a/dev-infra/pr/merge/pull-request.ts b/dev-infra/pr/merge/pull-request.ts index f0bb691841..69216bdc8b 100644 --- a/dev-infra/pr/merge/pull-request.ts +++ b/dev-infra/pr/merge/pull-request.ts @@ -17,6 +17,8 @@ import {PullRequestMergeTask} from './task'; /** Interface that describes a pull request. */ export interface PullRequest { + /** URL to the pull request. */ + url: string; /** Number of the pull request. */ prNumber: number; /** Title of the pull request. */ @@ -33,6 +35,8 @@ export interface PullRequest { requiredBaseSha?: string; /** Whether the pull request commit message fixup. */ needsCommitMessageFixup: boolean; + /** Whether the pull request has a caretaker note. */ + hasCaretakerNote: boolean; } /** @@ -77,13 +81,17 @@ export async function loadAndValidatePullRequest( config.requiredBaseCommits && config.requiredBaseCommits[githubTargetBranch]; const needsCommitMessageFixup = !!config.commitMessageFixupLabel && labels.some(name => matchesPattern(name, config.commitMessageFixupLabel)); + const hasCaretakerNote = !!config.caretakerNoteLabel && + labels.some(name => matchesPattern(name, config.caretakerNoteLabel!)); return { + url: prData.html_url, prNumber, labels, requiredBaseSha, githubTargetBranch, needsCommitMessageFixup, + hasCaretakerNote, title: prData.title, targetBranches: getBranchesFromTargetLabel(targetLabel, githubTargetBranch), commitCount: prData.commits, diff --git a/dev-infra/pr/merge/task.ts b/dev-infra/pr/merge/task.ts index 8ea89e67d6..e02815cbc0 100644 --- a/dev-infra/pr/merge/task.ts +++ b/dev-infra/pr/merge/task.ts @@ -6,10 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {promptConfirm} from '../../utils/console'; import {GitClient, GitCommandError} from '../../utils/git'; import {MergeConfigWithRemote} from './config'; import {PullRequestFailure} from './failures'; +import {getCaretakerNotePromptMessage} from './messages'; import {isPullRequest, loadAndValidatePullRequest,} from './pull-request'; import {GithubApiMergeStrategy} from './strategies/api-merge'; import {AutosquashMergeStrategy} from './strategies/autosquash-merge'; @@ -23,6 +25,7 @@ export const enum MergeStatus { DIRTY_WORKING_DIR, SUCCESS, FAILED, + USER_ABORTED, GITHUB_ERROR, } @@ -72,6 +75,14 @@ export class PullRequestMergeTask { return {status: MergeStatus.FAILED, failure: pullRequest}; } + // If the pull request has a caretaker note applied, raise awareness by prompting + // the caretaker. The caretaker can then decide to proceed or abort the merge. + if (pullRequest.hasCaretakerNote && + !await promptConfirm( + getCaretakerNotePromptMessage(pullRequest) + `\nDo you want to proceed merging?`)) { + return {status: MergeStatus.USER_ABORTED}; + } + const strategy = this.config.githubApiMerge ? new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) : new AutosquashMergeStrategy(this.git);