Skip to content

Commit

Permalink
Merge pull request #316 from kachkaev/dot-option
Browse files Browse the repository at this point in the history
Implement dot option
  • Loading branch information
MaksimZhukov authored Jun 5, 2023
2 parents 3cbc54c + d40596e commit 82a4f6f
Show file tree
Hide file tree
Showing 6 changed files with 132 additions and 40 deletions.
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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).
18 changes: 16 additions & 2 deletions __tests__/labeler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
66 changes: 52 additions & 14 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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');

Expand All @@ -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');
Expand All @@ -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');
Expand Down
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
23 changes: 12 additions & 11 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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)) {
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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;
}
}
Expand Down
36 changes: 25 additions & 11 deletions src/labeler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
}
Expand All @@ -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)) {
Expand All @@ -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)) {
Expand All @@ -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;
}
}
Expand Down

0 comments on commit 82a4f6f

Please sign in to comment.