feat(dev-infra): add support for `targetLabelExemptScopes` for merging (#41459)
Add a property, `targetLabelExemptScopes`, to the merge configuration allowing certain scopes to be exempted from requirements for features and breaking changes only included in PRs targetting certain labels. PR Close #41459
This commit is contained in:
parent
7dba0711c2
commit
c7d86bca21
|
@ -3355,13 +3355,13 @@ var PullRequestFailure = /** @class */ (function () {
|
||||||
};
|
};
|
||||||
PullRequestFailure.hasBreakingChanges = function (label) {
|
PullRequestFailure.hasBreakingChanges = function (label) {
|
||||||
var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request has " +
|
var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request has " +
|
||||||
"breaking changes. Breaking changes can only be merged with the \"target: major\" label.";
|
"breaking changes. Breaking changes can only be merged with the \"target: major\" label.";
|
||||||
return new this(message);
|
return new this(message);
|
||||||
};
|
};
|
||||||
PullRequestFailure.hasFeatureCommits = function (label) {
|
PullRequestFailure.hasFeatureCommits = function (label) {
|
||||||
var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request has " +
|
var message = "Cannot merge into branch for \"" + label.pattern + "\" as the pull request has " +
|
||||||
'commits with the "feat" type. New features can only be merged with the "target: minor" or ' +
|
'commits with the "feat" type. New features can only be merged with the "target: minor" ' +
|
||||||
'"target: major" label.';
|
'or "target: major" label.';
|
||||||
return new this(message);
|
return new this(message);
|
||||||
};
|
};
|
||||||
return PullRequestFailure;
|
return PullRequestFailure;
|
||||||
|
@ -3398,7 +3398,7 @@ function loadAndValidatePullRequest(_a, prNumber, ignoreNonFatalFailures) {
|
||||||
var git = _a.git, config = _a.config;
|
var git = _a.git, config = _a.config;
|
||||||
if (ignoreNonFatalFailures === void 0) { ignoreNonFatalFailures = false; }
|
if (ignoreNonFatalFailures === void 0) { ignoreNonFatalFailures = false; }
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
var prData, labels, targetLabel, state, githubTargetBranch, requiredBaseSha, needsCommitMessageFixup, hasCaretakerNote, targetBranches, error_1;
|
var prData, labels, targetLabel, commitMessages, state, githubTargetBranch, requiredBaseSha, needsCommitMessageFixup, hasCaretakerNote, targetBranches, error_1;
|
||||||
return tslib.__generator(this, function (_b) {
|
return tslib.__generator(this, function (_b) {
|
||||||
switch (_b.label) {
|
switch (_b.label) {
|
||||||
case 0: return [4 /*yield*/, fetchPullRequestFromGithub(git, prNumber)];
|
case 0: return [4 /*yield*/, fetchPullRequestFromGithub(git, prNumber)];
|
||||||
|
@ -3424,7 +3424,8 @@ function loadAndValidatePullRequest(_a, prNumber, ignoreNonFatalFailures) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
assertCorrectTargetForChanges(prData.commits.nodes.map(function (n) { return n.commit.message; }), targetLabel);
|
commitMessages = prData.commits.nodes.map(function (n) { return n.commit.message; });
|
||||||
|
assertChangesAllowForTargetLabel(commitMessages, targetLabel, config);
|
||||||
}
|
}
|
||||||
catch (error) {
|
catch (error) {
|
||||||
return [2 /*return*/, error];
|
return [2 /*return*/, error];
|
||||||
|
@ -3465,17 +3466,20 @@ function loadAndValidatePullRequest(_a, prNumber, ignoreNonFatalFailures) {
|
||||||
hasCaretakerNote: hasCaretakerNote,
|
hasCaretakerNote: hasCaretakerNote,
|
||||||
targetBranches: targetBranches,
|
targetBranches: targetBranches,
|
||||||
title: prData.title,
|
title: prData.title,
|
||||||
commitCount: prData.commits.nodes.length,
|
commitCount: prData.commits.totalCount,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
/* GraphQL schema for the response body the requested PR. */
|
/* GraphQL schema for the response body the requested pull request. */
|
||||||
var PR_SCHEMA$2 = {
|
var PR_SCHEMA$2 = {
|
||||||
url: typedGraphqlify.types.string,
|
url: typedGraphqlify.types.string,
|
||||||
number: typedGraphqlify.types.number,
|
number: typedGraphqlify.types.number,
|
||||||
commits: typedGraphqlify.params({ first: 100 }, {
|
// Only the last 100 commits from a pull request are obtained as we likely will never see a pull
|
||||||
|
// requests with more than 100 commits.
|
||||||
|
commits: typedGraphqlify.params({ last: 100 }, {
|
||||||
|
totalCount: typedGraphqlify.types.number,
|
||||||
nodes: [{
|
nodes: [{
|
||||||
commit: {
|
commit: {
|
||||||
status: {
|
status: {
|
||||||
|
@ -3496,13 +3500,15 @@ var PR_SCHEMA$2 = {
|
||||||
/** Fetches a pull request from Github. Returns null if an error occurred. */
|
/** Fetches a pull request from Github. Returns null if an error occurred. */
|
||||||
function fetchPullRequestFromGithub(git, prNumber) {
|
function fetchPullRequestFromGithub(git, prNumber) {
|
||||||
return tslib.__awaiter(this, void 0, void 0, function () {
|
return tslib.__awaiter(this, void 0, void 0, function () {
|
||||||
var e_1;
|
var x, e_1;
|
||||||
return tslib.__generator(this, function (_a) {
|
return tslib.__generator(this, function (_a) {
|
||||||
switch (_a.label) {
|
switch (_a.label) {
|
||||||
case 0:
|
case 0:
|
||||||
_a.trys.push([0, 2, , 3]);
|
_a.trys.push([0, 2, , 3]);
|
||||||
return [4 /*yield*/, getPr(PR_SCHEMA$2, prNumber, git)];
|
return [4 /*yield*/, getPr(PR_SCHEMA$2, prNumber, git)];
|
||||||
case 1: return [2 /*return*/, _a.sent()];
|
case 1:
|
||||||
|
x = _a.sent();
|
||||||
|
return [2 /*return*/, x];
|
||||||
case 2:
|
case 2:
|
||||||
e_1 = _a.sent();
|
e_1 = _a.sent();
|
||||||
// If the pull request could not be found, we want to return `null` so
|
// If the pull request could not be found, we want to return `null` so
|
||||||
|
@ -3524,32 +3530,39 @@ function isPullRequest(v) {
|
||||||
* Assert the commits provided are allowed to merge to the provided target label, throwing a
|
* Assert the commits provided are allowed to merge to the provided target label, throwing a
|
||||||
* PullRequestFailure otherwise.
|
* PullRequestFailure otherwise.
|
||||||
*/
|
*/
|
||||||
function assertCorrectTargetForChanges(rawCommits, label) {
|
function assertChangesAllowForTargetLabel(rawCommits, label, config) {
|
||||||
/** List of ParsedCommits for all of the commits in the pull request. */
|
/**
|
||||||
var commits = rawCommits.map(parseCommitMessage);
|
* List of commit scopes which are exempted from target label content requirements. i.e. no `feat`
|
||||||
|
* scopes in patch branches, no breaking changes in minor or patch changes.
|
||||||
|
*/
|
||||||
|
var exemptedScopes = config.targetLabelExemptScopes || [];
|
||||||
|
/** List of parsed commits which are subject to content requirements for the target label. */
|
||||||
|
var commits = rawCommits.map(parseCommitMessage).filter(function (commit) {
|
||||||
|
return !exemptedScopes.includes(commit.scope);
|
||||||
|
});
|
||||||
switch (label.pattern) {
|
switch (label.pattern) {
|
||||||
case 'target: major':
|
case 'target: major':
|
||||||
break;
|
break;
|
||||||
case 'target: minor':
|
case 'target: minor':
|
||||||
// Check if any commits in the PR contains a breaking change.
|
// Check if any commits in the pull request contains a breaking change.
|
||||||
if (commits.some(function (commit) { return commit.breakingChanges.length !== 0; })) {
|
if (commits.some(function (commit) { return commit.breakingChanges.length !== 0; })) {
|
||||||
throw PullRequestFailure.hasBreakingChanges(label);
|
throw PullRequestFailure.hasBreakingChanges(label);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'target: patch':
|
case 'target: patch':
|
||||||
case 'target: lts':
|
case 'target: lts':
|
||||||
// Check if any commits in the PR contains a breaking change.
|
// Check if any commits in the pull request contains a breaking change.
|
||||||
if (commits.some(function (commit) { return commit.breakingChanges.length !== 0; })) {
|
if (commits.some(function (commit) { return commit.breakingChanges.length !== 0; })) {
|
||||||
throw PullRequestFailure.hasBreakingChanges(label);
|
throw PullRequestFailure.hasBreakingChanges(label);
|
||||||
}
|
}
|
||||||
// Check if any commits in the PR contains a commit type of "feat".
|
// Check if any commits in the pull request contains a commit type of "feat".
|
||||||
if (commits.some(function (commit) { return commit.type === 'feat'; })) {
|
if (commits.some(function (commit) { return commit.type === 'feat'; })) {
|
||||||
throw PullRequestFailure.hasFeatureCommits(label);
|
throw PullRequestFailure.hasFeatureCommits(label);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
warn(red('WARNING: Unable to confirm all commits in the PR are eligible to be merged'));
|
warn(red('WARNING: Unable to confirm all commits in the pull request are eligible to be'));
|
||||||
warn(red("into the target branch: " + label.pattern));
|
warn(red("merged into the target branch: " + label.pattern));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,6 +71,11 @@ export interface MergeConfig {
|
||||||
* not support this.
|
* not support this.
|
||||||
*/
|
*/
|
||||||
githubApiMerge: false|GithubApiMergeStrategyConfig;
|
githubApiMerge: false|GithubApiMergeStrategyConfig;
|
||||||
|
/**
|
||||||
|
* List of commit scopes which are exempted from target label content requirements. i.e. no `feat`
|
||||||
|
* scopes in patch branches, no breaking changes in minor or patch changes.
|
||||||
|
*/
|
||||||
|
targetLabelExemptScopes?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -77,13 +77,13 @@ export class PullRequestFailure {
|
||||||
|
|
||||||
static hasBreakingChanges(label: TargetLabel) {
|
static hasBreakingChanges(label: TargetLabel) {
|
||||||
const message = `Cannot merge into branch for "${label.pattern}" as the pull request has ` +
|
const message = `Cannot merge into branch for "${label.pattern}" as the pull request has ` +
|
||||||
`breaking changes. Breaking changes can only be merged with the "target: major" label.`;
|
`breaking changes. Breaking changes can only be merged with the "target: major" label.`;
|
||||||
return new this(message);
|
return new this(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
static hasFeatureCommits(label: TargetLabel) {
|
static hasFeatureCommits(label: TargetLabel) {
|
||||||
const message = `Cannot merge into branch for "${label.pattern}" as the pull request has ` +
|
const message = `Cannot merge into branch for "${label.pattern}" as the pull request has ` +
|
||||||
'commits with the "feat" type. New features can only be merged with the "target: minor" ' +
|
'commits with the "feat" type. New features can only be merged with the "target: minor" ' +
|
||||||
'or "target: major" label.';
|
'or "target: major" label.';
|
||||||
return new this(message);
|
return new this(message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import {red, warn} from '../../utils/console';
|
||||||
|
|
||||||
import {GitClient} from '../../utils/git/index';
|
import {GitClient} from '../../utils/git/index';
|
||||||
import {getPr} from '../../utils/github';
|
import {getPr} from '../../utils/github';
|
||||||
import {TargetLabel} from './config';
|
import {MergeConfig, TargetLabel} from './config';
|
||||||
|
|
||||||
import {PullRequestFailure} from './failures';
|
import {PullRequestFailure} from './failures';
|
||||||
import {matchesPattern} from './string-pattern';
|
import {matchesPattern} from './string-pattern';
|
||||||
|
@ -76,11 +76,14 @@ export async function loadAndValidatePullRequest(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
assertCorrectTargetForChanges(prData.commits.nodes.map(n => n.commit.message), targetLabel);
|
/** Commit message strings for all commits in the pull request. */
|
||||||
|
const commitMessages = prData.commits.nodes.map(n => n.commit.message);
|
||||||
|
assertChangesAllowForTargetLabel(commitMessages, targetLabel, config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The combined status of the latest commit in the pull request. */
|
||||||
const state = prData.commits.nodes.slice(-1)[0].commit.status.state;
|
const state = prData.commits.nodes.slice(-1)[0].commit.status.state;
|
||||||
if (state === 'FAILURE' && !ignoreNonFatalFailures) {
|
if (state === 'FAILURE' && !ignoreNonFatalFailures) {
|
||||||
return PullRequestFailure.failingCiJobs();
|
return PullRequestFailure.failingCiJobs();
|
||||||
|
@ -121,15 +124,18 @@ export async function loadAndValidatePullRequest(
|
||||||
hasCaretakerNote,
|
hasCaretakerNote,
|
||||||
targetBranches,
|
targetBranches,
|
||||||
title: prData.title,
|
title: prData.title,
|
||||||
commitCount: prData.commits.nodes.length,
|
commitCount: prData.commits.totalCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/* GraphQL schema for the response body the requested PR. */
|
/* GraphQL schema for the response body the requested pull request. */
|
||||||
const PR_SCHEMA = {
|
const PR_SCHEMA = {
|
||||||
url: graphQLTypes.string,
|
url: graphQLTypes.string,
|
||||||
number: graphQLTypes.number,
|
number: graphQLTypes.number,
|
||||||
commits: params({first: 100}, {
|
// Only the last 100 commits from a pull request are obtained as we likely will never see a pull
|
||||||
|
// requests with more than 100 commits.
|
||||||
|
commits: params({last: 100}, {
|
||||||
|
totalCount: graphQLTypes.number,
|
||||||
nodes: [{
|
nodes: [{
|
||||||
commit: {
|
commit: {
|
||||||
status: {
|
status: {
|
||||||
|
@ -154,7 +160,8 @@ const PR_SCHEMA = {
|
||||||
async function fetchPullRequestFromGithub(
|
async function fetchPullRequestFromGithub(
|
||||||
git: GitClient, prNumber: number): Promise<typeof PR_SCHEMA|null> {
|
git: GitClient, prNumber: number): Promise<typeof PR_SCHEMA|null> {
|
||||||
try {
|
try {
|
||||||
return await getPr(PR_SCHEMA, prNumber, git);
|
const x = await getPr(PR_SCHEMA, prNumber, git);
|
||||||
|
return x;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If the pull request could not be found, we want to return `null` so
|
// If the pull request could not be found, we want to return `null` so
|
||||||
// that the error can be handled gracefully.
|
// that the error can be handled gracefully.
|
||||||
|
@ -174,32 +181,40 @@ export function isPullRequest(v: PullRequestFailure|PullRequest): v is PullReque
|
||||||
* Assert the commits provided are allowed to merge to the provided target label, throwing a
|
* Assert the commits provided are allowed to merge to the provided target label, throwing a
|
||||||
* PullRequestFailure otherwise.
|
* PullRequestFailure otherwise.
|
||||||
*/
|
*/
|
||||||
function assertCorrectTargetForChanges(rawCommits: string[], label: TargetLabel) {
|
function assertChangesAllowForTargetLabel(
|
||||||
/** List of ParsedCommits for all of the commits in the pull request. */
|
rawCommits: string[], label: TargetLabel, config: MergeConfig) {
|
||||||
const commits = rawCommits.map(parseCommitMessage);
|
/**
|
||||||
|
* List of commit scopes which are exempted from target label content requirements. i.e. no `feat`
|
||||||
|
* scopes in patch branches, no breaking changes in minor or patch changes.
|
||||||
|
*/
|
||||||
|
const exemptedScopes = config.targetLabelExemptScopes || [];
|
||||||
|
/** List of parsed commits which are subject to content requirements for the target label. */
|
||||||
|
let commits = rawCommits.map(parseCommitMessage).filter(commit => {
|
||||||
|
return !exemptedScopes.includes(commit.scope);
|
||||||
|
});
|
||||||
switch (label.pattern) {
|
switch (label.pattern) {
|
||||||
case 'target: major':
|
case 'target: major':
|
||||||
break;
|
break;
|
||||||
case 'target: minor':
|
case 'target: minor':
|
||||||
// Check if any commits in the PR contains a breaking change.
|
// Check if any commits in the pull request contains a breaking change.
|
||||||
if (commits.some(commit => commit.breakingChanges.length !== 0)) {
|
if (commits.some(commit => commit.breakingChanges.length !== 0)) {
|
||||||
throw PullRequestFailure.hasBreakingChanges(label);
|
throw PullRequestFailure.hasBreakingChanges(label);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'target: patch':
|
case 'target: patch':
|
||||||
case 'target: lts':
|
case 'target: lts':
|
||||||
// Check if any commits in the PR contains a breaking change.
|
// Check if any commits in the pull request contains a breaking change.
|
||||||
if (commits.some(commit => commit.breakingChanges.length !== 0)) {
|
if (commits.some(commit => commit.breakingChanges.length !== 0)) {
|
||||||
throw PullRequestFailure.hasBreakingChanges(label);
|
throw PullRequestFailure.hasBreakingChanges(label);
|
||||||
}
|
}
|
||||||
// Check if any commits in the PR contains a commit type of "feat".
|
// Check if any commits in the pull request contains a commit type of "feat".
|
||||||
if (commits.some(commit => commit.type === 'feat')) {
|
if (commits.some(commit => commit.type === 'feat')) {
|
||||||
throw PullRequestFailure.hasFeatureCommits(label);
|
throw PullRequestFailure.hasFeatureCommits(label);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
warn(red('WARNING: Unable to confirm all commits in the PR are eligible to be merged'));
|
warn(red('WARNING: Unable to confirm all commits in the pull request are eligible to be'));
|
||||||
warn(red(`into the target branch: ${label.pattern}`));
|
warn(red(`merged into the target branch: ${label.pattern}`));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue