diff --git a/README.md b/README.md index 52fcd7a4f..9ccc53125 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,11 @@ label1: 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. +> ⚠️ This action uses [minimatch](https://www.npmjs.com/package/minimatch) to apply glob patterns. +> For historical reasons, paths starting with dot (e.g. `.github`) are not matched by default. +> You need to set `dot: true` to change this behavior. +> See [Inputs](#inputs) table below for details. + #### Basic Examples ```yml @@ -108,8 +113,24 @@ Various inputs are defined in [`action.yml`](action.yml) to let you configure th | - | - | - | | `repo-token` | Token to use to authorize label changes. Typically the GITHUB_TOKEN secret, with `contents:read` and `pull-requests:write` access | `github.token` | | `configuration-path` | The path to the label configuration file | `.github/labeler.yml` | -| `sync-labels` | Whether or not to remove labels when matching files are reverted or no longer changed by the PR | `false`| +| `sync-labels` | Whether or not to remove labels when matching files are reverted or no longer changed by the PR | `false` | +| `dot` | Whether or not to auto-include paths starting with dot (e.g. `.github`) | `false` | + +When `dot` is disabled and you want to include _all_ files in a folder: + +```yml +label1: +- path/to/folder/**/* +- path/to/folder/**/.* +``` + +If `dot` is enabled: + +```yml +label1: +- path/to/folder/** +``` -# Contributions +## Contributions Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md). diff --git a/__tests__/labeler.test.ts b/__tests__/labeler.test.ts index d684d28bd..946d9ce18 100644 --- a/__tests__/labeler.test.ts +++ b/__tests__/labeler.test.ts @@ -15,15 +15,29 @@ const matchConfig = [{any: ['*.txt']}]; describe('checkGlobs', () => { it('returns true when our pattern does match changed files', () => { const changedFiles = ['foo.txt', 'bar.txt']; - const result = checkGlobs(changedFiles, matchConfig); + const result = checkGlobs(changedFiles, matchConfig, false); expect(result).toBeTruthy(); }); it('returns false when our pattern does not match changed files', () => { const changedFiles = ['foo.docx']; - const result = checkGlobs(changedFiles, matchConfig); + const result = checkGlobs(changedFiles, matchConfig, false); expect(result).toBeFalsy(); }); + + it('returns false for a file starting with dot if `dot` option is false', () => { + const changedFiles = ['.foo.txt']; + const result = checkGlobs(changedFiles, matchConfig, false); + + expect(result).toBeFalsy(); + }); + + it('returns true for a file starting with dot if `dot` option is true', () => { + const changedFiles = ['.foo.txt']; + const result = checkGlobs(changedFiles, matchConfig, true); + + expect(result).toBeTruthy(); + }); }); diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index ab62eae37..c4b77bc99 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -18,10 +18,27 @@ const yamlFixtures = { 'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml') }; +const configureInput = ( + mockInput: Partial<{ + 'repo-token': string; + 'configuration-path': string; + 'sync-labels': boolean; + dot: boolean; + }> +) => { + jest + .spyOn(core, 'getInput') + .mockImplementation((name: string, ...opts) => mockInput[name]); + jest + .spyOn(core, 'getBooleanInput') + .mockImplementation((name: string, ...opts) => mockInput[name]); +}; + afterAll(() => jest.restoreAllMocks()); describe('run', () => { - it('adds labels to PRs that match our glob patterns', async () => { + it('(with dot: false) adds labels to PRs that match our glob patterns', async () => { + configureInput({}); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('foo.pdf'); @@ -37,7 +54,36 @@ describe('run', () => { }); }); - it('does not add labels to PRs that do not match our glob patterns', async () => { + it('(with dot: true) adds labels to PRs that match our glob patterns', async () => { + configureInput({dot: true}); + usingLabelerConfigYaml('only_pdfs.yml'); + mockGitHubResponseChangedFiles('.foo.pdf'); + + await run(); + + expect(removeLabelMock).toHaveBeenCalledTimes(0); + expect(addLabelsMock).toHaveBeenCalledTimes(1); + expect(addLabelsMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + issue_number: 123, + labels: ['touched-a-pdf-file'] + }); + }); + + it('(with dot: false) does not add labels to PRs that do not match our glob patterns', async () => { + configureInput({}); + usingLabelerConfigYaml('only_pdfs.yml'); + mockGitHubResponseChangedFiles('.foo.pdf'); + + await run(); + + expect(removeLabelMock).toHaveBeenCalledTimes(0); + expect(addLabelsMock).toHaveBeenCalledTimes(0); + }); + + it('(with dot: true) does not add labels to PRs that do not match our glob patterns', async () => { + configureInput({dot: true}); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('foo.txt'); @@ -48,15 +94,11 @@ describe('run', () => { }); it('(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern', async () => { - const mockInput = { + configureInput({ 'repo-token': 'foo', 'configuration-path': 'bar', 'sync-labels': true - }; - - jest - .spyOn(core, 'getInput') - .mockImplementation((name: string, ...opts) => mockInput[name]); + }); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('foo.txt'); @@ -79,15 +121,11 @@ describe('run', () => { }); it('(with sync-labels: false) it issues no delete calls even when there are preexisting PR labels that no longer match the glob pattern', async () => { - const mockInput = { + configureInput({ 'repo-token': 'foo', 'configuration-path': 'bar', 'sync-labels': false - }; - - jest - .spyOn(core, 'getInput') - .mockImplementation((name: string, ...opts) => mockInput[name]); + }); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('foo.txt'); diff --git a/action.yml b/action.yml index a3df342b6..051718f0c 100644 --- a/action.yml +++ b/action.yml @@ -14,6 +14,10 @@ inputs: description: 'Whether or not to remove labels when matching files are reverted' default: false required: false + dot: + description: 'Whether or not to auto-include paths starting with dot (e.g. `.github`)' + default: false + required: false runs: using: 'node16' diff --git a/dist/index.js b/dist/index.js index b3d5dbf2a..f3695826a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -49,7 +49,8 @@ function run() { try { const token = core.getInput('repo-token'); const configPath = core.getInput('configuration-path', { required: true }); - const syncLabels = !!core.getInput('sync-labels', { required: false }); + const syncLabels = !!core.getInput('sync-labels'); + const dot = core.getBooleanInput('dot'); const prNumber = getPrNumber(); if (!prNumber) { core.info('Could not get pull request number from context, exiting'); @@ -68,7 +69,7 @@ function run() { const labelsToRemove = []; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs)) { + if (checkGlobs(changedFiles, globs, dot)) { labels.push(label); } else if (pullRequest.labels.find(l => l.name === label)) { @@ -158,11 +159,11 @@ function toMatchConfig(config) { function printPattern(matcher) { return (matcher.negate ? '!' : '') + matcher.pattern; } -function checkGlobs(changedFiles, globs) { +function checkGlobs(changedFiles, globs, dot) { for (const glob of globs) { core.debug(` checking pattern ${JSON.stringify(glob)}`); const matchConfig = toMatchConfig(glob); - if (checkMatch(changedFiles, matchConfig)) { + if (checkMatch(changedFiles, matchConfig, dot)) { return true; } } @@ -182,8 +183,8 @@ function isMatch(changedFile, matchers) { 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)); +function checkAny(changedFiles, globs, dot) { + const matchers = globs.map(g => new minimatch_1.Minimatch(g, { dot })); core.debug(` checking "any" patterns`); for (const changedFile of changedFiles) { if (isMatch(changedFile, matchers)) { @@ -195,8 +196,8 @@ function checkAny(changedFiles, globs) { return false; } // equivalent to "Array.every()" but expanded for debugging and clarity -function checkAll(changedFiles, globs) { - const matchers = globs.map(g => new minimatch_1.Minimatch(g)); +function checkAll(changedFiles, globs, dot) { + const matchers = globs.map(g => new minimatch_1.Minimatch(g, { dot })); core.debug(` checking "all" patterns`); for (const changedFile of changedFiles) { if (!isMatch(changedFile, matchers)) { @@ -207,14 +208,14 @@ function checkAll(changedFiles, globs) { core.debug(` "all" patterns matched all files`); return true; } -function checkMatch(changedFiles, matchConfig) { +function checkMatch(changedFiles, matchConfig, dot) { if (matchConfig.all !== undefined) { - if (!checkAll(changedFiles, matchConfig.all)) { + if (!checkAll(changedFiles, matchConfig.all, dot)) { return false; } } if (matchConfig.any !== undefined) { - if (!checkAny(changedFiles, matchConfig.any)) { + if (!checkAny(changedFiles, matchConfig.any, dot)) { return false; } } diff --git a/src/labeler.ts b/src/labeler.ts index dd89044f5..6331857cc 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -15,7 +15,8 @@ export async function run() { try { const token = core.getInput('repo-token'); const configPath = core.getInput('configuration-path', {required: true}); - const syncLabels = !!core.getInput('sync-labels', {required: false}); + const syncLabels = !!core.getInput('sync-labels'); + const dot = core.getBooleanInput('dot'); const prNumber = getPrNumber(); if (!prNumber) { @@ -42,7 +43,7 @@ export async function run() { const labelsToRemove: string[] = []; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs)) { + if (checkGlobs(changedFiles, globs, dot)) { labels.push(label); } else if (pullRequest.labels.find(l => l.name === label)) { labelsToRemove.push(label); @@ -157,12 +158,13 @@ function printPattern(matcher: Minimatch): string { export function checkGlobs( changedFiles: string[], - globs: StringOrMatchConfig[] + globs: StringOrMatchConfig[], + dot: boolean ): boolean { for (const glob of globs) { core.debug(` checking pattern ${JSON.stringify(glob)}`); const matchConfig = toMatchConfig(glob); - if (checkMatch(changedFiles, matchConfig)) { + if (checkMatch(changedFiles, matchConfig, dot)) { return true; } } @@ -184,8 +186,12 @@ function isMatch(changedFile: string, matchers: Minimatch[]): boolean { } // equivalent to "Array.some()" but expanded for debugging and clarity -function checkAny(changedFiles: string[], globs: string[]): boolean { - const matchers = globs.map(g => new Minimatch(g)); +function checkAny( + changedFiles: string[], + globs: string[], + dot: boolean +): boolean { + const matchers = globs.map(g => new Minimatch(g, {dot})); core.debug(` checking "any" patterns`); for (const changedFile of changedFiles) { if (isMatch(changedFile, matchers)) { @@ -199,8 +205,12 @@ function checkAny(changedFiles: string[], globs: string[]): boolean { } // equivalent to "Array.every()" but expanded for debugging and clarity -function checkAll(changedFiles: string[], globs: string[]): boolean { - const matchers = globs.map(g => new Minimatch(g)); +function checkAll( + changedFiles: string[], + globs: string[], + dot: boolean +): boolean { + const matchers = globs.map(g => new Minimatch(g, {dot})); core.debug(` checking "all" patterns`); for (const changedFile of changedFiles) { if (!isMatch(changedFile, matchers)) { @@ -213,15 +223,19 @@ function checkAll(changedFiles: string[], globs: string[]): boolean { return true; } -function checkMatch(changedFiles: string[], matchConfig: MatchConfig): boolean { +function checkMatch( + changedFiles: string[], + matchConfig: MatchConfig, + dot: boolean +): boolean { if (matchConfig.all !== undefined) { - if (!checkAll(changedFiles, matchConfig.all)) { + if (!checkAll(changedFiles, matchConfig.all, dot)) { return false; } } if (matchConfig.any !== undefined) { - if (!checkAny(changedFiles, matchConfig.any)) { + if (!checkAny(changedFiles, matchConfig.any, dot)) { return false; } }