Merge pull request #4 from joshdales/another-config-setup

Change the structure of the config
This commit is contained in:
Josh Dales
2023-03-27 17:25:27 -04:00
committed by GitHub
10 changed files with 606 additions and 347 deletions

View File

@@ -14,27 +14,36 @@ The key is the name of the label in your repository that you want to add (eg: "m
#### Match Object
The match object allows control over the matching options, you can specify the label to be applied based on the files that have changed or the name of the branch. For the changed files options you provide a [path glob](https://github.com/isaacs/minimatch#minimatch), and a regexp for the branch names.
The match object is defined as:
The match object allows control over the matching options, you can specify the label to be applied based on the files that have changed or the name of either the base branch or the head branch. For the changed files options you provide a [path glob](https://github.com/isaacs/minimatch#minimatch), and for the branches you provide a regexp to match against the branch name.
The base match object is defined as:
```yml
- changed-files:
- any: ['list', 'of', 'globs']
- all: ['list', 'of', 'globs']
- changed-files: ['list', 'of', 'globs']
- base-branch: ['list', 'of', 'regexps']
- head-branch: ['list', 'of', 'regexps']
```
One or all fields can be provided for fine-grained matching. Unlike the top-level list, the list of path globs provided to `any` and `all` must ALL match against a path for the label to be applied.
There are two top level keys of `any` and `all`, which both accept the same config options:
```yml
- any:
- changed-files: ['list', 'of', 'globs']
- base-branch: ['list', 'of', 'regexps']
- head-branch: ['list', 'of', 'regexps']
- all:
- changed-files: ['list', 'of', 'globs']
- base-branch: ['list', 'of', 'regexps']
- head-branch: ['list', 'of', 'regexps']
```
One or all fields can be provided for fine-grained matching.
The fields are defined as follows:
* `changed-files`
* `any`: match ALL globs against ANY changed path
* `all`: match ALL globs against ALL changed paths
* `all`: all of the provided options must match in order for the label to be applied
* `any`: if any of the provided options match then a label will be applied
* `base-branch`: match a regexp against the base branch name
* `changed-files`: match a glob against the changed paths
* `head-branch`: match a regexp against the head branch name
A simple path glob is the equivalent to `any: ['glob']`. More specifically, the following two configurations are equivalent:
If a base option is provided without a top-level key then it will default to `any`. More specifically, the following two configurations are equivalent:
```yml
label1:
- changed-files: example1/*
@@ -42,11 +51,11 @@ label1:
and
```yml
label1:
- changed-files:
- any: ['example1/*']
- any:
- changed-files: ['example1/*']
```
From a boolean logic perspective, top-level match objects are `OR`-ed together and individual match rules within an object are `AND`-ed. Combined with `!` negation, you can write complex matching rules.
From a boolean logic perspective, top-level match objects, and options within `all` are `AND`-ed together and individual match rules within the `any` object are `OR`-ed. If path globs are combined with `!` negation, you can write complex matching rules.
#### Basic Examples
@@ -96,9 +105,10 @@ source:
# Add 'frontend` label to any change to *.js files as long as the `main.js` hasn't changed
frontend:
- changed-files:
- any: ['src/**/*.js']
all: ['!src/main.js']
- any:
- changed-files: ['src/**/*.js']
- all:
- changed-files: ['!src/main.js']
# Add 'feature' label to any PR where the head branch name starts with `feature` or has a `feature` section in the name
feature:

View File

@@ -1,6 +1,7 @@
import {
getBranchName,
checkBranch,
checkAnyBranch,
checkAllBranch,
toBranchMatchConfig,
BranchMatchConfig
} from '../src/branch';
@@ -25,7 +26,7 @@ describe('getBranchName', () => {
});
});
describe('checkBranch', () => {
describe('checkAllBranch', () => {
beforeEach(() => {
github.context.payload.pull_request!.head = {
ref: 'test/feature/123'
@@ -38,37 +39,37 @@ describe('checkBranch', () => {
describe('when a single pattern is provided', () => {
describe('and the pattern matches the head branch', () => {
it('returns true', () => {
const result = checkBranch(['^test'], 'head');
const result = checkAllBranch(['^test'], 'head');
expect(result).toBe(true);
});
});
describe('and the pattern does not match the head branch', () => {
it('returns false', () => {
const result = checkBranch(['^feature/'], 'head');
const result = checkAllBranch(['^feature/'], 'head');
expect(result).toBe(false);
});
});
});
describe('when multiple patterns are provided', () => {
describe('and at least one pattern matches', () => {
it('returns true', () => {
const result = checkBranch(['^test/', '^feature/'], 'head');
expect(result).toBe(true);
describe('and not all patterns matched', () => {
it('returns false', () => {
const result = checkAllBranch(['^test/', '^feature/'], 'head');
expect(result).toBe(false);
});
});
describe('and all patterns match', () => {
it('returns true', () => {
const result = checkBranch(['^test/', '/feature/'], 'head');
const result = checkAllBranch(['^test/', '/feature/'], 'head');
expect(result).toBe(true);
});
});
describe('and no patterns match', () => {
it('returns false', () => {
const result = checkBranch(['^feature/', '/test$'], 'head');
const result = checkAllBranch(['^feature/', '/test$'], 'head');
expect(result).toBe(false);
});
});
@@ -77,7 +78,66 @@ describe('checkBranch', () => {
describe('when the branch to check is specified as the base branch', () => {
describe('and the pattern matches the base branch', () => {
it('returns true', () => {
const result = checkBranch(['^main$'], 'base');
const result = checkAllBranch(['^main$'], 'base');
expect(result).toBe(true);
});
});
});
});
describe('checkAnyBranch', () => {
beforeEach(() => {
github.context.payload.pull_request!.head = {
ref: 'test/feature/123'
};
github.context.payload.pull_request!.base = {
ref: 'main'
};
});
describe('when a single pattern is provided', () => {
describe('and the pattern matches the head branch', () => {
it('returns true', () => {
const result = checkAnyBranch(['^test'], 'head');
expect(result).toBe(true);
});
});
describe('and the pattern does not match the head branch', () => {
it('returns false', () => {
const result = checkAnyBranch(['^feature/'], 'head');
expect(result).toBe(false);
});
});
});
describe('when multiple patterns are provided', () => {
describe('and at least one pattern matches', () => {
it('returns true', () => {
const result = checkAnyBranch(['^test/', '^feature/'], 'head');
expect(result).toBe(true);
});
});
describe('and all patterns match', () => {
it('returns true', () => {
const result = checkAnyBranch(['^test/', '/feature/'], 'head');
expect(result).toBe(true);
});
});
describe('and no patterns match', () => {
it('returns false', () => {
const result = checkAnyBranch(['^feature/', '/test$'], 'head');
expect(result).toBe(false);
});
});
});
describe('when the branch to check is specified as the base branch', () => {
describe('and the pattern matches the base branch', () => {
it('returns true', () => {
const result = checkAnyBranch(['^main$'], 'base');
expect(result).toBe(true);
});
});

View File

@@ -1,21 +1,21 @@
import {
ChangedFilesMatchConfig,
checkAll,
checkAny,
checkAllChangedFiles,
checkAnyChangedFiles,
toChangedFilesMatchConfig
} from '../src/changedFiles';
jest.mock('@actions/core');
jest.mock('@actions/github');
describe('checkAll', () => {
describe('checkAllChangedFiles', () => {
const changedFiles = ['foo.txt', 'bar.txt'];
describe('when the globs match every file that has changed', () => {
const globs = ['*.txt'];
it('returns true', () => {
const result = checkAll(changedFiles, globs);
const result = checkAllChangedFiles(changedFiles, globs);
expect(result).toBe(true);
});
});
@@ -24,20 +24,20 @@ describe('checkAll', () => {
const globs = ['foo.txt'];
it('returns false', () => {
const result = checkAll(changedFiles, globs);
const result = checkAllChangedFiles(changedFiles, globs);
expect(result).toBe(false);
});
});
});
describe('checkAny', () => {
describe('checkAnyChangedFiles', () => {
const changedFiles = ['foo.txt', 'bar.txt'];
describe('when the globs match any of the files that have changed', () => {
const globs = ['foo.txt'];
it('returns true', () => {
const result = checkAny(changedFiles, globs);
const result = checkAnyChangedFiles(changedFiles, globs);
expect(result).toBe(true);
});
});
@@ -46,7 +46,7 @@ describe('checkAny', () => {
const globs = ['*.md'];
it('returns false', () => {
const result = checkAny(changedFiles, globs);
const result = checkAnyChangedFiles(changedFiles, globs);
expect(result).toBe(false);
});
});
@@ -63,72 +63,13 @@ describe('toChangedFilesMatchConfig', () => {
});
describe(`when there is a 'changed-files' key in the config`, () => {
describe(`and both 'any' and 'all' keys are present`, () => {
const config = {'changed-files': [{all: 'testing'}, {any: 'testing'}]};
describe('and the value is an array of strings', () => {
const config = {'changed-files': ['testing']};
it('sets both values in the config object', () => {
it('sets the value in the config object', () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
any: ['testing'],
all: ['testing']
}
});
});
});
describe(`and it contains a 'all' key`, () => {
describe('with a value of a string', () => {
const config = {'changed-files': [{all: 'testing'}]};
it('sets the value to be an array of strings in the config object', () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
all: ['testing']
}
});
});
});
describe('with a value of an array of strings', () => {
const config = {'changed-files': [{all: ['testing']}]};
it('sets the value in the config object', () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
all: ['testing']
}
});
});
});
});
describe(`and it contains a 'any' key`, () => {
describe('with a value of a string', () => {
const config = {'changed-files': [{any: 'testing'}]};
it('sets the value to be an array of strings on the config object', () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
any: ['testing']
}
});
});
});
describe('with a value of an array of strings', () => {
const config = {'changed-files': [{any: ['testing']}]};
it('sets the value in the config object', () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
any: ['testing']
}
});
changedFiles: ['testing']
});
});
});
@@ -136,31 +77,16 @@ describe('toChangedFilesMatchConfig', () => {
describe('and the value is a string', () => {
const config = {'changed-files': 'testing'};
it(`sets the string as an array under an 'any' key`, () => {
it(`sets the string as an array in the config object`, () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
any: ['testing']
}
changedFiles: ['testing']
});
});
});
describe('and the value is an array of strings', () => {
const config = {'changed-files': ['testing']};
it(`sets the array under an 'any' key`, () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
any: ['testing']
}
});
});
});
describe('but it has empty values', () => {
const config = {'changed-files': []};
describe('but the value is an empty string', () => {
const config = {'changed-files': ''};
it(`returns an empty object`, () => {
const result = toChangedFilesMatchConfig(config);
@@ -168,16 +94,12 @@ describe('toChangedFilesMatchConfig', () => {
});
});
describe('and there is an unknown value in the config', () => {
const config = {'changed-files': [{any: 'testing'}, {sneaky: 'test'}]};
describe('but the value is an empty array', () => {
const config = {'changed-files': []};
it(`does not set the value in the match config`, () => {
it(`returns an empty object`, () => {
const result = toChangedFilesMatchConfig(config);
expect(result).toEqual<ChangedFilesMatchConfig>({
changedFiles: {
any: ['testing']
}
});
expect(result).toEqual<ChangedFilesMatchConfig>({});
});
});
});

View File

@@ -0,0 +1,14 @@
label1:
- any:
- changed-files: ['glob']
- head-branch: ['regexp']
- base-branch: ['regexp']
- all:
- changed-files: ['glob']
- head-branch: ['regexp']
- base-branch: ['regexp']
label2:
- changed-files: ['glob']
- head-branch: ['regexp']
- base-branch: ['regexp']

View File

@@ -1,3 +1,2 @@
touched-a-pdf-file:
- changed-files:
- any: ['*.pdf']
- changed-files: ['*.pdf']

View File

@@ -1,6 +1,13 @@
import {checkMatchConfigs, MatchConfig, toMatchConfig} from '../src/labeler';
import {
checkMatchConfigs,
MatchConfig,
toMatchConfig,
getLabelConfigMapFromObject,
BaseMatchConfig
} from '../src/labeler';
import * as yaml from 'js-yaml';
import * as core from '@actions/core';
import * as fs from 'fs';
jest.mock('@actions/core');
@@ -10,18 +17,56 @@ beforeAll(() => {
});
});
const loadYaml = (filepath: string) => {
const loadedFile = fs.readFileSync(filepath);
const content = Buffer.from(loadedFile).toString();
return yaml.load(content);
};
describe('getLabelConfigMapFromObject', () => {
const yamlObject = loadYaml('__tests__/fixtures/all_options.yml');
const expected = new Map<string, MatchConfig[]>();
expected.set('label1', [
{
any: [
{changedFiles: ['glob']},
{baseBranch: undefined, headBranch: ['regexp']},
{baseBranch: ['regexp'], headBranch: undefined}
]
},
{
all: [
{changedFiles: ['glob']},
{baseBranch: undefined, headBranch: ['regexp']},
{baseBranch: ['regexp'], headBranch: undefined}
]
}
]);
expected.set('label2', [
{
any: [
{changedFiles: ['glob']},
{baseBranch: undefined, headBranch: ['regexp']},
{baseBranch: ['regexp'], headBranch: undefined}
]
}
]);
it('returns a MatchConfig', () => {
const result = getLabelConfigMapFromObject(yamlObject);
expect(result).toEqual(expected);
});
});
describe('toMatchConfig', () => {
describe('when all expected config options are present', () => {
const config = {
'changed-files': [{any: ['testing-any']}, {all: ['testing-all']}],
'changed-files': ['testing-files'],
'head-branch': ['testing-head'],
'base-branch': ['testing-base']
};
const expected: MatchConfig = {
changedFiles: {
all: ['testing-all'],
any: ['testing-any']
},
const expected: BaseMatchConfig = {
changedFiles: ['testing-files'],
headBranch: ['testing-head'],
baseBranch: ['testing-base']
};
@@ -43,19 +88,53 @@ describe('toMatchConfig', () => {
});
describe('checkMatchConfigs', () => {
const matchConfig: MatchConfig[] = [{changedFiles: {any: ['*.txt']}}];
describe('when a single match config is provided', () => {
const matchConfig: MatchConfig[] = [{any: [{changedFiles: ['*.txt']}]}];
it('returns true when our pattern does match changed files', () => {
const changedFiles = ['foo.txt', 'bar.txt'];
const result = checkMatchConfigs(changedFiles, matchConfig);
it('returns true when our pattern does match changed files', () => {
const changedFiles = ['foo.txt', 'bar.txt'];
const result = checkMatchConfigs(changedFiles, matchConfig);
expect(result).toBeTruthy();
expect(result).toBeTruthy();
});
it('returns false when our pattern does not match changed files', () => {
const changedFiles = ['foo.docx'];
const result = checkMatchConfigs(changedFiles, matchConfig);
expect(result).toBeFalsy();
});
it('returns true when either the branch or changed files patter matches', () => {
const matchConfig: MatchConfig[] = [
{any: [{changedFiles: ['*.txt']}, {headBranch: ['some-branch']}]}
];
const changedFiles = ['foo.txt', 'bar.txt'];
const result = checkMatchConfigs(changedFiles, matchConfig);
expect(result).toBe(true);
});
});
it('returns false when our pattern does not match changed files', () => {
const changedFiles = ['foo.docx'];
const result = checkMatchConfigs(changedFiles, matchConfig);
describe('when multiple MatchConfigs are supplied', () => {
const matchConfig: MatchConfig[] = [
{any: [{changedFiles: ['*.txt']}]},
{any: [{headBranch: ['some-branch']}]}
];
const changedFiles = ['foo.txt', 'bar.md'];
expect(result).toBeFalsy();
it('returns false when only one config matches', () => {
const result = checkMatchConfigs(changedFiles, matchConfig);
expect(result).toBe(false);
});
it('returns true when only both config matches', () => {
const matchConfig: MatchConfig[] = [
{any: [{changedFiles: ['*.txt']}]},
{any: [{headBranch: ['head-branch']}]}
];
const result = checkMatchConfigs(changedFiles, matchConfig);
expect(result).toBe(true);
});
});
});

267
dist/index.js vendored
View File

@@ -30,7 +30,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.checkBranch = exports.getBranchName = exports.toBranchMatchConfig = void 0;
exports.checkAllBranch = exports.checkAnyBranch = exports.getBranchName = exports.toBranchMatchConfig = void 0;
const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438));
function toBranchMatchConfig(config) {
@@ -63,31 +63,49 @@ function getBranchName(branchBase) {
}
}
exports.getBranchName = getBranchName;
function checkBranch(regexps, branchBase) {
function checkAnyBranch(regexps, branchBase) {
const branchName = getBranchName(branchBase);
if (!branchName) {
core.debug(` no branch name`);
core.debug(` no branch name`);
return false;
}
core.debug(` checking "branch" pattern against ${branchName}`);
core.debug(` checking "branch" pattern against ${branchName}`);
const matchers = regexps.map(regexp => new RegExp(regexp));
for (const matcher of matchers) {
if (matchBranchPattern(matcher, branchName)) {
core.debug(` "branch" patterns matched against ${branchName}`);
core.debug(` "branch" patterns matched against ${branchName}`);
return true;
}
}
core.debug(` "branch" patterns did not match against ${branchName}`);
core.debug(` "branch" patterns did not match against ${branchName}`);
return false;
}
exports.checkBranch = checkBranch;
exports.checkAnyBranch = checkAnyBranch;
function checkAllBranch(regexps, branchBase) {
const branchName = getBranchName(branchBase);
if (!branchName) {
core.debug(` no branch name`);
return false;
}
core.debug(` checking "branch" pattern against ${branchName}`);
const matchers = regexps.map(regexp => new RegExp(regexp));
for (const matcher of matchers) {
if (!matchBranchPattern(matcher, branchName)) {
core.debug(` "branch" patterns did not match against ${branchName}`);
return false;
}
}
core.debug(` "branch" patterns matched against ${branchName}`);
return true;
}
exports.checkAllBranch = checkAllBranch;
function matchBranchPattern(matcher, branchName) {
core.debug(` - ${matcher}`);
core.debug(` - ${matcher}`);
if (matcher.test(branchName)) {
core.debug(` "branch" pattern matched`);
core.debug(` "branch" pattern matched`);
return true;
}
core.debug(` ${matcher} did not match`);
core.debug(` ${matcher} did not match`);
return false;
}
@@ -132,7 +150,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.checkAll = exports.checkAny = exports.toChangedFilesMatchConfig = exports.getChangedFiles = void 0;
exports.checkAllChangedFiles = exports.checkAnyChangedFiles = exports.toChangedFilesMatchConfig = exports.getChangedFiles = void 0;
const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438));
const minimatch_1 = __nccwpck_require__(2002);
@@ -154,94 +172,68 @@ function getChangedFiles(client, prNumber) {
}
exports.getChangedFiles = getChangedFiles;
function toChangedFilesMatchConfig(config) {
if (!config['changed-files']) {
if (!config['changed-files'] || !config['changed-files'].length) {
return {};
}
const changedFilesConfig = config['changed-files'];
// If the value provided is a string or an array of strings then default to `any` matching
if (typeof changedFilesConfig === 'string') {
return {
changedFiles: {
any: [changedFilesConfig]
}
};
}
const changedFilesMatchConfig = {
changedFiles: {}
return {
changedFiles: Array.isArray(changedFilesConfig)
? changedFilesConfig
: [changedFilesConfig]
};
if (Array.isArray(changedFilesConfig)) {
if (changedFilesConfig.length &&
changedFilesConfig.every(entry => typeof entry === 'string')) {
changedFilesMatchConfig.changedFiles = {
any: changedFilesConfig
};
}
else {
// If it is not an array of strings then it should be array of further config options
// so assign them to our `changedFilesMatchConfig`
changedFilesMatchConfig.changedFiles = changedFilesConfig.reduce((updatedMatchConfig, configValue) => {
if (!configValue) {
return updatedMatchConfig;
}
Object.entries(configValue).forEach(([key, value]) => {
if (key === 'any' || key === 'all') {
updatedMatchConfig[key] = Array.isArray(value) ? value : [value];
}
});
return updatedMatchConfig;
}, {});
}
}
// If no items were added to `changedFiles` then return an empty object
if (!Object.keys(changedFilesMatchConfig.changedFiles).length) {
return {};
}
return changedFilesMatchConfig;
}
exports.toChangedFilesMatchConfig = toChangedFilesMatchConfig;
function printPattern(matcher) {
return (matcher.negate ? '!' : '') + matcher.pattern;
}
function isMatch(changedFile, matchers) {
function isAnyMatch(changedFile, matchers) {
core.debug(` matching patterns against file ${changedFile}`);
for (const matcher of matchers) {
core.debug(` - ${printPattern(matcher)}`);
if (!matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} did not match`);
return false;
}
}
core.debug(` all patterns matched`);
return true;
}
// equivalent to "Array.some()" but expanded for debugging and clarity
function checkAny(changedFiles, globs) {
const matchers = globs.map(g => new minimatch_1.Minimatch(g));
core.debug(` checking "any" patterns`);
for (const changedFile of changedFiles) {
if (isMatch(changedFile, matchers)) {
core.debug(` "any" patterns matched against ${changedFile}`);
core.debug(` - ${printPattern(matcher)}`);
if (matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} matched`);
return true;
}
}
core.debug(` "any" patterns did not match any files`);
core.debug(` no patterns matched`);
return false;
}
exports.checkAny = checkAny;
// equivalent to "Array.every()" but expanded for debugging and clarity
function checkAll(changedFiles, globs) {
const matchers = globs.map(g => new minimatch_1.Minimatch(g));
core.debug(` checking "all" patterns`);
for (const changedFile of changedFiles) {
if (!isMatch(changedFile, matchers)) {
core.debug(` "all" patterns did not match against ${changedFile}`);
function isAllMatch(changedFile, matchers) {
core.debug(` matching patterns against file ${changedFile}`);
for (const matcher of matchers) {
core.debug(` - ${printPattern(matcher)}`);
if (!matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} did not match`);
return false;
}
}
core.debug(` "all" patterns matched all files`);
core.debug(` all patterns matched`);
return true;
}
exports.checkAll = checkAll;
function checkAnyChangedFiles(changedFiles, globs) {
const matchers = globs.map(g => new minimatch_1.Minimatch(g));
for (const changedFile of changedFiles) {
if (isAnyMatch(changedFile, matchers)) {
core.debug(` "any" patterns matched against ${changedFile}`);
return true;
}
}
core.debug(` "any" patterns did not match any files`);
return false;
}
exports.checkAnyChangedFiles = checkAnyChangedFiles;
function checkAllChangedFiles(changedFiles, globs) {
const matchers = globs.map(g => new minimatch_1.Minimatch(g));
for (const changedFile of changedFiles) {
if (!isAllMatch(changedFile, matchers)) {
core.debug(` "all" patterns did not match against ${changedFile}`);
return false;
}
}
core.debug(` "all" patterns matched all files`);
return true;
}
exports.checkAllChangedFiles = checkAllChangedFiles;
/***/ }),
@@ -284,7 +276,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
});
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.checkMatchConfigs = exports.toMatchConfig = exports.run = void 0;
exports.checkAll = exports.checkAny = exports.checkMatchConfigs = exports.toMatchConfig = exports.getLabelConfigMapFromObject = exports.run = void 0;
const core = __importStar(__nccwpck_require__(2186));
const github = __importStar(__nccwpck_require__(5438));
const yaml = __importStar(__nccwpck_require__(1917));
@@ -370,11 +362,48 @@ function getLabelConfigMapFromObject(configObject) {
!configOptions.every(opts => typeof opts === 'object')) {
throw Error(`found unexpected type for label ${label} (should be array of config options)`);
}
const matchConfigs = configOptions.map(toMatchConfig);
labelMap.set(label, matchConfigs);
const matchConfigs = configOptions.reduce((updatedConfig, configValue) => {
if (!configValue) {
return updatedConfig;
}
Object.entries(configValue).forEach(([key, value]) => {
var _a;
// If the top level `any` or `all` keys are provided then set them, and convert their values to
// our config objects.
if (key === 'any' || key === 'all') {
if (Array.isArray(value)) {
const newConfigs = value.map(toMatchConfig);
updatedConfig.push({ [key]: newConfigs });
}
}
else if (
// These are the keys that we accept and know how to process
['changed-files', 'head-branch', 'base-branch'].includes(key)) {
const newMatchConfig = toMatchConfig({ [key]: value });
// Find or set the `any` key so that we can add these properties to that rule,
// Or create a new `any` key and add that to our array of configs.
const indexOfAny = updatedConfig.findIndex(mc => !!mc['any']);
if (indexOfAny >= 0) {
(_a = updatedConfig[indexOfAny].any) === null || _a === void 0 ? void 0 : _a.push(newMatchConfig);
}
else {
updatedConfig.push({ any: [newMatchConfig] });
}
}
else {
// Log the key that we don't know what to do with.
core.info(`An unknown config option was under ${label}: ${key}`);
}
});
return updatedConfig;
}, []);
if (matchConfigs.length) {
labelMap.set(label, matchConfigs);
}
}
return labelMap;
}
exports.getLabelConfigMapFromObject = getLabelConfigMapFromObject;
function toMatchConfig(config) {
const changedFilesConfig = (0, changedFiles_1.toChangedFilesMatchConfig)(config);
const branchConfig = (0, branch_1.toBranchMatchConfig)(config);
@@ -392,32 +421,78 @@ function checkMatchConfigs(changedFiles, matchConfigs) {
}
exports.checkMatchConfigs = checkMatchConfigs;
function checkMatch(changedFiles, matchConfig) {
var _a, _b;
if (!Object.keys(matchConfig).length) {
core.debug(` no "any" or "all" patterns to check`);
return false;
}
if ((_a = matchConfig.changedFiles) === null || _a === void 0 ? void 0 : _a.all) {
if (!(0, changedFiles_1.checkAll)(changedFiles, matchConfig.changedFiles.all)) {
if (matchConfig.all) {
if (!checkAll(matchConfig.all, changedFiles)) {
return false;
}
}
if ((_b = matchConfig.changedFiles) === null || _b === void 0 ? void 0 : _b.any) {
if (!(0, changedFiles_1.checkAny)(changedFiles, matchConfig.changedFiles.any)) {
return false;
}
}
if (matchConfig.headBranch) {
if (!(0, branch_1.checkBranch)(matchConfig.headBranch, 'head')) {
return false;
}
}
if (matchConfig.baseBranch) {
if (!(0, branch_1.checkBranch)(matchConfig.baseBranch, 'base')) {
if (matchConfig.any) {
if (!checkAny(matchConfig.any, changedFiles)) {
return false;
}
}
return true;
}
// equivalent to "Array.some()" but expanded for debugging and clarity
function checkAny(matchConfigs, changedFiles) {
core.debug(` checking "any" patterns`);
if (!Object.keys(matchConfigs).length) {
core.debug(` no "any" patterns to check`);
return false;
}
for (const matchConfig of matchConfigs) {
if (matchConfig.baseBranch) {
if ((0, branch_1.checkAnyBranch)(matchConfig.baseBranch, 'base')) {
return true;
}
}
if (matchConfig.changedFiles) {
if ((0, changedFiles_1.checkAnyChangedFiles)(changedFiles, matchConfig.changedFiles)) {
return true;
}
}
if (matchConfig.headBranch) {
if ((0, branch_1.checkAnyBranch)(matchConfig.headBranch, 'head')) {
return true;
}
}
}
core.debug(` "any" patterns did not match any configs`);
return false;
}
exports.checkAny = checkAny;
// equivalent to "Array.every()" but expanded for debugging and clarity
function checkAll(matchConfigs, changedFiles) {
core.debug(` checking "all" patterns`);
if (!Object.keys(matchConfigs).length) {
core.debug(` no "all" patterns to check`);
return false;
}
for (const matchConfig of matchConfigs) {
if (matchConfig.baseBranch) {
if (!(0, branch_1.checkAllBranch)(matchConfig.baseBranch, 'base')) {
return false;
}
}
if (matchConfig.changedFiles) {
if (!(0, changedFiles_1.checkAllChangedFiles)(changedFiles, matchConfig.changedFiles)) {
return false;
}
}
if (matchConfig.headBranch) {
if (!(0, branch_1.checkAllBranch)(matchConfig.headBranch, 'head')) {
return false;
}
}
}
core.debug(` "all" patterns matched all configs`);
return true;
}
exports.checkAll = checkAll;
function addLabels(client, prNumber, labels) {
return __awaiter(this, void 0, void 0, function* () {
yield client.rest.issues.addLabels({

View File

@@ -40,36 +40,59 @@ export function getBranchName(branchBase: BranchBase): string | undefined {
}
}
export function checkBranch(
export function checkAnyBranch(
regexps: string[],
branchBase: BranchBase
): boolean {
const branchName = getBranchName(branchBase);
if (!branchName) {
core.debug(` no branch name`);
core.debug(` no branch name`);
return false;
}
core.debug(` checking "branch" pattern against ${branchName}`);
core.debug(` checking "branch" pattern against ${branchName}`);
const matchers = regexps.map(regexp => new RegExp(regexp));
for (const matcher of matchers) {
if (matchBranchPattern(matcher, branchName)) {
core.debug(` "branch" patterns matched against ${branchName}`);
core.debug(` "branch" patterns matched against ${branchName}`);
return true;
}
}
core.debug(` "branch" patterns did not match against ${branchName}`);
core.debug(` "branch" patterns did not match against ${branchName}`);
return false;
}
export function checkAllBranch(
regexps: string[],
branchBase: BranchBase
): boolean {
const branchName = getBranchName(branchBase);
if (!branchName) {
core.debug(` no branch name`);
return false;
}
core.debug(` checking "branch" pattern against ${branchName}`);
const matchers = regexps.map(regexp => new RegExp(regexp));
for (const matcher of matchers) {
if (!matchBranchPattern(matcher, branchName)) {
core.debug(` "branch" patterns did not match against ${branchName}`);
return false;
}
}
core.debug(` "branch" patterns matched against ${branchName}`);
return true;
}
function matchBranchPattern(matcher: RegExp, branchName: string): boolean {
core.debug(` - ${matcher}`);
core.debug(` - ${matcher}`);
if (matcher.test(branchName)) {
core.debug(` "branch" pattern matched`);
core.debug(` "branch" pattern matched`);
return true;
}
core.debug(` ${matcher} did not match`);
core.debug(` ${matcher} did not match`);
return false;
}

View File

@@ -3,10 +3,7 @@ import * as github from '@actions/github';
import {Minimatch} from 'minimatch';
export interface ChangedFilesMatchConfig {
changedFiles?: {
all?: string[];
any?: string[];
};
changedFiles?: string[];
}
type ClientType = ReturnType<typeof github.getOctokit>;
@@ -35,106 +32,79 @@ export async function getChangedFiles(
export function toChangedFilesMatchConfig(
config: any
): ChangedFilesMatchConfig {
if (!config['changed-files']) {
if (!config['changed-files'] || !config['changed-files'].length) {
return {};
}
const changedFilesConfig = config['changed-files'];
// If the value provided is a string or an array of strings then default to `any` matching
if (typeof changedFilesConfig === 'string') {
return {
changedFiles: {
any: [changedFilesConfig]
}
};
}
const changedFilesMatchConfig = {
changedFiles: {}
return {
changedFiles: Array.isArray(changedFilesConfig)
? changedFilesConfig
: [changedFilesConfig]
};
if (Array.isArray(changedFilesConfig)) {
if (
changedFilesConfig.length &&
changedFilesConfig.every(entry => typeof entry === 'string')
) {
changedFilesMatchConfig.changedFiles = {
any: changedFilesConfig
};
} else {
// If it is not an array of strings then it should be array of further config options
// so assign them to our `changedFilesMatchConfig`
changedFilesMatchConfig.changedFiles = changedFilesConfig.reduce(
(updatedMatchConfig, configValue) => {
if (!configValue) {
return updatedMatchConfig;
}
Object.entries(configValue).forEach(([key, value]) => {
if (key === 'any' || key === 'all') {
updatedMatchConfig[key] = Array.isArray(value) ? value : [value];
}
});
return updatedMatchConfig;
},
{}
);
}
}
// If no items were added to `changedFiles` then return an empty object
if (!Object.keys(changedFilesMatchConfig.changedFiles).length) {
return {};
}
return changedFilesMatchConfig;
}
function printPattern(matcher: Minimatch): string {
return (matcher.negate ? '!' : '') + matcher.pattern;
}
function isMatch(changedFile: string, matchers: Minimatch[]): boolean {
function isAnyMatch(changedFile: string, matchers: Minimatch[]): boolean {
core.debug(` matching patterns against file ${changedFile}`);
for (const matcher of matchers) {
core.debug(` - ${printPattern(matcher)}`);
if (!matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} did not match`);
return false;
}
}
core.debug(` all patterns matched`);
return true;
}
// equivalent to "Array.some()" but expanded for debugging and clarity
export function checkAny(changedFiles: string[], globs: string[]): boolean {
const matchers = globs.map(g => new Minimatch(g));
core.debug(` checking "any" patterns`);
for (const changedFile of changedFiles) {
if (isMatch(changedFile, matchers)) {
core.debug(` "any" patterns matched against ${changedFile}`);
core.debug(` - ${printPattern(matcher)}`);
if (matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} matched`);
return true;
}
}
core.debug(` "any" patterns did not match any files`);
core.debug(` no patterns matched`);
return false;
}
// equivalent to "Array.every()" but expanded for debugging and clarity
export function checkAll(changedFiles: string[], globs: string[]): boolean {
const matchers = globs.map(g => new Minimatch(g));
core.debug(` checking "all" patterns`);
for (const changedFile of changedFiles) {
if (!isMatch(changedFile, matchers)) {
core.debug(` "all" patterns did not match against ${changedFile}`);
function isAllMatch(changedFile: string, matchers: Minimatch[]): boolean {
core.debug(` matching patterns against file ${changedFile}`);
for (const matcher of matchers) {
core.debug(` - ${printPattern(matcher)}`);
if (!matcher.match(changedFile)) {
core.debug(` ${printPattern(matcher)} did not match`);
return false;
}
}
core.debug(` "all" patterns matched all files`);
core.debug(` all patterns matched`);
return true;
}
export function checkAnyChangedFiles(
changedFiles: string[],
globs: string[]
): boolean {
const matchers = globs.map(g => new Minimatch(g));
for (const changedFile of changedFiles) {
if (isAnyMatch(changedFile, matchers)) {
core.debug(` "any" patterns matched against ${changedFile}`);
return true;
}
}
core.debug(` "any" patterns did not match any files`);
return false;
}
export function checkAllChangedFiles(
changedFiles: string[],
globs: string[]
): boolean {
const matchers = globs.map(g => new Minimatch(g));
for (const changedFile of changedFiles) {
if (!isAllMatch(changedFile, matchers)) {
core.debug(` "all" patterns did not match against ${changedFile}`);
return false;
}
}
core.debug(` "all" patterns matched all files`);
return true;
}

View File

@@ -6,12 +6,22 @@ import {
ChangedFilesMatchConfig,
getChangedFiles,
toChangedFilesMatchConfig,
checkAny,
checkAll
checkAllChangedFiles,
checkAnyChangedFiles
} from './changedFiles';
import {checkBranch, toBranchMatchConfig, BranchMatchConfig} from './branch';
import {
checkAnyBranch,
checkAllBranch,
toBranchMatchConfig,
BranchMatchConfig
} from './branch';
export type MatchConfig = ChangedFilesMatchConfig & BranchMatchConfig;
export type BaseMatchConfig = BranchMatchConfig & ChangedFilesMatchConfig;
export type MatchConfig = {
any?: BaseMatchConfig[];
all?: BaseMatchConfig[];
};
type ClientType = ReturnType<typeof github.getOctokit>;
@@ -105,7 +115,7 @@ async function fetchContent(
return Buffer.from(response.data.content, response.data.encoding).toString();
}
function getLabelConfigMapFromObject(
export function getLabelConfigMapFromObject(
configObject: any
): Map<string, MatchConfig[]> {
const labelMap: Map<string, MatchConfig[]> = new Map();
@@ -119,15 +129,53 @@ function getLabelConfigMapFromObject(
`found unexpected type for label ${label} (should be array of config options)`
);
}
const matchConfigs = configOptions.reduce<MatchConfig[]>(
(updatedConfig, configValue) => {
if (!configValue) {
return updatedConfig;
}
const matchConfigs = configOptions.map(toMatchConfig);
labelMap.set(label, matchConfigs);
Object.entries(configValue).forEach(([key, value]) => {
// If the top level `any` or `all` keys are provided then set them, and convert their values to
// our config objects.
if (key === 'any' || key === 'all') {
if (Array.isArray(value)) {
const newConfigs = value.map(toMatchConfig);
updatedConfig.push({[key]: newConfigs});
}
} else if (
// These are the keys that we accept and know how to process
['changed-files', 'head-branch', 'base-branch'].includes(key)
) {
const newMatchConfig = toMatchConfig({[key]: value});
// Find or set the `any` key so that we can add these properties to that rule,
// Or create a new `any` key and add that to our array of configs.
const indexOfAny = updatedConfig.findIndex(mc => !!mc['any']);
if (indexOfAny >= 0) {
updatedConfig[indexOfAny].any?.push(newMatchConfig);
} else {
updatedConfig.push({any: [newMatchConfig]});
}
} else {
// Log the key that we don't know what to do with.
core.info(`An unknown config option was under ${label}: ${key}`);
}
});
return updatedConfig;
},
[]
);
if (matchConfigs.length) {
labelMap.set(label, matchConfigs);
}
}
return labelMap;
}
export function toMatchConfig(config: any): MatchConfig {
export function toMatchConfig(config: any): BaseMatchConfig {
const changedFilesConfig = toChangedFilesMatchConfig(config);
const branchConfig = toBranchMatchConfig(config);
@@ -153,33 +201,92 @@ export function checkMatchConfigs(
function checkMatch(changedFiles: string[], matchConfig: MatchConfig): boolean {
if (!Object.keys(matchConfig).length) {
core.debug(` no "any" or "all" patterns to check`);
return false;
}
if (matchConfig.changedFiles?.all) {
if (!checkAll(changedFiles, matchConfig.changedFiles.all)) {
if (matchConfig.all) {
if (!checkAll(matchConfig.all, changedFiles)) {
return false;
}
}
if (matchConfig.changedFiles?.any) {
if (!checkAny(changedFiles, matchConfig.changedFiles.any)) {
if (matchConfig.any) {
if (!checkAny(matchConfig.any, changedFiles)) {
return false;
}
}
if (matchConfig.headBranch) {
if (!checkBranch(matchConfig.headBranch, 'head')) {
return false;
return true;
}
// equivalent to "Array.some()" but expanded for debugging and clarity
export function checkAny(
matchConfigs: BaseMatchConfig[],
changedFiles: string[]
): boolean {
core.debug(` checking "any" patterns`);
if (!Object.keys(matchConfigs).length) {
core.debug(` no "any" patterns to check`);
return false;
}
for (const matchConfig of matchConfigs) {
if (matchConfig.baseBranch) {
if (checkAnyBranch(matchConfig.baseBranch, 'base')) {
return true;
}
}
if (matchConfig.changedFiles) {
if (checkAnyChangedFiles(changedFiles, matchConfig.changedFiles)) {
return true;
}
}
if (matchConfig.headBranch) {
if (checkAnyBranch(matchConfig.headBranch, 'head')) {
return true;
}
}
}
if (matchConfig.baseBranch) {
if (!checkBranch(matchConfig.baseBranch, 'base')) {
return false;
core.debug(` "any" patterns did not match any configs`);
return false;
}
// equivalent to "Array.every()" but expanded for debugging and clarity
export function checkAll(
matchConfigs: BaseMatchConfig[],
changedFiles: string[]
): boolean {
core.debug(` checking "all" patterns`);
if (!Object.keys(matchConfigs).length) {
core.debug(` no "all" patterns to check`);
return false;
}
for (const matchConfig of matchConfigs) {
if (matchConfig.baseBranch) {
if (!checkAllBranch(matchConfig.baseBranch, 'base')) {
return false;
}
}
if (matchConfig.changedFiles) {
if (!checkAllChangedFiles(changedFiles, matchConfig.changedFiles)) {
return false;
}
}
if (matchConfig.headBranch) {
if (!checkAllBranch(matchConfig.headBranch, 'head')) {
return false;
}
}
}
core.debug(` "all" patterns matched all configs`);
return true;
}