There are a few changes in this PR to ensure conditions that are based
on groups (i.e. `- groups.pending.length == 0`) do not fail the verify
task:
* Remove the warning when a condition is encountered that depends on the
`groups` state. The warning will otherwise be printed once for every
file that triggers the execution of the condition (400,000+ times)
* Add an `unverifiable` flag to `GroupCondition` interface and set it to
true when an error is encountered due to attempting to get the state of
`groups` in a condition
* Ignore any unverifiable conditions when gathering unmatched
conditions. These should not be considered `unmatched` for verification
purposes.
* Print the unverifiable conditions by group in the results
Sample output:
```
┌──────────────────────────────────────────────────────────────────────────────┐
│                         PullApprove results by group                         │
└──────────────────────────────────────────────────────────────────────────────┘
Groups skipped (4 groups)
Matched conditions by Group (37 groups)
Unmatched conditions by Group (0 groups)
Unverifiable conditions by Group (3 groups)
  [public-api]
    len(groups.pending.exclude("required-minimum-review")...
    len(groups.rejected.exclude("required-minimum-review")...
  [size-tracking]
    len(groups.pending.exclude("required-minimum-review")...
    len(groups.rejected.exclude("required-minimum-review")...
  [circular-dependencies]
    len(groups.pending.exclude("required-minimum-review")...
    len(groups.rejected.exclude("required-minimum-review")...
```
PR Close #37798
		
	
			
		
			
				
	
	
		
			130 lines
		
	
	
		
			4.5 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			130 lines
		
	
	
		
			4.5 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 {error} from '../utils/console';
 | |
| import {convertConditionToFunction} from './condition_evaluator';
 | |
| import {PullApproveGroupConfig} from './parse-yaml';
 | |
| import {PullApproveGroupStateDependencyError} from './pullapprove_arrays';
 | |
| 
 | |
| /** A condition for a group. */
 | |
| interface GroupCondition {
 | |
|   expression: string;
 | |
|   checkFn: (files: string[], groups: PullApproveGroup[]) => boolean;
 | |
|   matchedFiles: Set<string>;
 | |
|   unverifiable: boolean;
 | |
| }
 | |
| 
 | |
| /** Result of testing files against the group. */
 | |
| export interface PullApproveGroupResult {
 | |
|   groupName: string;
 | |
|   matchedConditions: GroupCondition[];
 | |
|   matchedCount: number;
 | |
|   unmatchedConditions: GroupCondition[];
 | |
|   unmatchedCount: number;
 | |
|   unverifiableConditions: GroupCondition[];
 | |
| }
 | |
| 
 | |
| // Regular expression that matches conditions for the global approval.
 | |
| const GLOBAL_APPROVAL_CONDITION_REGEX = /^"global-(docs-)?approvers" not in groups.approved$/;
 | |
| 
 | |
| // Name of the PullApprove group that serves as fallback. This group should never capture
 | |
| // any conditions as it would always match specified files. This is not desired as we want
 | |
| // to figure out as part of this tool, whether there actually are unmatched files.
 | |
| const FALLBACK_GROUP_NAME = 'fallback';
 | |
| 
 | |
| /** A PullApprove group to be able to test files against. */
 | |
| export class PullApproveGroup {
 | |
|   /** List of conditions for the group. */
 | |
|   conditions: GroupCondition[] = [];
 | |
| 
 | |
|   constructor(
 | |
|       public groupName: string, config: PullApproveGroupConfig,
 | |
|       readonly precedingGroups: PullApproveGroup[] = []) {
 | |
|     this._captureConditions(config);
 | |
|   }
 | |
| 
 | |
|   private _captureConditions(config: PullApproveGroupConfig) {
 | |
|     if (config.conditions && this.groupName !== FALLBACK_GROUP_NAME) {
 | |
|       return config.conditions.forEach(condition => {
 | |
|         const expression = condition.trim();
 | |
| 
 | |
|         if (expression.match(GLOBAL_APPROVAL_CONDITION_REGEX)) {
 | |
|           // Currently a noop as we don't take any action for global approval conditions.
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           this.conditions.push({
 | |
|             expression,
 | |
|             checkFn: convertConditionToFunction(expression),
 | |
|             matchedFiles: new Set(),
 | |
|             unverifiable: false,
 | |
|           });
 | |
|         } catch (e) {
 | |
|           error(`Could not parse condition in group: ${this.groupName}`);
 | |
|           error(` - ${expression}`);
 | |
|           error(`Error:`);
 | |
|           error(e.message);
 | |
|           error(e.stack);
 | |
|           process.exit(1);
 | |
|         }
 | |
|       });
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Tests a provided file path to determine if it would be considered matched by
 | |
|    * the pull approve group's conditions.
 | |
|    */
 | |
|   testFile(filePath: string): boolean {
 | |
|     return this.conditions.every((condition) => {
 | |
|       const {matchedFiles, checkFn, expression} = condition;
 | |
|       try {
 | |
|         const matchesFile = checkFn([filePath], this.precedingGroups);
 | |
|         if (matchesFile) {
 | |
|           matchedFiles.add(filePath);
 | |
|         }
 | |
|         return matchesFile;
 | |
|       } catch (e) {
 | |
|         // In the case of a condition that depends on the state of groups we want to
 | |
|         // ignore that the verification can't accurately evaluate the condition and then
 | |
|         // continue processing. Other types of errors fail the verification, as conditions
 | |
|         // should otherwise be able to execute without throwing.
 | |
|         if (e instanceof PullApproveGroupStateDependencyError) {
 | |
|           condition.unverifiable = true;
 | |
|           // Return true so that `this.conditions.every` can continue evaluating.
 | |
|           return true;
 | |
|         } else {
 | |
|           const errMessage = `Condition could not be evaluated: \n\n` +
 | |
|               `From the [${this.groupName}] group:\n` +
 | |
|               ` - ${expression}` +
 | |
|               `\n\n${e.message} ${e.stack}\n\n`;
 | |
|           error(errMessage);
 | |
|           process.exit(1);
 | |
|         }
 | |
|       }
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   /** Retrieve the results for the Group, all matched and unmatched conditions. */
 | |
|   getResults(): PullApproveGroupResult {
 | |
|     const matchedConditions = this.conditions.filter(c => c.matchedFiles.size > 0);
 | |
|     const unmatchedConditions =
 | |
|         this.conditions.filter(c => c.matchedFiles.size === 0 && !c.unverifiable);
 | |
|     const unverifiableConditions = this.conditions.filter(c => c.unverifiable);
 | |
|     return {
 | |
|       matchedConditions,
 | |
|       matchedCount: matchedConditions.length,
 | |
|       unmatchedConditions,
 | |
|       unmatchedCount: unmatchedConditions.length,
 | |
|       unverifiableConditions,
 | |
|       groupName: this.groupName,
 | |
|     };
 | |
|   }
 | |
| }
 |