mirror of
https://github.com/actions/labeler
synced 2026-05-09 17:21:03 +02:00
Add 'changed-files-labels-limit' and 'max-files-changed' configs to allow capping number of labels added (#923)
* README.md: drop trailing whitespace * Add 'changed-files-labels-limit' config to allow capping number of labels added When a repository has many components, each with a changed-files label, a large refactor ends up with the labeler spamming the pull request with label changes. The end result is not very useful as it's not very readable, and due to how github automatically hides comments when label changes overflow the discussion tab, it means useful information is hidden and one has to manually click "Load more..." dozens of time every time the page is loaded. Add a changed-files-labels-limit top level config knob. If more than the configured limit of labels is set to be added, none are added. This only affects changed-files labels. * Add 'max-files-changed' config to allow capping number of files for labelling When a PR modifies a very large number of files (e.g., tree-wide refactors, automated code formatting), this new options allows skipping file-based labeling entirely when the number of files that are changed hits the configured limit. Fixes https://github.com/actions/labeler/issues/486
This commit is contained in:
18
__tests__/fixtures/limit_0.yml
Normal file
18
__tests__/fixtures/limit_0.yml
Normal file
@@ -0,0 +1,18 @@
|
||||
# Limit to 0 changed-files labels (none allowed)
|
||||
changed-files-labels-limit: 0
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
26
__tests__/fixtures/limit_1.yml
Normal file
26
__tests__/fixtures/limit_1.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Limit to 1 changed-files label
|
||||
changed-files-labels-limit: 1
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
26
__tests__/fixtures/limit_2.yml
Normal file
26
__tests__/fixtures/limit_2.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Limit to 2 changed-files labels
|
||||
changed-files-labels-limit: 2
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
26
__tests__/fixtures/limit_3.yml
Normal file
26
__tests__/fixtures/limit_3.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
# Limit to 3 changed-files labels
|
||||
changed-files-labels-limit: 3
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
15
__tests__/fixtures/max_files_5.yml
Normal file
15
__tests__/fixtures/max_files_5.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Skip file-based labeling if more than 5 files changed
|
||||
max-files-changed: 5
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
15
__tests__/fixtures/max_files_with_branch.yml
Normal file
15
__tests__/fixtures/max_files_with_branch.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Skip file-based labeling if more than 3 files changed
|
||||
max-files-changed: 3
|
||||
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
# Branch-based label (should not be affected)
|
||||
test-branch:
|
||||
- head-branch: ['^test/']
|
||||
23
__tests__/fixtures/mixed_labels.yml
Normal file
23
__tests__/fixtures/mixed_labels.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
# Labels based on changed files
|
||||
component-a:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
component-b:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/b/**']
|
||||
|
||||
component-c:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/c/**']
|
||||
|
||||
component-d:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/d/**']
|
||||
|
||||
# Labels based on branch patterns only
|
||||
test-branch:
|
||||
- head-branch: '^test/'
|
||||
|
||||
feature-branch:
|
||||
- head-branch: '/feature/'
|
||||
16
__tests__/fixtures/mixed_rules.yml
Normal file
16
__tests__/fixtures/mixed_rules.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
# Test fixture for mixed rules behavior
|
||||
# A label with both branch and changed-files rules is considered a "changed-files label"
|
||||
# and is subject to the limit, even if it matches via the branch rule
|
||||
changed-files-labels-limit: 0
|
||||
|
||||
# This label has both branch and changed-files rules
|
||||
# It should be subject to the limit even if matched via branch
|
||||
mixed-label:
|
||||
- any:
|
||||
- head-branch: '^test/'
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ['components/a/**']
|
||||
|
||||
# Pure branch-based label - not subject to limit
|
||||
pure-branch-label:
|
||||
- head-branch: '^test/'
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
MatchConfig,
|
||||
toMatchConfig,
|
||||
getLabelConfigMapFromObject,
|
||||
BaseMatchConfig
|
||||
getLabelConfigResultFromObject,
|
||||
BaseMatchConfig,
|
||||
configUsesChangedFiles
|
||||
} from '../src/api/get-label-configs';
|
||||
|
||||
jest.mock('@actions/core');
|
||||
@@ -60,6 +62,204 @@ describe('getLabelConfigMapFromObject', () => {
|
||||
const result = getLabelConfigMapFromObject(yamlObject);
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('ignores top-level options like changed-files-labels-limit and max-files-changed', () => {
|
||||
const configWithLimit = {
|
||||
'changed-files-labels-limit': 5,
|
||||
'max-files-changed': 100,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigMapFromObject(configWithLimit);
|
||||
expect(result.has('changed-files-labels-limit')).toBe(false);
|
||||
expect(result.has('max-files-changed')).toBe(false);
|
||||
expect(result.has('label1')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLabelConfigResultFromObject', () => {
|
||||
it('extracts changed-files-labels-limit as a number', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 5,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(5);
|
||||
expect(result.labelConfigs.has('label1')).toBe(true);
|
||||
});
|
||||
|
||||
it('parses changed-files-labels-limit from string', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': '10',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(10);
|
||||
});
|
||||
|
||||
it('trims whitespace when parsing string values', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': ' 5 ',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(5);
|
||||
});
|
||||
|
||||
it('returns undefined changedFilesLimit when not set', () => {
|
||||
const config = {
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws error for invalid changed-files-labels-limit value', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 'invalid',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/Invalid value for 'changed-files-labels-limit'/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for negative changed-files-labels-limit value', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': -1,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for string with trailing characters', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': '10abc',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for decimal string', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': '3.2',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for float number', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 3.2,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts zero as a valid changed-files-labels-limit', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 0,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(0);
|
||||
});
|
||||
|
||||
it('extracts max-files-changed as a number', () => {
|
||||
const config = {
|
||||
'max-files-changed': 100,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBe(100);
|
||||
expect(result.labelConfigs.has('label1')).toBe(true);
|
||||
});
|
||||
|
||||
it('parses max-files-changed from string', () => {
|
||||
const config = {
|
||||
'max-files-changed': '50',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBe(50);
|
||||
});
|
||||
|
||||
it('returns undefined maxFilesChanged when not set', () => {
|
||||
const config = {
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBeUndefined();
|
||||
});
|
||||
|
||||
it('throws error for invalid max-files-changed value', () => {
|
||||
const config = {
|
||||
'max-files-changed': 'invalid',
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/Invalid value for 'max-files-changed'/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws error for negative max-files-changed value', () => {
|
||||
const config = {
|
||||
'max-files-changed': -1,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/must be a non-negative integer/
|
||||
);
|
||||
});
|
||||
|
||||
it('accepts zero as a valid max-files-changed', () => {
|
||||
const config = {
|
||||
'max-files-changed': 0,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.maxFilesChanged).toBe(0);
|
||||
});
|
||||
|
||||
it('supports both options together', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': 5,
|
||||
'max-files-changed': 100,
|
||||
label1: [{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}]
|
||||
};
|
||||
const result = getLabelConfigResultFromObject(config);
|
||||
expect(result.changedFilesLimit).toBe(5);
|
||||
expect(result.maxFilesChanged).toBe(100);
|
||||
});
|
||||
|
||||
it('throws a clear error when max-files-changed is used as a label', () => {
|
||||
const config = {
|
||||
'max-files-changed': [
|
||||
{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}
|
||||
]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/reserved top-level option and cannot be used as a label name/
|
||||
);
|
||||
});
|
||||
|
||||
it('throws a clear error when changed-files-labels-limit is used as a label', () => {
|
||||
const config = {
|
||||
'changed-files-labels-limit': [
|
||||
{'changed-files': [{'any-glob-to-any-file': ['*.txt']}]}
|
||||
]
|
||||
};
|
||||
expect(() => getLabelConfigResultFromObject(config)).toThrow(
|
||||
/reserved top-level option and cannot be used as a label name/
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toMatchConfig', () => {
|
||||
@@ -164,6 +364,52 @@ describe('checkMatchConfigs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('configUsesChangedFiles', () => {
|
||||
it('returns true when config has changed-files in any block', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{any: [{changedFiles: [{anyGlobToAnyFile: ['*.txt']}]}]}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when config has changed-files in all block', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{all: [{changedFiles: [{allGlobsToAllFiles: ['*.txt']}]}]}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when config only has branch patterns', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{any: [{headBranch: ['^test/']}]},
|
||||
{any: [{baseBranch: ['main']}]}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when config has empty changed-files array', () => {
|
||||
const matchConfig: MatchConfig[] = [{any: [{changedFiles: []}]}];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when config has changed-files with empty objects', () => {
|
||||
const matchConfig: MatchConfig[] = [{any: [{changedFiles: [{}]}]}];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when config has mixed branch and changed-files patterns', () => {
|
||||
const matchConfig: MatchConfig[] = [
|
||||
{
|
||||
any: [
|
||||
{changedFiles: [{anyGlobToAnyFile: ['*.txt']}]},
|
||||
{headBranch: ['^feature/']}
|
||||
]
|
||||
}
|
||||
];
|
||||
expect(configUsesChangedFiles(matchConfig)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('labeler error handling', () => {
|
||||
const mockClient = {} as any;
|
||||
const mockPullRequest = {
|
||||
@@ -183,9 +429,10 @@ describe('labeler error handling', () => {
|
||||
}
|
||||
]);
|
||||
|
||||
(api.getLabelConfigs as jest.Mock).mockResolvedValue(
|
||||
new Map([['new-label', ['dummy-config']]])
|
||||
);
|
||||
(api.getLabelConfigs as jest.Mock).mockResolvedValue({
|
||||
labelConfigs: new Map([['new-label', ['dummy-config']]]),
|
||||
changedFilesLimit: undefined
|
||||
});
|
||||
|
||||
// Force match so "new-label" is always added
|
||||
jest.spyOn({checkMatchConfigs}, 'checkMatchConfigs').mockReturnValue(true);
|
||||
|
||||
@@ -37,7 +37,17 @@ const yamlFixtures = {
|
||||
'branches.yml': fs.readFileSync('__tests__/fixtures/branches.yml'),
|
||||
'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml'),
|
||||
'not_supported.yml': fs.readFileSync('__tests__/fixtures/not_supported.yml'),
|
||||
'any_and_all.yml': fs.readFileSync('__tests__/fixtures/any_and_all.yml')
|
||||
'any_and_all.yml': fs.readFileSync('__tests__/fixtures/any_and_all.yml'),
|
||||
'mixed_labels.yml': fs.readFileSync('__tests__/fixtures/mixed_labels.yml'),
|
||||
'limit_0.yml': fs.readFileSync('__tests__/fixtures/limit_0.yml'),
|
||||
'limit_1.yml': fs.readFileSync('__tests__/fixtures/limit_1.yml'),
|
||||
'limit_2.yml': fs.readFileSync('__tests__/fixtures/limit_2.yml'),
|
||||
'limit_3.yml': fs.readFileSync('__tests__/fixtures/limit_3.yml'),
|
||||
'mixed_rules.yml': fs.readFileSync('__tests__/fixtures/mixed_rules.yml'),
|
||||
'max_files_5.yml': fs.readFileSync('__tests__/fixtures/max_files_5.yml'),
|
||||
'max_files_with_branch.yml': fs.readFileSync(
|
||||
'__tests__/fixtures/max_files_with_branch.yml'
|
||||
)
|
||||
};
|
||||
|
||||
const configureInput = (
|
||||
@@ -440,6 +450,341 @@ describe('run', () => {
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
describe('changed-files-labels-limit', () => {
|
||||
it('applies all labels when count is within limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_3.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips changed-files labels when count exceeds limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// No labels should be applied since changed-files labels exceed limit
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('still applies branch-based labels when changed-files limit is exceeded', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('limit_1.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// Only the branch-based label should be applied
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['test-branch']
|
||||
});
|
||||
});
|
||||
|
||||
it('applies all labels when no limit is set', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('mixed_labels.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts',
|
||||
'components/d/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c', 'component-d']
|
||||
});
|
||||
});
|
||||
|
||||
it('does not count preexisting labels toward the limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts',
|
||||
'components/d/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: [{name: 'component-a'}, {name: 'component-b'}]}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// component-a and component-b are preexisting, so only 2 new labels (c, d) would be added
|
||||
// which equals the limit of 2, so labels should be applied
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c', 'component-d']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips new labels when new count exceeds limit even with preexisting', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts',
|
||||
'components/d/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: [{name: 'component-a'}]}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// component-a is preexisting, so 3 new labels (b, c, d) would be added
|
||||
// which exceeds the limit of 2, so no new changed-files labels are applied
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('applies labels when new count equals the limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('limit_2.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips all changed-files labels when limit is 0', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('limit_0.yml');
|
||||
mockGitHubResponseChangedFiles('components/a/file.ts');
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// With limit 0, only branch-based labels should be applied
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['test-branch']
|
||||
});
|
||||
});
|
||||
|
||||
it('treats labels with mixed rules as changed-files labels', async () => {
|
||||
// A label that has both branch and changed-files rules is considered
|
||||
// a "changed-files label" and subject to the limit, even if it matches
|
||||
// via the branch rule
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('mixed_rules.yml');
|
||||
mockGitHubResponseChangedFiles('unrelated/file.ts');
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// The mixed-label matches via branch rule but is still subject to limit
|
||||
// because it contains a changed-files rule in its definition.
|
||||
// Only pure-branch-label should be applied.
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['pure-branch-label']
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('max-files-changed', () => {
|
||||
it('applies labels when changed files count is within limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file.ts',
|
||||
'components/b/file.ts',
|
||||
'components/c/file.ts'
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c']
|
||||
});
|
||||
});
|
||||
|
||||
it('skips file-based labels when changed files exceed limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file1.ts',
|
||||
'components/a/file2.ts',
|
||||
'components/b/file1.ts',
|
||||
'components/b/file2.ts',
|
||||
'components/c/file1.ts',
|
||||
'components/c/file2.ts' // 6 files > limit of 5
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// No labels should be applied since changed files exceed limit
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('applies labels when changed files count equals limit', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file1.ts',
|
||||
'components/a/file2.ts',
|
||||
'components/b/file1.ts',
|
||||
'components/b/file2.ts',
|
||||
'components/c/file.ts' // exactly 5 files = limit
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['component-a', 'component-b', 'component-c']
|
||||
});
|
||||
});
|
||||
|
||||
it('still applies branch-based labels when max-files-changed is exceeded', async () => {
|
||||
configureInput({});
|
||||
github.context.payload.pull_request!.head = {ref: 'test/some-feature'};
|
||||
usingLabelerConfigYaml('max_files_with_branch.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'components/a/file1.ts',
|
||||
'components/a/file2.ts',
|
||||
'components/b/file1.ts',
|
||||
'components/b/file2.ts' // 4 files > limit of 3
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: []}
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// Only the branch-based label should be applied
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(1);
|
||||
expect(setLabelsMock).toHaveBeenCalledWith({
|
||||
owner: 'monalisa',
|
||||
repo: 'helloworld',
|
||||
issue_number: 123,
|
||||
labels: ['test-branch']
|
||||
});
|
||||
});
|
||||
|
||||
it('preserves preexisting changed-files labels with sync-labels when max-files-changed is exceeded', async () => {
|
||||
configureInput({'sync-labels': true});
|
||||
github.context.payload.pull_request!.head = {ref: 'main'};
|
||||
usingLabelerConfigYaml('max_files_5.yml');
|
||||
mockGitHubResponseChangedFiles(
|
||||
'unrelated/file1.ts',
|
||||
'unrelated/file2.ts',
|
||||
'unrelated/file3.ts',
|
||||
'unrelated/file4.ts',
|
||||
'unrelated/file5.ts',
|
||||
'unrelated/file6.ts' // 6 files > limit of 5
|
||||
);
|
||||
getPullMock.mockResolvedValue(<any>{
|
||||
data: {labels: [{name: 'component-a'}]} // preexisting label
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
// No setLabels call because labels should remain unchanged
|
||||
// (component-a is preserved, not removed by sync-labels)
|
||||
expect(setLabelsMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use local configuration file if it exists', async () => {
|
||||
const configFile = 'only_pdfs.yml';
|
||||
const configFilePath = path.join(__dirname, 'fixtures', configFile);
|
||||
|
||||
Reference in New Issue
Block a user