Skip to content

Commit 3a5a6ce

Browse files
committed
Merge branch 'master' into date-range
2 parents 428fccb + 46babbd commit 3a5a6ce

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+2608
-719
lines changed

merge-config.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
module.exports = () => {
2+
const {major, minor} = parseVersion(require('./package').version);
3+
const patchBranch = `${major}.${minor}.x`;
4+
const minorBranch = `${major}.x`;
5+
6+
return {
7+
projectRoot: __dirname,
8+
repository: {
9+
user: 'angular',
10+
name: 'components',
11+
},
12+
// By default, the merge script merges locally with `git cherry-pick` and autosquash.
13+
// This has the downside of pull requests showing up as `Closed` instead of `Merged`.
14+
// In the components repository, since we don't use fixup or squash commits, we can
15+
// use the Github API merge strategy. That way we ensure that PRs show up as `Merged`.
16+
githubApiMerge: {
17+
default: 'squash',
18+
labels: [
19+
{pattern: 'preserve commits', method: 'rebase'}
20+
]
21+
},
22+
claSignedLabel: 'cla: yes',
23+
mergeReadyLabel: 'merge ready',
24+
commitMessageFixupLabel: 'commit message fixup',
25+
labels: [
26+
{
27+
pattern: 'target: patch',
28+
branches: ['master', patchBranch],
29+
},
30+
{
31+
pattern: 'target: minor',
32+
branches: ['master', minorBranch],
33+
},
34+
{
35+
pattern: 'target: major',
36+
branches: ['master'],
37+
},
38+
{
39+
pattern: 'target: development-branch',
40+
// Merge PRs with the given label only into the target branch that has
41+
// been specified through the Github UI.
42+
branches: (target) => [target],
43+
}
44+
],
45+
}
46+
};
47+
48+
/** Converts a version string into an object. */
49+
function parseVersion(version) {
50+
const [major = 0, minor = 0, patch = 0] = version.split('.').map(segment => Number(segment));
51+
return {major, minor, patch};
52+
}

