Joey Perrott 719224bffd feat(dev-infra): add support for new global approvers in pullapprove (#36324)
Pullapprove as added a few new features to allow for us to better
execute our expectation for global approvals. We need to allow for
an expectation that our global approver groups are not in the list
of approved groups. Additionally, since approval groups apply to
all files in the repo, the global approval groups also do not have
conditions defined for them, which means pullapprove verification
need to allow for no conditions need to be defined.

PR Close #36324
2020-04-01 13:25:48 -07:00

168 lines
6.2 KiB
TypeScript

/**
* @license
* Copyright Google Inc. 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 {IMinimatch, Minimatch, match} from 'minimatch';
import {PullApproveGroupConfig} from './parse-yaml';
/** A condition for a group. */
interface GroupCondition {
glob: string;
matcher: IMinimatch;
matchedFiles: Set<string>;
}
/** Result of testing files against the group. */
export interface PullApproveGroupResult {
groupName: string;
matchedIncludes: GroupCondition[];
matchedExcludes: GroupCondition[];
matchedCount: number;
unmatchedIncludes: GroupCondition[];
unmatchedExcludes: GroupCondition[];
unmatchedCount: number;
}
// Regex Matcher for contains_any_globs conditions
const CONTAINS_ANY_GLOBS_REGEX = /^'([^']+)',?$/;
const CONDITION_TYPES = {
INCLUDE_GLOBS: /^contains_any_globs/,
EXCLUDE_GLOBS: /^not contains_any_globs/,
ATTR_LENGTH: /^len\(.*\)/,
GLOBAL_APPROVAL: /^global-(docs-)?approvers not in groups.approved$/,
};
/** A PullApprove group to be able to test files against. */
export class PullApproveGroup {
// Lines which were not able to be parsed as expected.
private misconfiguredLines: string[] = [];
// Conditions for the group for including files.
private includeConditions: GroupCondition[] = [];
// Conditions for the group for excluding files.
private excludeConditions: GroupCondition[] = [];
// Whether the group has file matchers.
public hasMatchers = false;
constructor(public groupName: string, group: PullApproveGroupConfig) {
if (group.conditions) {
for (let condition of group.conditions) {
condition = condition.trim();
if (condition.match(CONDITION_TYPES.INCLUDE_GLOBS)) {
const [conditions, misconfiguredLines] = getLinesForContainsAnyGlobs(condition);
conditions.forEach(globString => this.includeConditions.push({
glob: globString,
matcher: new Minimatch(globString, {dot: true}),
matchedFiles: new Set<string>(),
}));
this.misconfiguredLines.push(...misconfiguredLines);
this.hasMatchers = true;
} else if (condition.match(CONDITION_TYPES.EXCLUDE_GLOBS)) {
const [conditions, misconfiguredLines] = getLinesForContainsAnyGlobs(condition);
conditions.forEach(globString => this.excludeConditions.push({
glob: globString,
matcher: new Minimatch(globString, {dot: true}),
matchedFiles: new Set<string>(),
}));
this.misconfiguredLines.push(...misconfiguredLines);
this.hasMatchers = true;
} else if (condition.match(CONDITION_TYPES.ATTR_LENGTH)) {
// Currently a noop as we do not take any action on this condition type.
} else if (condition.match(CONDITION_TYPES.GLOBAL_APPROVAL)) {
// Currently a noop as we don't take any action for global approval conditions.
} else {
const errMessage =
`Unrecognized condition found, unable to parse the following condition: \n\n` +
`From the [${groupName}] group:\n` +
` - ${condition}` +
`\n\n` +
`Known condition regexs:\n` +
`${Object.entries(CONDITION_TYPES).map(([k, v]) => ` ${k} - $ {
v
}
`).join('\n')}` +
`\n\n`;
console.error(errMessage);
process.exit(1);
}
}
}
}
/** Retrieve all of the lines which were not able to be parsed. */
getBadLines(): string[] { return this.misconfiguredLines; }
/** Retrieve the results for the Group, all matched and unmatched conditions. */
getResults(): PullApproveGroupResult {
const matchedIncludes = this.includeConditions.filter(c => !!c.matchedFiles.size);
const matchedExcludes = this.excludeConditions.filter(c => !!c.matchedFiles.size);
const unmatchedIncludes = this.includeConditions.filter(c => !c.matchedFiles.size);
const unmatchedExcludes = this.excludeConditions.filter(c => !c.matchedFiles.size);
const unmatchedCount = unmatchedIncludes.length + unmatchedExcludes.length;
const matchedCount = matchedIncludes.length + matchedExcludes.length;
return {
matchedIncludes,
matchedExcludes,
matchedCount,
unmatchedIncludes,
unmatchedExcludes,
unmatchedCount,
groupName: this.groupName,
};
}
/**
* Tests a provided file path to determine if it would be considered matched by
* the pull approve group's conditions.
*/
testFile(file: string) {
let matched = false;
this.includeConditions.forEach((includeCondition: GroupCondition) => {
if (includeCondition.matcher.match(file)) {
let matchedExclude = false;
this.excludeConditions.forEach((excludeCondition: GroupCondition) => {
if (excludeCondition.matcher.match(file)) {
// Add file as a discovered exclude as it is negating a matched
// include condition.
excludeCondition.matchedFiles.add(file);
matchedExclude = true;
}
});
// An include condition is only considered matched if no exclude
// conditions are found to matched the file.
if (!matchedExclude) {
includeCondition.matchedFiles.add(file);
matched = true;
}
}
});
return matched;
}
}
/**
* Extract all of the individual globs from a group condition,
* providing both the valid and invalid lines.
*/
function getLinesForContainsAnyGlobs(lines: string) {
const invalidLines: string[] = [];
const validLines = lines.split('\n')
.slice(1, -1)
.map((glob: string) => {
const trimmedGlob = glob.trim();
const match = trimmedGlob.match(CONTAINS_ANY_GLOBS_REGEX);
if (!match) {
invalidLines.push(trimmedGlob);
return '';
}
return match[1];
})
.filter(globString => !!globString);
return [validLines, invalidLines];
}