diff --git a/README.md b/README.md index 2abc38b7..55bb580b 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,6 @@ repo: # Add '@domain/core' label to any change within the 'core' package @domain/core: -- package/core/* - package/core/**/* # Add 'test' label to any change to *.spec.js files within the source dir @@ -113,7 +112,23 @@ 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 | N/A | | `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` +| `dot` | Whether or not to auto-include paths starting with dot (e.g. `.github`) | `false` -# Contributions +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 are welcome! See the [Contributor's Guide](CONTRIBUTING.md). diff --git a/__tests__/labeler.test.ts b/__tests__/labeler.test.ts index 082ed672..c78bb407 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 false 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 de145599..a39675de 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -18,10 +18,24 @@ 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]); +}; + 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 +51,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 +91,11 @@ describe("run", () => { }); it("(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern", async () => { - let 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 +118,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 () => { - let 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 16e71a8d..9cddef1c 100644 --- a/action.yml +++ b/action.yml @@ -12,6 +12,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: 'node12' diff --git a/src/labeler.ts b/src/labeler.ts index 59cf23f3..9d603d90 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -16,6 +16,7 @@ export async function run() { const token = core.getInput("repo-token", { required: true }); const configPath = core.getInput("configuration-path", { required: true }); const syncLabels = !!core.getInput("sync-labels", { required: false }); + const dot = !!core.getInput("dot", { required: false }); 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: IMinimatch): 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: IMinimatch[]): 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,7 +205,11 @@ function checkAny(changedFiles: string[], globs: string[]): boolean { } // equivalent to "Array.every()" but expanded for debugging and clarity -function checkAll(changedFiles: string[], globs: string[]): boolean { +function checkAll( + changedFiles: string[], + globs: string[], + dot: boolean +): boolean { const matchers = globs.map((g) => new Minimatch(g)); core.debug(` checking "all" patterns`); for (const changedFile of changedFiles) { @@ -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; } }