package.json

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@
4242
"stylelint": "stylelint \"src/**/*.+(css|scss)\" --config .stylelintrc.json --syntax scss",
4343
"resync-caretaker-app": "ts-node --project scripts scripts/caretaking/resync-caretaker-app-prs.ts",
4444
"ts-circular-deps:check": "yarn -s ts-circular-deps check --config ./src/circular-deps-test.conf.js",
45-
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve --config ./src/circular-deps-test.conf.js"
45+
"ts-circular-deps:approve": "yarn -s ts-circular-deps approve --config ./src/circular-deps-test.conf.js",
46+
"merge": "ts-node --project scripts scripts/merge-script/cli.ts --config ./merge-config.js"
4647
},
4748
"version": "9.2.1",
4849
"dependencies": {
@@ -57,7 +58,7 @@
5758
"@types/youtube": "^0.0.38",
5859
"@webcomponents/custom-elements": "^1.1.0",
5960
"core-js": "^2.6.9",
60-
"material-components-web": "6.0.0-canary.c4b4bba96.0",
61+
"material-components-web": "6.0.0-canary.deda86d8c.0",
6162
"rxjs": "^6.5.3",
6263
"systemjs": "0.19.43",
6364
"tslib": "^1.10.0",
@@ -68,7 +69,7 @@
6869
"@angular-devkit/schematics": "^9.0.7",
6970
"@angular/bazel": "^9.1.0",
7071
"@angular/compiler-cli": "^9.1.0",
71-
"@angular/dev-infra-private": "angular/dev-infra-private-builds#cdf6637",
72+
"@angular/dev-infra-private": "angular/dev-infra-private-builds#27a2022",
7273
"@angular/platform-browser-dynamic": "^9.1.0",
7374
"@angular/platform-server": "^9.1.0",
7475
"@angular/router": "^9.1.0",
@@ -85,7 +86,7 @@
8586
"@types/autoprefixer": "^9.7.2",
8687
"@types/browser-sync": "^2.26.1",
8788
"@types/fs-extra": "^4.0.3",
88-
"@types/glob": "^5.0.33",
89+
"@types/glob": "^5.0.36",
8990
"@types/gulp": "3.8.32",
9091
"@types/inquirer": "^0.0.43",
9192
"@types/jasmine": "^3.5.4",
@@ -162,7 +163,9 @@
162163
"typescript": "~3.8.3",
163164
"typescript-3.6": "npm:typescript@~3.6.4",
164165
"typescript-3.7": "npm:typescript@~3.7.0",
165-
"vrsource-tslint-rules": "5.1.1"
166+
"vrsource-tslint-rules": "5.1.1",
167+
"yaml": "^1.7.2",
168+
"yargs": "15.3.0"
166169
},
167170
"resolutions": {
168171
"dgeni-packages/typescript": "3.8.3",

scripts/merge-script/cli.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import chalk from 'chalk';
10+
import * as minimist from 'minimist';
11+
import {isAbsolute, resolve} from 'path';
12+
13+
import {Config, readAndValidateConfig} from './config';
14+
import {promptConfirm} from './console';
15+
import {MergeResult, MergeStatus, PullRequestMergeTask} from './index';
16+
17+
// Run the CLI.
18+
main();
19+
20+
/**
21+
* Entry-point for the merge script CLI. The script can be used to merge individual pull requests
22+
* into branches based on the `PR target` labels that have been set in a configuration. The script
23+
* aims to reduce the manual work that needs to be performed to cherry-pick a PR into multiple
24+
* branches based on a target label.
25+
*/
26+
async function main() {
27+
const {config, prNumber, force, githubToken} = parseCommandLine();
28+
const api = new PullRequestMergeTask(config, githubToken);
29+
30+
// Perform the merge. Force mode can be activated through a command line flag.
31+
// Alternatively, if the merge fails with non-fatal failures, the script
32+
// will prompt whether it should rerun in force mode.
33+
const mergeResult = await api.merge(prNumber, force);
34+
35+
// Handle the result of the merge. If failures have been reported, exit
36+
// the process with a non-zero exit code.
37+
if (!await handleMergeResult(mergeResult, force)) {
38+
process.exit(1);
39+
}
40+
41+
/**
42+
* Prompts whether the specified pull request should be forcibly merged. If so, merges
43+
* the specified pull request forcibly (ignoring non-critical failures).
44+
* @returns Whether the specified pull request has been forcibly merged.
45+
*/
46+
async function promptAndPerformForceMerge(): Promise<boolean> {
47+
if (await promptConfirm('Do you want to forcibly proceed with merging?')) {
48+
// Perform the merge in force mode. This means that non-fatal failures
49+
// are ignored and the merge continues.
50+
const forceMergeResult = await api.merge(prNumber, true);
51+
// Handle the merge result. Note that we disable the force merge prompt since
52+
// a failed force merge will never succeed with a second force merge.
53+
return await handleMergeResult(forceMergeResult, true);
54+
}
55+
return false;
56+
}
57+
58+
/**
59+
* Handles the merge result by printing console messages, exiting the process
60+
* based on the result, or by restarting the merge if force mode has been enabled.
61+
* @returns Whether the merge was successful or not.
62+
*/
63+
async function handleMergeResult(result: MergeResult, disableForceMergePrompt = false) {
64+
const {failure, status} = result;
65+
const canForciblyMerge = failure && failure.nonFatal;
66+
67+
switch (status) {
68+
case MergeStatus.SUCCESS:
69+
console.info(chalk.green(`Successfully merged the pull request: ${prNumber}`));
70+
return true;
71+
case MergeStatus.DIRTY_WORKING_DIR:
72+
console.error(chalk.red(
73+
`Local working repository not clean. Please make sure there are ` +
74+
`no uncommitted changes.`));
75+
return false;
76+
case MergeStatus.UNKNOWN_GIT_ERROR:
77+
console.error(chalk.red(
78+
'An unknown Git error has been thrown. Please check the output ' +
79+
'above for details.'));
80+
return false;
81+
case MergeStatus.FAILED:
82+
console.error(chalk.yellow(`Could not merge the specified pull request.`));
83+
console.error(chalk.red(failure!.message));
84+
if (canForciblyMerge && !disableForceMergePrompt) {
85+
console.info();
86+
console.info(chalk.yellow('The pull request above failed due to non-critical errors.'));
87+
console.info(chalk.yellow(`This error can be forcibly ignored if desired.`));
88+
return await promptAndPerformForceMerge();
89+
}
90+
return false;
91+
default:
92+
throw Error(`Unexpected merge result: ${status}`);
93+
}
94+
}
95+
}
96+
97+
// TODO(devversion): Use Yargs for this once the script has been moved to `angular/angular`.
98+
/** Parses the command line and returns the passed options. */
99+
function parseCommandLine():
100+
{config: Config, force: boolean, prNumber: number, dryRun?: boolean, githubToken: string} {
101+
const {config: configPath, githubToken: githubTokenArg, force, _: [prNumber]} =
102+
minimist<any>(process.argv.slice(2), {
103+
string: ['githubToken', 'config', 'pr'],
104+
alias: {
105+
'githubToken': 'github-token',
106+
},
107+
});
108+
109+
if (!configPath) {
110+
console.error(chalk.red('No configuration file specified. Please pass the `--config` option.'));
111+
process.exit(1);
112+
}
113+
114+
if (!prNumber) {
115+
console.error(chalk.red('No pull request specified. Please pass a pull request number.'));
116+
process.exit(1);
117+
}
118+
119+
const configFilePath = isAbsolute(configPath) ? configPath : resolve(configPath);
120+
const {config, errors} = readAndValidateConfig(configFilePath);
121+
122+
if (errors) {
123+
console.error(chalk.red('Configuration could not be read:'));
124+
errors.forEach(desc => console.error(chalk.yellow(` * ${desc}`)));
125+
process.exit(1);
126+
}
127+
128+
const githubToken = githubTokenArg || process.env.GITHUB_TOKEN || process.env.TOKEN;
129+
if (!githubToken) {
130+
console.error(
131+
chalk.red('No Github token is set. Please set the `GITHUB_TOKEN` environment variable.'));
132+
console.error(chalk.red('Alternatively, pass the `--github-token` command line flag.'));
133+
process.exit(1);
134+
}
135+
136+
return {config: config!, prNumber, githubToken, force};
137+
}

scripts/merge-script/config.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {dirname, resolve} from 'path';
10+
import {GithubApiMergeStrategyConfig} from './strategies/api-merge';
11+
12+
/**
13+
* Possible merge methods supported by the Github API.
14+
* https://developer.github.com/v3/pulls/#merge-a-pull-request-merge-button.
15+
*/
16+
export type GithubApiMergeMethod = 'merge'|'squash'|'rebase';
17+
18+
/**
19+
* Target labels represent Github pull requests labels. These labels instruct the merge
20+
* script into which branches a given pull request should be merged to.
21+
*/
22+
export interface TargetLabel {
23+
/** Pattern that matches the given target label. */
24+
pattern: RegExp|string;
25+
/**
26+
* List of branches a pull request with this target label should be merged into.
27+
* Can also be wrapped in a function that accepts the target branch specified in the
28+
* Github Web UI. This is useful for supporting labels like `target: development-branch`.
29+
*/
30+
branches: string[]|((githubTargetBranch: string) => string[]);
31+
}
32+
33+
/** Configuration for the merge script. */
34+
export interface Config {
35+
/** Relative path (based on the config location) to the project root. */
36+
projectRoot: string;
37+
/** Configuration for the upstream repository. */
38+
repository: {user: string; name: string; useSsh?: boolean};
39+
/** List of target labels. */
40+
labels: TargetLabel[];
41+
/** Required base commits for given branches. */
42+
requiredBaseCommits?: {[branchName: string]: string};
43+
/** Pattern that matches labels which imply a signed CLA. */
44+
claSignedLabel: string|RegExp;
45+
/** Pattern that matches labels which imply a merge ready pull request. */
46+
mergeReadyLabel: string|RegExp;
47+
/** Label which can be applied to fixup commit messages in the merge script. */
48+
commitMessageFixupLabel: string|RegExp;
49+
/**
50+
* Whether pull requests should be merged using the Github API. This can be enabled
51+
* if projects want to have their pull requests show up as `Merged` in the Github UI.
52+
* The downside is that fixup or squash commits no longer work as the Github API does
53+
* not support this.
54+
*/
55+
githubApiMerge: false | GithubApiMergeStrategyConfig;
56+
}
57+
58+
/** Reads and validates the configuration file at the specified location. */
59+
export function readAndValidateConfig(filePath: string): {config?: Config, errors?: string[]} {
60+
// Capture errors when the configuration cannot be read. Errors will be thrown if the file
61+
// does not exist, if the config file cannot be evaluated, or if no default export is found.
62+
try {
63+
const config = require(filePath)() as Config;
64+
const errors = validateConfig(config);
65+
if (errors.length) {
66+
return {errors};
67+
}
68+
// Resolves the project root path to an absolute path based on the
69+
// config file location.
70+
config.projectRoot = resolve(dirname(filePath), config.projectRoot);
71+
return {config};
72+
} catch (e) {
73+
return {errors: [`File could not be loaded. Error: ${e.message}`]};
74+
}
75+
}
76+
77+
/** Validates the specified configuration. Returns a list of failure messages. */
78+
function validateConfig(config: Config): string[] {
79+
const errors: string[] = [];
80+
if (!config.projectRoot) {
81+
errors.push('Missing project root.');
82+
}
83+
if (!config.labels) {
84+
errors.push('No label configuration.');
85+
} else if (!Array.isArray(config.labels)) {
86+
errors.push('Label configuration needs to be an array.');
87+
}
88+
if (!config.repository) {
89+
errors.push('No repository is configured.');
90+
} else if (!config.repository.user || !config.repository.name) {
91+
errors.push('Repository configuration needs to specify a `user` and repository `name`.');
92+
}
93+
if (!config.claSignedLabel) {
94+
errors.push('No CLA signed label configured.');
95+
}
96+
if (!config.mergeReadyLabel) {
97+
errors.push('No merge ready label configured.');
98+
}
99+
if (config.githubApiMerge === undefined) {
100+
errors.push('No explicit choice of merge strategy. Please set `githubApiMerge`.');
101+
}
102+
return errors;
103+
}

scripts/merge-script/console.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {prompt} from 'inquirer';
10+
11+
/** Prompts the user with a confirmation question and a specified message. */
12+
export async function promptConfirm(message: string, defaultValue = false): Promise<boolean> {
13+
return (await prompt<{result: boolean}>({
14+
type: 'confirm',
15+
name: 'result',
16+
message: message,
17+
default: defaultValue,
18+
}))
19+
.result;
20+
}

0 commit comments

Comments
 (0)