mirror of
https://github.com/actions/labeler.git
synced 2025-12-13 13:07:24 +00:00
25
README.md
25
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.
|
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
|
#### Basic Examples
|
||||||
|
|
||||||
```yml
|
```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` |
|
| `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` |
|
| `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` |
|
||||||
|
|
||||||
# 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).
|
Contributions are welcome! See the [Contributor's Guide](CONTRIBUTING.md).
|
||||||
|
|||||||
@@ -15,15 +15,29 @@ const matchConfig = [{any: ['*.txt']}];
|
|||||||
describe('checkGlobs', () => {
|
describe('checkGlobs', () => {
|
||||||
it('returns true when our pattern does match changed files', () => {
|
it('returns true when our pattern does match changed files', () => {
|
||||||
const changedFiles = ['foo.txt', 'bar.txt'];
|
const changedFiles = ['foo.txt', 'bar.txt'];
|
||||||
const result = checkGlobs(changedFiles, matchConfig);
|
const result = checkGlobs(changedFiles, matchConfig, false);
|
||||||
|
|
||||||
expect(result).toBeTruthy();
|
expect(result).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns false when our pattern does not match changed files', () => {
|
it('returns false when our pattern does not match changed files', () => {
|
||||||
const changedFiles = ['foo.docx'];
|
const changedFiles = ['foo.docx'];
|
||||||
const result = checkGlobs(changedFiles, matchConfig);
|
const result = checkGlobs(changedFiles, matchConfig, false);
|
||||||
|
|
||||||
expect(result).toBeFalsy();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,10 +18,27 @@ const yamlFixtures = {
|
|||||||
'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml')
|
'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());
|
afterAll(() => jest.restoreAllMocks());
|
||||||
|
|
||||||
describe('run', () => {
|
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');
|
usingLabelerConfigYaml('only_pdfs.yml');
|
||||||
mockGitHubResponseChangedFiles('foo.pdf');
|
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');
|
usingLabelerConfigYaml('only_pdfs.yml');
|
||||||
mockGitHubResponseChangedFiles('foo.txt');
|
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 () => {
|
it('(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern', async () => {
|
||||||
const mockInput = {
|
configureInput({
|
||||||
'repo-token': 'foo',
|
'repo-token': 'foo',
|
||||||
'configuration-path': 'bar',
|
'configuration-path': 'bar',
|
||||||
'sync-labels': true
|
'sync-labels': true
|
||||||
};
|
});
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(core, 'getInput')
|
|
||||||
.mockImplementation((name: string, ...opts) => mockInput[name]);
|
|
||||||
|
|
||||||
usingLabelerConfigYaml('only_pdfs.yml');
|
usingLabelerConfigYaml('only_pdfs.yml');
|
||||||
mockGitHubResponseChangedFiles('foo.txt');
|
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 () => {
|
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',
|
'repo-token': 'foo',
|
||||||
'configuration-path': 'bar',
|
'configuration-path': 'bar',
|
||||||
'sync-labels': false
|
'sync-labels': false
|
||||||
};
|
});
|
||||||
|
|
||||||
jest
|
|
||||||
.spyOn(core, 'getInput')
|
|
||||||
.mockImplementation((name: string, ...opts) => mockInput[name]);
|
|
||||||
|
|
||||||
usingLabelerConfigYaml('only_pdfs.yml');
|
usingLabelerConfigYaml('only_pdfs.yml');
|
||||||
mockGitHubResponseChangedFiles('foo.txt');
|
mockGitHubResponseChangedFiles('foo.txt');
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ inputs:
|
|||||||
description: 'Whether or not to remove labels when matching files are reverted'
|
description: 'Whether or not to remove labels when matching files are reverted'
|
||||||
default: false
|
default: false
|
||||||
required: false
|
required: false
|
||||||
|
dot:
|
||||||
|
description: 'Whether or not to auto-include paths starting with dot (e.g. `.github`)'
|
||||||
|
default: false
|
||||||
|
required: false
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
|
|||||||
23
dist/index.js
vendored
23
dist/index.js
vendored
@@ -49,7 +49,8 @@ function run() {
|
|||||||
try {
|
try {
|
||||||
const token = core.getInput('repo-token');
|
const token = core.getInput('repo-token');
|
||||||
const configPath = core.getInput('configuration-path', { required: true });
|
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();
|
const prNumber = getPrNumber();
|
||||||
if (!prNumber) {
|
if (!prNumber) {
|
||||||
core.info('Could not get pull request number from context, exiting');
|
core.info('Could not get pull request number from context, exiting');
|
||||||
@@ -68,7 +69,7 @@ function run() {
|
|||||||
const labelsToRemove = [];
|
const labelsToRemove = [];
|
||||||
for (const [label, globs] of labelGlobs.entries()) {
|
for (const [label, globs] of labelGlobs.entries()) {
|
||||||
core.debug(`processing ${label}`);
|
core.debug(`processing ${label}`);
|
||||||
if (checkGlobs(changedFiles, globs)) {
|
if (checkGlobs(changedFiles, globs, dot)) {
|
||||||
labels.push(label);
|
labels.push(label);
|
||||||
}
|
}
|
||||||
else if (pullRequest.labels.find(l => l.name === label)) {
|
else if (pullRequest.labels.find(l => l.name === label)) {
|
||||||
@@ -158,11 +159,11 @@ function toMatchConfig(config) {
|
|||||||
function printPattern(matcher) {
|
function printPattern(matcher) {
|
||||||
return (matcher.negate ? '!' : '') + matcher.pattern;
|
return (matcher.negate ? '!' : '') + matcher.pattern;
|
||||||
}
|
}
|
||||||
function checkGlobs(changedFiles, globs) {
|
function checkGlobs(changedFiles, globs, dot) {
|
||||||
for (const glob of globs) {
|
for (const glob of globs) {
|
||||||
core.debug(` checking pattern ${JSON.stringify(glob)}`);
|
core.debug(` checking pattern ${JSON.stringify(glob)}`);
|
||||||
const matchConfig = toMatchConfig(glob);
|
const matchConfig = toMatchConfig(glob);
|
||||||
if (checkMatch(changedFiles, matchConfig)) {
|
if (checkMatch(changedFiles, matchConfig, dot)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,8 +183,8 @@ function isMatch(changedFile, matchers) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// equivalent to "Array.some()" but expanded for debugging and clarity
|
// equivalent to "Array.some()" but expanded for debugging and clarity
|
||||||
function checkAny(changedFiles, globs) {
|
function checkAny(changedFiles, globs, dot) {
|
||||||
const matchers = globs.map(g => new minimatch_1.Minimatch(g));
|
const matchers = globs.map(g => new minimatch_1.Minimatch(g, { dot }));
|
||||||
core.debug(` checking "any" patterns`);
|
core.debug(` checking "any" patterns`);
|
||||||
for (const changedFile of changedFiles) {
|
for (const changedFile of changedFiles) {
|
||||||
if (isMatch(changedFile, matchers)) {
|
if (isMatch(changedFile, matchers)) {
|
||||||
@@ -195,8 +196,8 @@ function checkAny(changedFiles, globs) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// equivalent to "Array.every()" but expanded for debugging and clarity
|
// equivalent to "Array.every()" but expanded for debugging and clarity
|
||||||
function checkAll(changedFiles, globs) {
|
function checkAll(changedFiles, globs, dot) {
|
||||||
const matchers = globs.map(g => new minimatch_1.Minimatch(g));
|
const matchers = globs.map(g => new minimatch_1.Minimatch(g, { dot }));
|
||||||
core.debug(` checking "all" patterns`);
|
core.debug(` checking "all" patterns`);
|
||||||
for (const changedFile of changedFiles) {
|
for (const changedFile of changedFiles) {
|
||||||
if (!isMatch(changedFile, matchers)) {
|
if (!isMatch(changedFile, matchers)) {
|
||||||
@@ -207,14 +208,14 @@ function checkAll(changedFiles, globs) {
|
|||||||
core.debug(` "all" patterns matched all files`);
|
core.debug(` "all" patterns matched all files`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
function checkMatch(changedFiles, matchConfig) {
|
function checkMatch(changedFiles, matchConfig, dot) {
|
||||||
if (matchConfig.all !== undefined) {
|
if (matchConfig.all !== undefined) {
|
||||||
if (!checkAll(changedFiles, matchConfig.all)) {
|
if (!checkAll(changedFiles, matchConfig.all, dot)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (matchConfig.any !== undefined) {
|
if (matchConfig.any !== undefined) {
|
||||||
if (!checkAny(changedFiles, matchConfig.any)) {
|
if (!checkAny(changedFiles, matchConfig.any, dot)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ export async function run() {
|
|||||||
try {
|
try {
|
||||||
const token = core.getInput('repo-token');
|
const token = core.getInput('repo-token');
|
||||||
const configPath = core.getInput('configuration-path', {required: true});
|
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();
|
const prNumber = getPrNumber();
|
||||||
if (!prNumber) {
|
if (!prNumber) {
|
||||||
@@ -42,7 +43,7 @@ export async function run() {
|
|||||||
const labelsToRemove: string[] = [];
|
const labelsToRemove: string[] = [];
|
||||||
for (const [label, globs] of labelGlobs.entries()) {
|
for (const [label, globs] of labelGlobs.entries()) {
|
||||||
core.debug(`processing ${label}`);
|
core.debug(`processing ${label}`);
|
||||||
if (checkGlobs(changedFiles, globs)) {
|
if (checkGlobs(changedFiles, globs, dot)) {
|
||||||
labels.push(label);
|
labels.push(label);
|
||||||
} else if (pullRequest.labels.find(l => l.name === label)) {
|
} else if (pullRequest.labels.find(l => l.name === label)) {
|
||||||
labelsToRemove.push(label);
|
labelsToRemove.push(label);
|
||||||
@@ -157,12 +158,13 @@ function printPattern(matcher: Minimatch): string {
|
|||||||
|
|
||||||
export function checkGlobs(
|
export function checkGlobs(
|
||||||
changedFiles: string[],
|
changedFiles: string[],
|
||||||
globs: StringOrMatchConfig[]
|
globs: StringOrMatchConfig[],
|
||||||
|
dot: boolean
|
||||||
): boolean {
|
): boolean {
|
||||||
for (const glob of globs) {
|
for (const glob of globs) {
|
||||||
core.debug(` checking pattern ${JSON.stringify(glob)}`);
|
core.debug(` checking pattern ${JSON.stringify(glob)}`);
|
||||||
const matchConfig = toMatchConfig(glob);
|
const matchConfig = toMatchConfig(glob);
|
||||||
if (checkMatch(changedFiles, matchConfig)) {
|
if (checkMatch(changedFiles, matchConfig, dot)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,8 +186,12 @@ function isMatch(changedFile: string, matchers: Minimatch[]): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// equivalent to "Array.some()" but expanded for debugging and clarity
|
// equivalent to "Array.some()" but expanded for debugging and clarity
|
||||||
function checkAny(changedFiles: string[], globs: string[]): boolean {
|
function checkAny(
|
||||||
const matchers = globs.map(g => new Minimatch(g));
|
changedFiles: string[],
|
||||||
|
globs: string[],
|
||||||
|
dot: boolean
|
||||||
|
): boolean {
|
||||||
|
const matchers = globs.map(g => new Minimatch(g, {dot}));
|
||||||
core.debug(` checking "any" patterns`);
|
core.debug(` checking "any" patterns`);
|
||||||
for (const changedFile of changedFiles) {
|
for (const changedFile of changedFiles) {
|
||||||
if (isMatch(changedFile, matchers)) {
|
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
|
// equivalent to "Array.every()" but expanded for debugging and clarity
|
||||||
function checkAll(changedFiles: string[], globs: string[]): boolean {
|
function checkAll(
|
||||||
const matchers = globs.map(g => new Minimatch(g));
|
changedFiles: string[],
|
||||||
|
globs: string[],
|
||||||
|
dot: boolean
|
||||||
|
): boolean {
|
||||||
|
const matchers = globs.map(g => new Minimatch(g, {dot}));
|
||||||
core.debug(` checking "all" patterns`);
|
core.debug(` checking "all" patterns`);
|
||||||
for (const changedFile of changedFiles) {
|
for (const changedFile of changedFiles) {
|
||||||
if (!isMatch(changedFile, matchers)) {
|
if (!isMatch(changedFile, matchers)) {
|
||||||
@@ -213,15 +223,19 @@ function checkAll(changedFiles: string[], globs: string[]): boolean {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkMatch(changedFiles: string[], matchConfig: MatchConfig): boolean {
|
function checkMatch(
|
||||||
|
changedFiles: string[],
|
||||||
|
matchConfig: MatchConfig,
|
||||||
|
dot: boolean
|
||||||
|
): boolean {
|
||||||
if (matchConfig.all !== undefined) {
|
if (matchConfig.all !== undefined) {
|
||||||
if (!checkAll(changedFiles, matchConfig.all)) {
|
if (!checkAll(changedFiles, matchConfig.all, dot)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (matchConfig.any !== undefined) {
|
if (matchConfig.any !== undefined) {
|
||||||
if (!checkAny(changedFiles, matchConfig.any)) {
|
if (!checkAny(changedFiles, matchConfig.any, dot)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user