Add support for exclusions (#22)

* Add support for exclusions

Paths can be negated to stop searching through the remaining patterns in
a the glob list. All changed files tested and at least one matching file
will result in a label being added.

Fixes actions/labeler#9

* Add support for "AND"-ed matches

A new "rich" matcher object can be provided instead of a normal glob.

The matcher object has two fields that accept an array of globs:
* Globs in "all" must all match every changed file.
* Globs in "some" must all match at least one changed file.

Combined with negated globs, this allows for a precise control of when
labels are applied.

* Rename `some` to `any`

* Update README
This commit is contained in:
Jameel Al-Aziz
2020-06-01 14:01:37 -07:00
committed by GitHub
parent 9984882865
commit 4b52aec09b
3 changed files with 310 additions and 275 deletions

View File

@@ -1,7 +1,14 @@
import * as core from '@actions/core';
import * as github from '@actions/github';
import * as yaml from 'js-yaml';
import {Minimatch} from 'minimatch';
import {Minimatch, IMinimatch} from 'minimatch';
interface MatchConfig {
all?: string[];
any?: string[];
}
type StringOrMatchConfig = string | MatchConfig;
async function run() {
try {
@@ -18,7 +25,7 @@ async function run() {
core.debug(`fetching changed files for pr #${prNumber}`);
const changedFiles: string[] = await getChangedFiles(client, prNumber);
const labelGlobs: Map<string, string[]> = await getLabelGlobs(
const labelGlobs: Map<string, StringOrMatchConfig[]> = await getLabelGlobs(
client,
configPath
);
@@ -72,16 +79,16 @@ async function getChangedFiles(
async function getLabelGlobs(
client: github.GitHub,
configurationPath: string
): Promise<Map<string, string[]>> {
): Promise<Map<string, StringOrMatchConfig[]>> {
const configurationContent: string = await fetchContent(
client,
configurationPath
);
// loads (hopefully) a `{[label:string]: string | string[]}`, but is `any`:
// loads (hopefully) a `{[label:string]: string | StringOrMatchConfig[]}`, but is `any`:
const configObject: any = yaml.safeLoad(configurationContent);
// transform `any` => `Map<string,string[]>` or throw if yaml is malformed:
// transform `any` => `Map<string,StringOrMatchConfig[]>` or throw if yaml is malformed:
return getLabelGlobMapFromObject(configObject);
}
@@ -99,10 +106,12 @@ async function fetchContent(
return Buffer.from(response.data.content, response.data.encoding).toString();
}
function getLabelGlobMapFromObject(configObject: any): Map<string, string[]> {
const labelGlobs: Map<string, string[]> = new Map();
function getLabelGlobMapFromObject(
configObject: any
): Map<string, StringOrMatchConfig[]> {
const labelGlobs: Map<string, StringOrMatchConfig[]> = new Map();
for (const label in configObject) {
if (typeof configObject[label] === 'string') {
if (typeof configObject[label] === "string") {
labelGlobs.set(label, [configObject[label]]);
} else if (configObject[label] instanceof Array) {
labelGlobs.set(label, configObject[label]);
@@ -116,21 +125,80 @@ function getLabelGlobMapFromObject(configObject: any): Map<string, string[]> {
return labelGlobs;
}
function checkGlobs(changedFiles: string[], globs: string[]): boolean {
function toMatchConfig(config: StringOrMatchConfig): MatchConfig {
if (typeof config === "string") {
return {
any: [config]
};
}
return config;
}
function checkGlobs(
changedFiles: string[],
globs: StringOrMatchConfig[]
): boolean {
for (const glob of globs) {
core.debug(` checking pattern ${glob}`);
const matcher = new Minimatch(glob);
for (const changedFile of changedFiles) {
core.debug(` - ${changedFile}`);
if (matcher.match(changedFile)) {
core.debug(` ${changedFile} matches`);
return true;
}
core.debug(` checking pattern ${JSON.stringify(glob)}`);
const matchConfig = toMatchConfig(glob);
if (checkMatch(changedFiles, matchConfig)) {
return true;
}
}
return false;
}
// equivalent to "Array.some()" but expanded for debugging and clarity
function checkAny(changedFiles: string[], glob: string): boolean {
core.debug(` checking "any" pattern ${glob}`);
const matcher = new Minimatch(glob);
for (const changedFile of changedFiles) {
core.debug(` - ${changedFile}`);
if (matcher.match(changedFile)) {
core.debug(` ${changedFile} matches`);
return true;
}
}
return false;
}
// equivalent to "Array.every()" but expanded for debugging and clarity
function checkAll(changedFiles: string[], glob: string): boolean {
core.debug(` checking "all" pattern ${glob}`);
const matcher = new Minimatch(glob);
for (const changedFile of changedFiles) {
core.debug(` - ${changedFile}`);
if (!matcher.match(changedFile)) {
core.debug(` ${changedFile} did not match`);
return false;
}
}
return true;
}
function checkMatch(changedFiles: string[], matchConfig: MatchConfig): boolean {
if (matchConfig.all !== undefined) {
for (const glob of matchConfig.all) {
if (!checkAll(changedFiles, glob)) {
return false;
}
}
}
if (matchConfig.any !== undefined) {
for (const glob of matchConfig.any) {
if (!checkAny(changedFiles, glob)) {
return false;
}
}
}
return true;
}
async function addLabels(
client: github.GitHub,
prNumber: number,