From f4eefdc6597b5595d931f60cf9be372ea8584b52 Mon Sep 17 00:00:00 2001 From: MaksimZhukov <46996400+MaksimZhukov@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:43:39 +0200 Subject: [PATCH] [Beta] Implement the new structure of the match object for the changed-files section (#680) * Implement the new structure of the match object for changed files section * Replace inner loops with the find method --- __tests__/changedFiles.test.ts | 190 ++++++++++++++---- __tests__/fixtures/all_options.yml | 9 +- __tests__/fixtures/any_and_all.yml | 6 +- __tests__/fixtures/only_pdfs.yml | 3 +- __tests__/labeler.test.ts | 25 ++- dist/index.js | 185 +++++++++++++++--- src/changedFiles.ts | 303 ++++++++++++++++++++++++++--- src/labeler.ts | 6 + 8 files changed, 612 insertions(+), 115 deletions(-) diff --git a/__tests__/changedFiles.test.ts b/__tests__/changedFiles.test.ts index 0f611c5b9..965af565d 100644 --- a/__tests__/changedFiles.test.ts +++ b/__tests__/changedFiles.test.ts @@ -2,7 +2,11 @@ import { ChangedFilesMatchConfig, checkAllChangedFiles, checkAnyChangedFiles, - toChangedFilesMatchConfig + toChangedFilesMatchConfig, + checkIfAnyGlobMatchesAnyFile, + checkIfAllGlobsMatchAnyFile, + checkIfAnyGlobMatchesAllFiles, + checkIfAllGlobsMatchAllFiles } from '../src/changedFiles'; jest.mock('@actions/core'); @@ -11,20 +15,28 @@ jest.mock('@actions/github'); describe('checkAllChangedFiles', () => { const changedFiles = ['foo.txt', 'bar.txt']; - describe('when the globs match every file that has been changed', () => { - const globs = ['*.txt']; + describe('when all given glob pattern configs matched', () => { + const globPatternsConfigs = [ + {AnyGlobToAnyFile: ['foo.txt']}, + {AnyGlobToAllFiles: ['*.txt']}, + {AllGlobsToAllFiles: ['**']} + ]; it('returns true', () => { - const result = checkAllChangedFiles(changedFiles, globs); + const result = checkAllChangedFiles(changedFiles, globPatternsConfigs); expect(result).toBe(true); }); }); - describe(`when the globs don't match every file that has changed`, () => { - const globs = ['foo.txt']; + describe(`when some given glob pattern config did not match`, () => { + const globPatternsConfigs = [ + {AnyGlobToAnyFile: ['*.md']}, + {AnyGlobToAllFiles: ['*.txt']}, + {AllGlobsToAllFiles: ['**']} + ]; it('returns false', () => { - const result = checkAllChangedFiles(changedFiles, globs); + const result = checkAllChangedFiles(changedFiles, globPatternsConfigs); expect(result).toBe(false); }); }); @@ -33,20 +45,26 @@ describe('checkAllChangedFiles', () => { describe('checkAnyChangedFiles', () => { const changedFiles = ['foo.txt', 'bar.txt']; - describe('when any glob matches any of the files that have changed', () => { - const globs = ['*.txt', '*.md']; + describe('when any given glob pattern config matched', () => { + const globPatternsConfigs = [ + {AnyGlobToAnyFile: ['*.md']}, + {AnyGlobToAllFiles: ['*.txt']} + ]; it('returns true', () => { - const result = checkAnyChangedFiles(changedFiles, globs); + const result = checkAnyChangedFiles(changedFiles, globPatternsConfigs); expect(result).toBe(true); }); }); - describe('when none of the globs match any files that have changed', () => { - const globs = ['*.md']; + describe('when none of the given glob pattern configs matched', () => { + const globPatternsConfigs = [ + {AnyGlobToAnyFile: ['*.md']}, + {AnyGlobToAllFiles: ['!*.txt']} + ]; it('returns false', () => { - const result = checkAnyChangedFiles(changedFiles, globs); + const result = checkAnyChangedFiles(changedFiles, globPatternsConfigs); expect(result).toBe(false); }); }); @@ -63,44 +81,140 @@ describe('toChangedFilesMatchConfig', () => { }); describe(`when there is a 'changed-files' key in the config`, () => { - describe('and the value is an array of strings', () => { - const config = {'changed-files': ['testing']}; + describe('but the glob pattern config key is not provided', () => { + const config = {'changed-files': ['bar']}; + + it('throws the error', () => { + expect(() => { + toChangedFilesMatchConfig(config); + }).toThrow( + `The "changed-files" section must have a valid config structure. Please read the action documentation for more information` + ); + }); + }); - it('sets the value in the config object', () => { - const result = toChangedFilesMatchConfig(config); - expect(result).toEqual({ - changedFiles: ['testing'] - }); + describe('but the glob pattern config key is not valid', () => { + const config = {'changed-files': [{NotValidConfigKey: ['bar']}]}; + + it('throws the error', () => { + expect(() => { + toChangedFilesMatchConfig(config); + }).toThrow( + `Unknown config options were under "changed-files": NotValidConfigKey` + ); }); }); - describe('and the value is a string', () => { - const config = {'changed-files': 'testing'}; + describe('and the glob pattern config key is provided', () => { + describe('and the value is an array of strings', () => { + const config = {'changed-files': [{AnyGlobToAnyFile: ['testing']}]}; - it(`sets the string as an array in the config object`, () => { - const result = toChangedFilesMatchConfig(config); - expect(result).toEqual({ - changedFiles: ['testing'] + it('sets the value in the config object', () => { + const result = toChangedFilesMatchConfig(config); + expect(result).toEqual({ + changedFiles: [{AnyGlobToAnyFile: ['testing']}] + }); }); }); - }); - describe('but the value is an empty string', () => { - const config = {'changed-files': ''}; + describe('and the value is a string', () => { + const config = {'changed-files': [{AnyGlobToAnyFile: 'testing'}]}; - it(`returns an empty object`, () => { - const result = toChangedFilesMatchConfig(config); - expect(result).toEqual({}); + it(`sets the string as an array in the config object`, () => { + const result = toChangedFilesMatchConfig(config); + expect(result).toEqual({ + changedFiles: [{AnyGlobToAnyFile: ['testing']}] + }); + }); }); }); + }); +}); - describe('but the value is an empty array', () => { - const config = {'changed-files': []}; +describe('checkIfAnyGlobMatchesAnyFile', () => { + const changedFiles = ['foo.txt', 'bar.txt']; - it(`returns an empty object`, () => { - const result = toChangedFilesMatchConfig(config); - expect(result).toEqual({}); - }); + describe('when any given glob pattern matched any file', () => { + const globPatterns = ['*.md', 'foo.txt']; + + it('returns true', () => { + const result = checkIfAnyGlobMatchesAnyFile(changedFiles, globPatterns); + expect(result).toBe(true); + }); + }); + + describe('when none of the given glob pattern matched any file', () => { + const globPatterns = ['*.md', '!*.txt']; + + it('returns false', () => { + const result = checkIfAnyGlobMatchesAnyFile(changedFiles, globPatterns); + expect(result).toBe(false); + }); + }); +}); + +describe('checkIfAllGlobsMatchAnyFile', () => { + const changedFiles = ['foo.txt', 'bar.txt']; + + describe('when all given glob patterns matched any file', () => { + const globPatterns = ['**/bar.txt', 'bar.txt']; + + it('returns true', () => { + const result = checkIfAllGlobsMatchAnyFile(changedFiles, globPatterns); + expect(result).toBe(true); + }); + }); + + describe('when some of the given glob patterns did not match any file', () => { + const globPatterns = ['*.txt', '*.md']; + + it('returns false', () => { + const result = checkIfAllGlobsMatchAnyFile(changedFiles, globPatterns); + expect(result).toBe(false); + }); + }); +}); + +describe('checkIfAnyGlobMatchesAllFiles', () => { + const changedFiles = ['foo.txt', 'bar.txt']; + + describe('when any given glob pattern matched all files', () => { + const globPatterns = ['*.md', '*.txt']; + + it('returns true', () => { + const result = checkIfAnyGlobMatchesAllFiles(changedFiles, globPatterns); + expect(result).toBe(true); + }); + }); + + describe('when none of the given glob patterns matched all files', () => { + const globPatterns = ['*.md', 'bar.txt', 'foo.txt']; + + it('returns false', () => { + const result = checkIfAnyGlobMatchesAllFiles(changedFiles, globPatterns); + expect(result).toBe(false); + }); + }); +}); + +describe('checkIfAllGlobsMatchAllFiles', () => { + const changedFiles = ['foo.txt', 'bar.txt']; + + describe('when all given glob patterns matched all files', () => { + const globPatterns = ['*.txt', '**']; + + it('returns true', () => { + const result = checkIfAllGlobsMatchAllFiles(changedFiles, globPatterns); + expect(result).toBe(true); + }); + }); + + describe('when some of the given glob patterns did not match all files', () => { + const globPatterns = ['**', 'foo.txt']; + + it('returns false', () => { + const result = checkIfAllGlobsMatchAllFiles(changedFiles, globPatterns); + expect(result).toBe(false); }); }); }); diff --git a/__tests__/fixtures/all_options.yml b/__tests__/fixtures/all_options.yml index f4d5ecc5f..51f515017 100644 --- a/__tests__/fixtures/all_options.yml +++ b/__tests__/fixtures/all_options.yml @@ -1,14 +1,17 @@ label1: - any: - - changed-files: ['glob'] + - changed-files: + - AnyGlobToAnyFile: ['glob'] - head-branch: ['regexp'] - base-branch: ['regexp'] - all: - - changed-files: ['glob'] + - changed-files: + - AllGlobsToAllFiles: ['glob'] - head-branch: ['regexp'] - base-branch: ['regexp'] label2: - - changed-files: ['glob'] + - changed-files: + - AnyGlobToAnyFile: ['glob'] - head-branch: ['regexp'] - base-branch: ['regexp'] diff --git a/__tests__/fixtures/any_and_all.yml b/__tests__/fixtures/any_and_all.yml index 5b8901e5b..3ad0bcd9c 100644 --- a/__tests__/fixtures/any_and_all.yml +++ b/__tests__/fixtures/any_and_all.yml @@ -1,6 +1,8 @@ tests: - any: - head-branch: ['^tests/', '^test/'] - - changed-files: ['tests/**/*'] + - changed-files: + - AnyGlobToAnyFile: ['tests/**/*'] - all: - - changed-files: ['!tests/requirements.txt'] + - changed-files: + - AllGlobsToAllFiles: ['!tests/requirements.txt'] diff --git a/__tests__/fixtures/only_pdfs.yml b/__tests__/fixtures/only_pdfs.yml index a645acfc6..72c2eca16 100644 --- a/__tests__/fixtures/only_pdfs.yml +++ b/__tests__/fixtures/only_pdfs.yml @@ -1,2 +1,3 @@ touched-a-pdf-file: - - changed-files: ['*.pdf'] + - changed-files: + - AnyGlobToAnyFile: ['*.pdf'] diff --git a/__tests__/labeler.test.ts b/__tests__/labeler.test.ts index 0b9fb307b..d4890a3c3 100644 --- a/__tests__/labeler.test.ts +++ b/__tests__/labeler.test.ts @@ -29,14 +29,14 @@ describe('getLabelConfigMapFromObject', () => { expected.set('label1', [ { any: [ - {changedFiles: ['glob']}, + {changedFiles: [{AnyGlobToAnyFile: ['glob']}]}, {baseBranch: undefined, headBranch: ['regexp']}, {baseBranch: ['regexp'], headBranch: undefined} ] }, { all: [ - {changedFiles: ['glob']}, + {changedFiles: [{AllGlobsToAllFiles: ['glob']}]}, {baseBranch: undefined, headBranch: ['regexp']}, {baseBranch: ['regexp'], headBranch: undefined} ] @@ -45,7 +45,7 @@ describe('getLabelConfigMapFromObject', () => { expected.set('label2', [ { any: [ - {changedFiles: ['glob']}, + {changedFiles: [{AnyGlobToAnyFile: ['glob']}]}, {baseBranch: undefined, headBranch: ['regexp']}, {baseBranch: ['regexp'], headBranch: undefined} ] @@ -61,12 +61,12 @@ describe('getLabelConfigMapFromObject', () => { describe('toMatchConfig', () => { describe('when all expected config options are present', () => { const config = { - 'changed-files': ['testing-files'], + 'changed-files': [{AnyGlobToAnyFile: ['testing-files']}], 'head-branch': ['testing-head'], 'base-branch': ['testing-base'] }; const expected: BaseMatchConfig = { - changedFiles: ['testing-files'], + changedFiles: [{AnyGlobToAnyFile: ['testing-files']}], headBranch: ['testing-head'], baseBranch: ['testing-base'] }; @@ -89,7 +89,9 @@ describe('toMatchConfig', () => { describe('checkMatchConfigs', () => { describe('when a single match config is provided', () => { - const matchConfig: MatchConfig[] = [{any: [{changedFiles: ['*.txt']}]}]; + const matchConfig: MatchConfig[] = [ + {any: [{changedFiles: [{AnyGlobToAnyFile: ['*.txt']}]}]} + ]; it('returns true when our pattern does match changed files', () => { const changedFiles = ['foo.txt', 'bar.txt']; @@ -107,7 +109,12 @@ describe('checkMatchConfigs', () => { it('returns true when either the branch or changed files patter matches', () => { const matchConfig: MatchConfig[] = [ - {any: [{changedFiles: ['*.txt']}, {headBranch: ['some-branch']}]} + { + any: [ + {changedFiles: [{AnyGlobToAnyFile: ['*.txt']}]}, + {headBranch: ['some-branch']} + ] + } ]; const changedFiles = ['foo.txt', 'bar.txt']; @@ -118,7 +125,7 @@ describe('checkMatchConfigs', () => { describe('when multiple MatchConfigs are supplied', () => { const matchConfig: MatchConfig[] = [ - {any: [{changedFiles: ['*.txt']}]}, + {any: [{changedFiles: [{AnyGlobToAnyFile: ['*.txt']}]}]}, {any: [{headBranch: ['some-branch']}]} ]; const changedFiles = ['foo.txt', 'bar.md']; @@ -130,7 +137,7 @@ describe('checkMatchConfigs', () => { it('returns true when only both config matches', () => { const matchConfig: MatchConfig[] = [ - {any: [{changedFiles: ['*.txt']}]}, + {any: [{changedFiles: [{AnyGlobToAnyFile: ['*.txt']}]}]}, {any: [{headBranch: ['head-branch']}]} ]; const result = checkMatchConfigs(changedFiles, matchConfig); diff --git a/dist/index.js b/dist/index.js index 71af26a98..c2e479cb2 100644 --- a/dist/index.js +++ b/dist/index.js @@ -150,10 +150,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge }); }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.checkAllChangedFiles = exports.checkAnyChangedFiles = exports.toChangedFilesMatchConfig = exports.getChangedFiles = void 0; +exports.checkIfAllGlobsMatchAllFiles = exports.checkIfAnyGlobMatchesAllFiles = exports.checkIfAllGlobsMatchAnyFile = exports.checkIfAnyGlobMatchesAnyFile = 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); +const ALLOWED_FILES_CONFIG_KEYS = [ + 'AnyGlobToAnyFile', + 'AnyGlobToAllFiles', + 'AllGlobsToAnyFile', + 'AllGlobsToAllFiles' +]; function getChangedFiles(client, prNumber) { return __awaiter(this, void 0, void 0, function* () { const listFilesOptions = client.rest.pulls.listFiles.endpoint.merge({ @@ -175,65 +181,174 @@ function toChangedFilesMatchConfig(config) { if (!config['changed-files'] || !config['changed-files'].length) { return {}; } - const changedFilesConfig = config['changed-files']; + const changedFilesConfigs = Array.isArray(config['changed-files']) + ? config['changed-files'] + : [config['changed-files']]; + const validChangedFilesConfigs = []; + changedFilesConfigs.forEach(changedFilesConfig => { + if (!isObject(changedFilesConfig)) { + throw new Error(`The "changed-files" section must have a valid config structure. Please read the action documentation for more information`); + } + const changedFilesConfigKeys = Object.keys(changedFilesConfig); + const invalidKeys = changedFilesConfigKeys.filter(key => !ALLOWED_FILES_CONFIG_KEYS.includes(key)); + if (invalidKeys.length) { + throw new Error(`Unknown config options were under "changed-files": ${invalidKeys.join(', ')}`); + } + changedFilesConfigKeys.forEach(key => { + validChangedFilesConfigs.push({ + [key]: Array.isArray(changedFilesConfig[key]) + ? changedFilesConfig[key] + : [changedFilesConfig[key]] + }); + }); + }); return { - changedFiles: Array.isArray(changedFilesConfig) - ? changedFilesConfig - : [changedFilesConfig] + changedFiles: validChangedFilesConfigs }; } exports.toChangedFilesMatchConfig = toChangedFilesMatchConfig; +function isObject(obj) { + return obj !== null && typeof obj === 'object' && !Array.isArray(obj); +} function printPattern(matcher) { return (matcher.negate ? '!' : '') + matcher.pattern; } -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)} matched`); - return true; +function checkAnyChangedFiles(changedFiles, globPatternsConfigs) { + core.debug(` checking "changed-files" patterns`); + for (const globPatternsConfig of globPatternsConfigs) { + if (globPatternsConfig.AnyGlobToAnyFile) { + if (checkIfAnyGlobMatchesAnyFile(changedFiles, globPatternsConfig.AnyGlobToAnyFile)) { + core.debug(` "changed-files" matched`); + return true; + } + } + if (globPatternsConfig.AnyGlobToAllFiles) { + if (checkIfAnyGlobMatchesAllFiles(changedFiles, globPatternsConfig.AnyGlobToAllFiles)) { + core.debug(` "changed-files" matched`); + return true; + } + } + if (globPatternsConfig.AllGlobsToAnyFile) { + if (checkIfAllGlobsMatchAnyFile(changedFiles, globPatternsConfig.AllGlobsToAnyFile)) { + core.debug(` "changed-files" matched`); + return true; + } + } + if (globPatternsConfig.AllGlobsToAllFiles) { + if (checkIfAllGlobsMatchAllFiles(changedFiles, globPatternsConfig.AllGlobsToAllFiles)) { + core.debug(` "changed-files" matched`); + return true; + } } } - core.debug(` no patterns matched`); + core.debug(` "changed-files" did not match`); return false; } -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; +exports.checkAnyChangedFiles = checkAnyChangedFiles; +function checkAllChangedFiles(changedFiles, globPatternsConfigs) { + core.debug(` checking "changed-files" patterns`); + for (const globPatternsConfig of globPatternsConfigs) { + if (globPatternsConfig.AnyGlobToAnyFile) { + if (!checkIfAnyGlobMatchesAnyFile(changedFiles, globPatternsConfig.AnyGlobToAnyFile)) { + core.debug(` "changed-files" did not match`); + return false; + } + } + if (globPatternsConfig.AnyGlobToAllFiles) { + if (!checkIfAnyGlobMatchesAllFiles(changedFiles, globPatternsConfig.AnyGlobToAllFiles)) { + core.debug(` "changed-files" did not match`); + return false; + } + } + if (globPatternsConfig.AllGlobsToAnyFile) { + if (!checkIfAllGlobsMatchAnyFile(changedFiles, globPatternsConfig.AllGlobsToAnyFile)) { + core.debug(` "changed-files" did not match`); + return false; + } + } + if (globPatternsConfig.AllGlobsToAllFiles) { + if (!checkIfAllGlobsMatchAllFiles(changedFiles, globPatternsConfig.AllGlobsToAllFiles)) { + core.debug(` "changed-files" did not match`); + return false; + } } } - core.debug(` all patterns matched`); + core.debug(` "changed-files" patterns matched`); return true; } -function checkAnyChangedFiles(changedFiles, globs) { +exports.checkAllChangedFiles = checkAllChangedFiles; +function checkIfAnyGlobMatchesAnyFile(changedFiles, globs) { + core.debug(` checking "AnyGlobToAnyFile" config patterns`); 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}`); + for (const matcher of matchers) { + const matchedFile = changedFiles.find(changedFile => { + core.debug(` checking "${printPattern(matcher)}" pattern against ${changedFile}`); + return matcher.match(changedFile); + }); + if (matchedFile) { + core.debug(` "${printPattern(matcher)}" pattern matched ${matchedFile}`); return true; } } - core.debug(` "any" patterns did not match any files`); + core.debug(` none of the patterns matched any of the files`); return false; } -exports.checkAnyChangedFiles = checkAnyChangedFiles; -function checkAllChangedFiles(changedFiles, globs) { +exports.checkIfAnyGlobMatchesAnyFile = checkIfAnyGlobMatchesAnyFile; +function checkIfAllGlobsMatchAnyFile(changedFiles, globs) { + core.debug(` checking "AllGlobsToAnyFile" config patterns`); + const matchers = globs.map(g => new minimatch_1.Minimatch(g)); + for (const changedFile of changedFiles) { + const mismatchedGlob = matchers.find(matcher => { + core.debug(` checking "${printPattern(matcher)}" pattern against ${changedFile}`); + return !matcher.match(changedFile); + }); + if (mismatchedGlob) { + core.debug(` "${printPattern(mismatchedGlob)}" pattern did not match ${changedFile}`); + continue; + } + core.debug(` all patterns matched ${changedFile}`); + return true; + } + core.debug(` none of the files matched all patterns`); + return false; +} +exports.checkIfAllGlobsMatchAnyFile = checkIfAllGlobsMatchAnyFile; +function checkIfAnyGlobMatchesAllFiles(changedFiles, globs) { + core.debug(` checking "AnyGlobToAllFiles" config patterns`); + const matchers = globs.map(g => new minimatch_1.Minimatch(g)); + for (const matcher of matchers) { + const mismatchedFile = changedFiles.find(changedFile => { + core.debug(` checking "${printPattern(matcher)}" pattern against ${changedFile}`); + return !matcher.match(changedFile); + }); + if (mismatchedFile) { + core.debug(` "${printPattern(matcher)}" pattern did not match ${mismatchedFile}`); + continue; + } + core.debug(` "${printPattern(matcher)}" pattern matched all files`); + return true; + } + core.debug(` none of the patterns matched all files`); + return false; +} +exports.checkIfAnyGlobMatchesAllFiles = checkIfAnyGlobMatchesAllFiles; +function checkIfAllGlobsMatchAllFiles(changedFiles, globs) { + core.debug(` checking "AllGlobsToAllFiles" config patterns`); 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}`); + const mismatchedGlob = matchers.find(matcher => { + core.debug(` checking "${printPattern(matcher)}" pattern against ${changedFile}`); + return !matcher.match(changedFile); + }); + if (mismatchedGlob) { + core.debug(` "${printPattern(mismatchedGlob)}" pattern did not match ${changedFile}`); return false; } } - core.debug(` "all" patterns matched all files`); + core.debug(` all patterns matched all files`); return true; } -exports.checkAllChangedFiles = checkAllChangedFiles; +exports.checkIfAllGlobsMatchAllFiles = checkIfAllGlobsMatchAllFiles; /***/ }), @@ -447,16 +562,19 @@ function checkAny(matchConfigs, changedFiles) { for (const matchConfig of matchConfigs) { if (matchConfig.baseBranch) { if ((0, branch_1.checkAnyBranch)(matchConfig.baseBranch, 'base')) { + core.debug(` "any" patterns matched`); return true; } } if (matchConfig.changedFiles) { if ((0, changedFiles_1.checkAnyChangedFiles)(changedFiles, matchConfig.changedFiles)) { + core.debug(` "any" patterns matched`); return true; } } if (matchConfig.headBranch) { if ((0, branch_1.checkAnyBranch)(matchConfig.headBranch, 'head')) { + core.debug(` "any" patterns matched`); return true; } } @@ -476,6 +594,7 @@ function checkAll(matchConfigs, changedFiles) { for (const matchConfig of matchConfigs) { if (matchConfig.baseBranch) { if (!(0, branch_1.checkAllBranch)(matchConfig.baseBranch, 'base')) { + core.debug(` "all" patterns did not match`); return false; } } @@ -485,11 +604,13 @@ function checkAll(matchConfigs, changedFiles) { return false; } if (!(0, changedFiles_1.checkAllChangedFiles)(changedFiles, matchConfig.changedFiles)) { + core.debug(` "all" patterns did not match`); return false; } } if (matchConfig.headBranch) { if (!(0, branch_1.checkAllBranch)(matchConfig.headBranch, 'head')) { + core.debug(` "all" patterns did not match`); return false; } } diff --git a/src/changedFiles.ts b/src/changedFiles.ts index cd83f3d62..ac9470d6a 100644 --- a/src/changedFiles.ts +++ b/src/changedFiles.ts @@ -3,11 +3,25 @@ import * as github from '@actions/github'; import {Minimatch} from 'minimatch'; export interface ChangedFilesMatchConfig { - changedFiles?: string[]; + changedFiles?: ChangedFilesGlobPatternsConfig[]; +} + +interface ChangedFilesGlobPatternsConfig { + AnyGlobToAnyFile?: string[]; + AnyGlobToAllFiles?: string[]; + AllGlobsToAnyFile?: string[]; + AllGlobsToAllFiles?: string[]; } type ClientType = ReturnType; +const ALLOWED_FILES_CONFIG_KEYS = [ + 'AnyGlobToAnyFile', + 'AnyGlobToAllFiles', + 'AllGlobsToAnyFile', + 'AllGlobsToAllFiles' +]; + export async function getChangedFiles( client: ClientType, prNumber: number @@ -35,76 +49,305 @@ export function toChangedFilesMatchConfig( if (!config['changed-files'] || !config['changed-files'].length) { return {}; } + const changedFilesConfigs = Array.isArray(config['changed-files']) + ? config['changed-files'] + : [config['changed-files']]; + + const validChangedFilesConfigs: ChangedFilesGlobPatternsConfig[] = []; + + changedFilesConfigs.forEach(changedFilesConfig => { + if (!isObject(changedFilesConfig)) { + throw new Error( + `The "changed-files" section must have a valid config structure. Please read the action documentation for more information` + ); + } + + const changedFilesConfigKeys = Object.keys(changedFilesConfig); + const invalidKeys = changedFilesConfigKeys.filter( + key => !ALLOWED_FILES_CONFIG_KEYS.includes(key) + ); - const changedFilesConfig = config['changed-files']; + if (invalidKeys.length) { + throw new Error( + `Unknown config options were under "changed-files": ${invalidKeys.join( + ', ' + )}` + ); + } + + changedFilesConfigKeys.forEach(key => { + validChangedFilesConfigs.push({ + [key]: Array.isArray(changedFilesConfig[key]) + ? changedFilesConfig[key] + : [changedFilesConfig[key]] + }); + }); + }); return { - changedFiles: Array.isArray(changedFilesConfig) - ? changedFilesConfig - : [changedFilesConfig] + changedFiles: validChangedFilesConfigs }; } +function isObject(obj: unknown): obj is object { + return obj !== null && typeof obj === 'object' && !Array.isArray(obj); +} + function printPattern(matcher: Minimatch): string { return (matcher.negate ? '!' : '') + matcher.pattern; } -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)} matched`); - return true; +export function checkAnyChangedFiles( + changedFiles: string[], + globPatternsConfigs: ChangedFilesGlobPatternsConfig[] +): boolean { + core.debug(` checking "changed-files" patterns`); + + for (const globPatternsConfig of globPatternsConfigs) { + if (globPatternsConfig.AnyGlobToAnyFile) { + if ( + checkIfAnyGlobMatchesAnyFile( + changedFiles, + globPatternsConfig.AnyGlobToAnyFile + ) + ) { + core.debug(` "changed-files" matched`); + return true; + } + } + + if (globPatternsConfig.AnyGlobToAllFiles) { + if ( + checkIfAnyGlobMatchesAllFiles( + changedFiles, + globPatternsConfig.AnyGlobToAllFiles + ) + ) { + core.debug(` "changed-files" matched`); + return true; + } + } + + if (globPatternsConfig.AllGlobsToAnyFile) { + if ( + checkIfAllGlobsMatchAnyFile( + changedFiles, + globPatternsConfig.AllGlobsToAnyFile + ) + ) { + core.debug(` "changed-files" matched`); + return true; + } + } + + if (globPatternsConfig.AllGlobsToAllFiles) { + if ( + checkIfAllGlobsMatchAllFiles( + changedFiles, + globPatternsConfig.AllGlobsToAllFiles + ) + ) { + core.debug(` "changed-files" matched`); + return true; + } } } - core.debug(` no patterns matched`); + core.debug(` "changed-files" did not match`); return false; } -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; +export function checkAllChangedFiles( + changedFiles: string[], + globPatternsConfigs: ChangedFilesGlobPatternsConfig[] +): boolean { + core.debug(` checking "changed-files" patterns`); + + for (const globPatternsConfig of globPatternsConfigs) { + if (globPatternsConfig.AnyGlobToAnyFile) { + if ( + !checkIfAnyGlobMatchesAnyFile( + changedFiles, + globPatternsConfig.AnyGlobToAnyFile + ) + ) { + core.debug(` "changed-files" did not match`); + return false; + } + } + + if (globPatternsConfig.AnyGlobToAllFiles) { + if ( + !checkIfAnyGlobMatchesAllFiles( + changedFiles, + globPatternsConfig.AnyGlobToAllFiles + ) + ) { + core.debug(` "changed-files" did not match`); + return false; + } + } + + if (globPatternsConfig.AllGlobsToAnyFile) { + if ( + !checkIfAllGlobsMatchAnyFile( + changedFiles, + globPatternsConfig.AllGlobsToAnyFile + ) + ) { + core.debug(` "changed-files" did not match`); + return false; + } + } + + if (globPatternsConfig.AllGlobsToAllFiles) { + if ( + !checkIfAllGlobsMatchAllFiles( + changedFiles, + globPatternsConfig.AllGlobsToAllFiles + ) + ) { + core.debug(` "changed-files" did not match`); + return false; + } } } - core.debug(` all patterns matched`); + core.debug(` "changed-files" patterns matched`); return true; } -export function checkAnyChangedFiles( +export function checkIfAnyGlobMatchesAnyFile( changedFiles: string[], globs: string[] ): boolean { + core.debug(` checking "AnyGlobToAnyFile" config patterns`); const matchers = globs.map(g => new Minimatch(g)); - for (const changedFile of changedFiles) { - if (isAnyMatch(changedFile, matchers)) { - core.debug(` "any" patterns matched against ${changedFile}`); + + for (const matcher of matchers) { + const matchedFile = changedFiles.find(changedFile => { + core.debug( + ` checking "${printPattern( + matcher + )}" pattern against ${changedFile}` + ); + + return matcher.match(changedFile); + }); + + if (matchedFile) { + core.debug( + ` "${printPattern(matcher)}" pattern matched ${matchedFile}` + ); return true; } } - core.debug(` "any" patterns did not match any files`); + core.debug(` none of the patterns matched any of the files`); return false; } -export function checkAllChangedFiles( +export function checkIfAllGlobsMatchAnyFile( changedFiles: string[], globs: string[] ): boolean { + core.debug(` checking "AllGlobsToAnyFile" config patterns`); 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}`); + const mismatchedGlob = matchers.find(matcher => { + core.debug( + ` checking "${printPattern( + matcher + )}" pattern against ${changedFile}` + ); + + return !matcher.match(changedFile); + }); + + if (mismatchedGlob) { + core.debug( + ` "${printPattern( + mismatchedGlob + )}" pattern did not match ${changedFile}` + ); + + continue; + } + + core.debug(` all patterns matched ${changedFile}`); + return true; + } + + core.debug(` none of the files matched all patterns`); + return false; +} + +export function checkIfAnyGlobMatchesAllFiles( + changedFiles: string[], + globs: string[] +): boolean { + core.debug(` checking "AnyGlobToAllFiles" config patterns`); + const matchers = globs.map(g => new Minimatch(g)); + + for (const matcher of matchers) { + const mismatchedFile = changedFiles.find(changedFile => { + core.debug( + ` checking "${printPattern( + matcher + )}" pattern against ${changedFile}` + ); + + return !matcher.match(changedFile); + }); + + if (mismatchedFile) { + core.debug( + ` "${printPattern( + matcher + )}" pattern did not match ${mismatchedFile}` + ); + + continue; + } + + core.debug(` "${printPattern(matcher)}" pattern matched all files`); + return true; + } + + core.debug(` none of the patterns matched all files`); + return false; +} + +export function checkIfAllGlobsMatchAllFiles( + changedFiles: string[], + globs: string[] +): boolean { + core.debug(` checking "AllGlobsToAllFiles" config patterns`); + const matchers = globs.map(g => new Minimatch(g)); + + for (const changedFile of changedFiles) { + const mismatchedGlob = matchers.find(matcher => { + core.debug( + ` checking "${printPattern( + matcher + )}" pattern against ${changedFile}` + ); + + return !matcher.match(changedFile); + }); + + if (mismatchedGlob) { + core.debug( + ` "${printPattern( + mismatchedGlob + )}" pattern did not match ${changedFile}` + ); + return false; } } - core.debug(` "all" patterns matched all files`); + core.debug(` all patterns matched all files`); return true; } diff --git a/src/labeler.ts b/src/labeler.ts index ca6a113b8..2a759cf59 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -236,18 +236,21 @@ export function checkAny( for (const matchConfig of matchConfigs) { if (matchConfig.baseBranch) { if (checkAnyBranch(matchConfig.baseBranch, 'base')) { + core.debug(` "any" patterns matched`); return true; } } if (matchConfig.changedFiles) { if (checkAnyChangedFiles(changedFiles, matchConfig.changedFiles)) { + core.debug(` "any" patterns matched`); return true; } } if (matchConfig.headBranch) { if (checkAnyBranch(matchConfig.headBranch, 'head')) { + core.debug(` "any" patterns matched`); return true; } } @@ -274,6 +277,7 @@ export function checkAll( for (const matchConfig of matchConfigs) { if (matchConfig.baseBranch) { if (!checkAllBranch(matchConfig.baseBranch, 'base')) { + core.debug(` "all" patterns did not match`); return false; } } @@ -285,12 +289,14 @@ export function checkAll( } if (!checkAllChangedFiles(changedFiles, matchConfig.changedFiles)) { + core.debug(` "all" patterns did not match`); return false; } } if (matchConfig.headBranch) { if (!checkAllBranch(matchConfig.headBranch, 'head')) { + core.debug(` "all" patterns did not match`); return false; } }