Paul Gschwendtner d4e00ee5a1 feat(dev-infra): move merge script over from components repo (#37184)
Moves the merge script from the components repository over
to the shared dev-infra package. The merge script has been
orginally built for all Angular repositories, but we just
kept it in the components repo temporarily to test it.

Since everything went well on the components side, we now
move the script over and integrate it into the dev-infra package.

PR Close #37184
2020-05-18 11:55:31 -07:00

115 lines
4.6 KiB
TypeScript

/**
* @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 {MergeResult, MergeStatus, PullRequestMergeTask} from './task';
import {GithubApiRequestError} from './git';
import chalk from 'chalk';
import {promptConfirm} from './console';
import {loadAndValidateConfig} from './config';
import {getRepoBaseDir} from '../../utils/config';
/** URL to the Github page where personal access tokens can be generated. */
export const GITHUB_TOKEN_GENERATE_URL = `https://github.com/settings/tokens`;
/**
* Entry-point for the merge script CLI. The script can be used to merge individual pull requests
* into branches based on the `PR target` labels that have been set in a configuration. The script
* aims to reduce the manual work that needs to be performed to cherry-pick a PR into multiple
* branches based on a target label.
*/
export async function mergePullRequest(prNumber: number, githubToken: string) {
const projectRoot = getRepoBaseDir();
const {config, errors} = loadAndValidateConfig();
if (errors) {
console.error(chalk.red('Invalid configuration:'));
errors.forEach(desc => console.error(chalk.yellow(` - ${desc}`)));
process.exit(1);
}
const api = new PullRequestMergeTask(projectRoot, config, githubToken);
// Perform the merge. Force mode can be activated through a command line flag.
// Alternatively, if the merge fails with non-fatal failures, the script
// will prompt whether it should rerun in force mode.
if (!await performMerge(false)) {
process.exit(1);
}
/** Performs the merge and returns whether it was successful or not. */
async function performMerge(ignoreFatalErrors: boolean): Promise<boolean> {
try {
const result = await api.merge(prNumber, ignoreFatalErrors);
return await handleMergeResult(result, ignoreFatalErrors);
} catch (e) {
// Catch errors to the Github API for invalid requests. We want to
// exit the script with a better explanation of the error.
if (e instanceof GithubApiRequestError && e.status === 401) {
console.error(chalk.red('Github API request failed. ' + e.message));
console.error(chalk.yellow('Please ensure that your provided token is valid.'));
console.error(chalk.yellow(`You can generate a token here: ${GITHUB_TOKEN_GENERATE_URL}`));
process.exit(1);
}
throw e;
}
}
/**
* Prompts whether the specified pull request should be forcibly merged. If so, merges
* the specified pull request forcibly (ignoring non-critical failures).
* @returns Whether the specified pull request has been forcibly merged.
*/
async function promptAndPerformForceMerge(): Promise<boolean> {
if (await promptConfirm('Do you want to forcibly proceed with merging?')) {
// Perform the merge in force mode. This means that non-fatal failures
// are ignored and the merge continues.
return performMerge(true);
}
return false;
}
/**
* 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.
*/
async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) {
const {failure, status} = result;
const canForciblyMerge = failure && failure.nonFatal;
switch (status) {
case MergeStatus.SUCCESS:
console.info(chalk.green(`Successfully merged the pull request: ${prNumber}`));
return true;
case MergeStatus.DIRTY_WORKING_DIR:
console.error(chalk.red(
`Local working repository not clean. Please make sure there are ` +
`no uncommitted changes.`));
return false;
case MergeStatus.UNKNOWN_GIT_ERROR:
console.error(chalk.red(
'An unknown Git error has been thrown. Please check the output ' +
'above for details.'));
return false;
case MergeStatus.FAILED:
console.error(chalk.yellow(`Could not merge the specified pull request.`));
console.error(chalk.red(failure!.message));
if (canForciblyMerge && !disableForceMergePrompt) {
console.info();
console.info(chalk.yellow('The pull request above failed due to non-critical errors.'));
console.info(chalk.yellow(`This error can be forcibly ignored if desired.`));
return await promptAndPerformForceMerge();
}
return false;
default:
throw Error(`Unexpected merge result: ${status}`);
}
}
}