diff --git a/.licenses/npm/lodash.isequal.dep.yml b/.licenses/npm/lodash.isequal.dep.yml
deleted file mode 100644
index 4bc8358e0..000000000
--- a/.licenses/npm/lodash.isequal.dep.yml
+++ /dev/null
@@ -1,58 +0,0 @@
----
-name: lodash.isequal
-version: 4.5.0
-type: npm
-summary: The Lodash method `_.isEqual` exported as a module.
-homepage: https://lodash.com/
-license: mit
-licenses:
-- sources: LICENSE
- text: |
- Copyright JS Foundation and other contributors
-
- Based on Underscore.js, copyright Jeremy Ashkenas,
- DocumentCloud and Investigative Reporters & Editors
-
- This software consists of voluntary contributions made by many
- individuals. For exact contribution history, see the revision history
- available at https://github.com/lodash/lodash
-
- The following license applies to all parts of this software except as
- documented below:
-
- ====
-
- Permission is hereby granted, free of charge, to any person obtaining
- a copy of this software and associated documentation files (the
- "Software"), to deal in the Software without restriction, including
- without limitation the rights to use, copy, modify, merge, publish,
- distribute, sublicense, and/or sell copies of the Software, and to
- permit persons to whom the Software is furnished to do so, subject to
- the following conditions:
-
- The above copyright notice and this permission notice shall be
- included in all copies or substantial portions of the Software.
-
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
- ====
-
- Copyright and related rights for sample code are waived via CC0. Sample
- code is defined as all source code displayed within the prose of the
- documentation.
-
- CC0: http://creativecommons.org/publicdomain/zero/1.0/
-
- ====
-
- Files located in the node_modules and vendor directories are externally
- maintained libraries used by this software which have their own
- licenses; we recommend you read them, as their terms may differ from the
- terms above.
-notices: []
diff --git a/README.md b/README.md
index 57a829bcc..03c4e2a69 100644
--- a/README.md
+++ b/README.md
@@ -274,10 +274,16 @@ 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 | `github.token` |
| `configuration-path` | The path to the label configuration file. If the file doesn't exist at the specified path on the runner, action will read from the source repository via the Github API. | `.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 to remove configured labels when they no longer match. Labels not present in the labeler configuration are never removed. | `false` |
| `dot` | Whether or not to auto-include paths starting with dot (e.g. `.github`) | `true` |
| `pr-number` | The number(s) of pull request to update, rather than detecting from the workflow context | N/A |
+When `sync-labels` is enabled, labeler synchronizes only labels whose names are
+present in the labeler configuration. Matching configured labels are added in
+one batch, and configured labels that no longer match are removed in one batch.
+Other labels, including labels added by users or other automation, are not
+rewritten or removed.
+
##### Using `configuration-path` input together with the `@actions/checkout` action
You might want to use action called [@actions/checkout](https://github.com/actions/checkout) to upload label configuration file onto the runner from the current or any other repositories. See usage example below:
@@ -328,6 +334,11 @@ Labeler provides the following outputs:
| `new-labels` | A comma-separated list of all new labels |
| `all-labels` | A comma-separated list of all labels that the PR contains |
+The outputs are calculated from the pull request label snapshot used by the
+action and the label changes it successfully applies. A label added
+concurrently by another actor is preserved, but may not appear in the outputs
+for that run.
+
The following example performs steps based on the output of labeler:
```yml
name: "Pull Request Labeler"
diff --git a/__tests__/add-labels.test.ts b/__tests__/add-labels.test.ts
new file mode 100644
index 000000000..85595e133
--- /dev/null
+++ b/__tests__/add-labels.test.ts
@@ -0,0 +1,94 @@
+import {jest, describe, it, expect} from '@jest/globals';
+import type {ClientType} from '../src/api/types.js';
+
+jest.unstable_mockModule('@actions/github', () => ({
+ context: {
+ repo: {owner: 'monalisa', repo: 'helloworld'}
+ }
+}));
+
+const {addLabels} = await import('../src/api/add-labels.js');
+
+const createClient = () => {
+ const addLabelsMock = jest.fn();
+ const listLabelsOnIssueMock = jest.fn();
+ const client = {
+ rest: {
+ issues: {
+ addLabels: addLabelsMock,
+ listLabelsOnIssue: listLabelsOnIssueMock
+ }
+ }
+ } as ClientType;
+
+ return {client, addLabelsMock, listLabelsOnIssueMock};
+};
+
+describe('addLabels', () => {
+ it('does not verify a successful addition', async () => {
+ const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
+ addLabelsMock.mockResolvedValue({data: []});
+
+ await addLabels(client, 123, ['bug']);
+
+ expect(addLabelsMock).toHaveBeenCalledWith({
+ owner: 'monalisa',
+ repo: 'helloworld',
+ issue_number: 123,
+ labels: ['bug'],
+ request: {retries: 0}
+ });
+ expect(listLabelsOnIssueMock).not.toHaveBeenCalled();
+ });
+
+ it('accepts a server error when every requested label was committed', async () => {
+ const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
+ const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
+ addLabelsMock.mockRejectedValue(serverError);
+ listLabelsOnIssueMock.mockResolvedValue({
+ data: [{name: 'BUG'}, {name: 'documentation'}]
+ });
+
+ await expect(
+ addLabels(client, 123, ['bug', 'documentation'])
+ ).resolves.toBeUndefined();
+ expect(listLabelsOnIssueMock).toHaveBeenCalledWith({
+ owner: 'monalisa',
+ repo: 'helloworld',
+ issue_number: 123,
+ per_page: 100,
+ request: {retries: 0}
+ });
+ });
+
+ it('preserves a server error when any requested label is missing', async () => {
+ const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
+ const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
+ addLabelsMock.mockRejectedValue(serverError);
+ listLabelsOnIssueMock.mockResolvedValue({data: [{name: 'bug'}]});
+
+ await expect(addLabels(client, 123, ['bug', 'documentation'])).rejects.toBe(
+ serverError
+ );
+ });
+
+ it('does not verify non-server errors', async () => {
+ const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
+ const validationError = Object.assign(new Error('Validation Failed'), {
+ status: 422
+ });
+ addLabelsMock.mockRejectedValue(validationError);
+
+ await expect(addLabels(client, 123, ['bug'])).rejects.toBe(validationError);
+ expect(listLabelsOnIssueMock).not.toHaveBeenCalled();
+ });
+
+ it('preserves the server error when verification fails', async () => {
+ const {client, addLabelsMock, listLabelsOnIssueMock} = createClient();
+ const serverError = Object.assign(new Error('Bad Gateway'), {status: 502});
+ addLabelsMock.mockRejectedValue(serverError);
+ listLabelsOnIssueMock.mockRejectedValue(new Error('Service unavailable'));
+
+ await expect(addLabels(client, 123, ['bug'])).rejects.toBe(serverError);
+ });
+});
diff --git a/__tests__/labeler.test.ts b/__tests__/labeler.test.ts
index b0bbc157d..9f21969df 100644
--- a/__tests__/labeler.test.ts
+++ b/__tests__/labeler.test.ts
@@ -9,7 +9,8 @@ import type {
// Define API mock functions at module level
const getPullRequestsMock = jest.fn();
const getLabelConfigsMock = jest.fn();
-const setLabelsMock = jest.fn();
+const addLabelsMock = jest.fn();
+const removeLabelsMock = jest.fn();
const getChangedFilesMock = jest.fn();
const getContentMock = jest.fn();
@@ -42,7 +43,8 @@ jest.unstable_mockModule('@actions/github', () => ({
jest.unstable_mockModule('../src/api/index.js', () => ({
getPullRequests: getPullRequestsMock,
getLabelConfigs: getLabelConfigsMock,
- setLabels: setLabelsMock,
+ addLabels: addLabelsMock,
+ removeLabels: removeLabelsMock,
getChangedFiles: getChangedFilesMock,
getContent: getContentMock
}));
@@ -478,7 +480,7 @@ describe('labeler error handling', () => {
});
it('throws a custom error for HttpError 403 with "unauthorized" message', async () => {
- setLabelsMock.mockRejectedValue({
+ addLabelsMock.mockRejectedValue({
name: 'HttpError',
status: 403,
message: 'Request failed with status code 403: Unauthorized'
@@ -495,7 +497,7 @@ describe('labeler error handling', () => {
status: 404,
message: 'Not Found'
};
- setLabelsMock.mockRejectedValue(unexpectedError);
+ addLabelsMock.mockRejectedValue(unexpectedError);
// NOTE: In the current implementation, labeler rethrows the raw error object (not an Error instance).
// `rejects.toThrow` only works with real Error objects, so here we must use `rejects.toEqual`.
@@ -508,7 +510,7 @@ describe('labeler error handling', () => {
name: 'HttpError',
message: 'Resource not accessible by integration'
};
- setLabelsMock.mockRejectedValue(error);
+ addLabelsMock.mockRejectedValue(error);
await labeler();
@@ -518,4 +520,33 @@ describe('labeler error handling', () => {
);
expect(core.setFailed).toHaveBeenCalledWith(error.message);
});
+
+ it('reports the configured labels when a bulk removal fails', async () => {
+ (core.getBooleanInput as jest.Mock).mockReturnValue(true);
+ getPullRequestsMock.mockReturnValue([
+ {
+ number: 123,
+ data: {
+ node_id: 'PR_node_id',
+ labels: [{name: 'stale-label', node_id: 'label_node_id'}]
+ },
+ changedFiles: ['file.txt']
+ }
+ ]);
+ getLabelConfigsMock.mockResolvedValue({
+ labelConfigs: new Map([
+ [
+ 'stale-label',
+ [{any: [{changedFiles: [{anyGlobToAnyFile: ['*.pdf']}]}]}]
+ ]
+ ]),
+ changedFilesLimit: undefined
+ });
+ removeLabelsMock.mockRejectedValue(new Error('GraphQL request failed'));
+
+ await expect(labeler()).rejects.toThrow(
+ "Failed to remove configured labels 'stale-label' from PR #123"
+ );
+ expect(addLabelsMock).not.toHaveBeenCalled();
+ });
});
diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts
index 0ac908c86..00fd1f7bf 100644
--- a/__tests__/main.test.ts
+++ b/__tests__/main.test.ts
@@ -14,6 +14,13 @@ import fs from 'fs';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
// Define mock functions before mocking modules
+const addLabelsMock = jest.fn();
+const addLabelsRequestMock = jest.fn(options => {
+ const params = {...options};
+ delete params.request;
+ return addLabelsMock(params);
+});
+const removeLabelsMock = jest.fn();
const setLabelsMock = jest.fn();
const reposMock = jest.fn();
const paginateMock = jest.fn();
@@ -54,8 +61,12 @@ jest.unstable_mockModule('@actions/core', () => ({
jest.unstable_mockModule('@actions/github', () => ({
context: mockGithubContext,
getOctokit: jest.fn(() => ({
+ graphql: removeLabelsMock,
rest: {
- issues: {setLabels: setLabelsMock},
+ issues: {
+ addLabels: addLabelsRequestMock,
+ setLabels: setLabelsMock
+ },
repos: {getContent: reposMock},
pulls: {
get: getPullMock,
@@ -144,14 +155,17 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
labels: ['touched-a-pdf-file']
});
+ expect(addLabelsRequestMock).toHaveBeenCalledWith(
+ expect.objectContaining({request: {retries: 0}})
+ );
expect(setOutputSpy).toHaveBeenCalledWith(
'new-labels',
'touched-a-pdf-file'
@@ -174,8 +188,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -191,6 +205,29 @@ describe('run', () => {
);
});
+ it('does not lose a label added concurrently with matching labels', async () => {
+ configureInput({'sync-labels': true});
+ usingLabelerConfigYaml('only_pdfs.yml');
+ mockGitHubResponseChangedFiles('foo.pdf');
+
+ const labelsOnPullRequest: string[] = [];
+ getPullMock.mockResolvedValue({
+ data: {node_id: 'PR_node_id', labels: []}
+ });
+ addLabelsMock.mockImplementationOnce(({labels}: {labels: string[]}) => {
+ labelsOnPullRequest.push('external-label', ...labels);
+ });
+
+ await run();
+
+ expect(labelsOnPullRequest).toEqual([
+ 'external-label',
+ 'touched-a-pdf-file'
+ ]);
+ expect(getPullMock).toHaveBeenCalledTimes(1);
+ expect(setLabelsMock).not.toHaveBeenCalled();
+ });
+
it('(with dot: false) does not add labels to PRs that do not match our glob patterns', async () => {
configureInput({});
usingLabelerConfigYaml('only_pdfs.yml');
@@ -203,7 +240,7 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
expect(setOutputSpy).toHaveBeenCalledWith('all-labels', '');
});
@@ -215,7 +252,7 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('does not add a label when the match config options are not supported', async () => {
@@ -223,7 +260,7 @@ describe('run', () => {
usingLabelerConfigYaml('not_supported.yml');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('adds labels based on the branch names that match the regexp pattern', async () => {
@@ -232,8 +269,8 @@ describe('run', () => {
usingLabelerConfigYaml('branches.yml');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -252,8 +289,8 @@ describe('run', () => {
usingLabelerConfigYaml('branches.yml');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -276,8 +313,8 @@ describe('run', () => {
usingLabelerConfigYaml('branches.yml');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -294,8 +331,8 @@ describe('run', () => {
usingLabelerConfigYaml('branches.yml');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -312,8 +349,8 @@ describe('run', () => {
mockGitHubResponseChangedFiles('tests/test.ts');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -330,7 +367,7 @@ describe('run', () => {
mockGitHubResponseChangedFiles('tests/requirements.txt');
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern', async () => {
@@ -344,19 +381,26 @@ describe('run', () => {
mockGitHubResponseChangedFiles('foo.txt');
getPullMock.mockResolvedValue({
data: {
- labels: [{name: 'touched-a-pdf-file'}, {name: 'manually-added'}]
+ node_id: 'PR_node_id',
+ labels: [
+ {name: 'touched-a-pdf-file', node_id: 'stale_label_node_id'},
+ {name: 'manually-added', node_id: 'manual_label_node_id'}
+ ]
}
});
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
- owner: 'monalisa',
- repo: 'helloworld',
- issue_number: 123,
- labels: ['manually-added']
- });
+ expect(addLabelsMock).not.toHaveBeenCalled();
+ expect(removeLabelsMock).toHaveBeenCalledTimes(1);
+ expect(removeLabelsMock).toHaveBeenCalledWith(
+ expect.stringContaining('removeLabelsFromLabelable'),
+ {
+ labelableId: 'PR_node_id',
+ labelIds: ['stale_label_node_id']
+ }
+ );
+ expect(setLabelsMock).not.toHaveBeenCalled();
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
expect(setOutputSpy).toHaveBeenCalledWith('all-labels', 'manually-added');
@@ -379,7 +423,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
+ expect(removeLabelsMock).not.toHaveBeenCalled();
expect(setOutputSpy).toHaveBeenCalledWith('new-labels', '');
expect(setOutputSpy).toHaveBeenCalledWith(
'all-labels',
@@ -387,6 +432,36 @@ describe('run', () => {
);
});
+ it('removes stale configured labels in one mutation', async () => {
+ configureInput({'sync-labels': true});
+ usingLabelerConfigYaml('mixed_labels.yml');
+ mockGitHubResponseChangedFiles('unrelated.txt');
+ getPullMock.mockResolvedValue({
+ data: {
+ node_id: 'PR_node_id',
+ labels: [
+ {name: 'component-a', node_id: 'label_a'},
+ {name: 'component-b', node_id: 'label_b'},
+ {name: 'external-label', node_id: 'external_label'}
+ ]
+ }
+ });
+
+ await run();
+
+ expect(removeLabelsMock).toHaveBeenCalledTimes(1);
+ expect(removeLabelsMock).toHaveBeenCalledWith(
+ expect.stringContaining('removeLabelsFromLabelable'),
+ {
+ labelableId: 'PR_node_id',
+ labelIds: ['label_a', 'label_b']
+ }
+ );
+ expect(addLabelsMock).not.toHaveBeenCalled();
+ expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(setOutputSpy).toHaveBeenCalledWith('all-labels', 'external-label');
+ });
+
it('(with sync-labels: false) it only logs the excess labels', async () => {
configureInput({
'repo-token': 'foo',
@@ -408,7 +483,7 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
expect(coreWarningMock).toHaveBeenCalledTimes(1);
expect(coreWarningMock).toHaveBeenCalledWith(
@@ -437,12 +512,12 @@ describe('run', () => {
});
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 104,
- labels: ['manually-added', 'touched-a-pdf-file']
+ labels: ['touched-a-pdf-file']
});
expect(setOutputSpy).toHaveBeenCalledWith(
'new-labels',
@@ -477,14 +552,14 @@ describe('run', () => {
});
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(2);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(2);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 104,
- labels: ['manually-added', 'touched-a-pdf-file']
+ labels: ['touched-a-pdf-file']
});
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 150,
@@ -515,7 +590,7 @@ describe('run', () => {
"'abc' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: negative number) warns and makes no API call', async () => {
@@ -533,7 +608,7 @@ describe('run', () => {
"'-1' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: zero) warns and makes no API call', async () => {
@@ -551,7 +626,7 @@ describe('run', () => {
"'0' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: number with internal space) warns and makes no API call', async () => {
@@ -569,7 +644,7 @@ describe('run', () => {
"'10 4' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: number with trailing non-numeric chars) warns and makes no API call', async () => {
@@ -587,7 +662,7 @@ describe('run', () => {
"'104abc' is not a valid pull request number"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: valid number with surrounding whitespace) trims and processes correctly', async () => {
@@ -624,7 +699,7 @@ describe('run', () => {
"'abc\\x0ddef' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: string with tab) sanitizes tab as \\x09 in warning', async () => {
@@ -642,7 +717,7 @@ describe('run', () => {
"'abc\\x09def' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: string with ANSI escape sequence) sanitizes ESC byte as \\x1b in warning', async () => {
@@ -660,7 +735,7 @@ describe('run', () => {
"'abc\\x1b[31mINJECTED\\x1b[0m' is not a valid pull request number (non-printable characters were escaped as \\xNN)"
);
expect(getPullMock).not.toHaveBeenCalled();
- expect(setLabelsMock).not.toHaveBeenCalled();
+ expect(addLabelsMock).not.toHaveBeenCalled();
});
it('(with pr-number: mix of valid and invalid) processes valid, skips invalid with warning', async () => {
@@ -682,8 +757,8 @@ describe('run', () => {
expect(coreWarningMock).toHaveBeenCalledWith(
"'abc' is not a valid pull request number"
);
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 104,
@@ -697,7 +772,7 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
describe('changed-files-labels-limit', () => {
@@ -715,8 +790,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -740,7 +815,7 @@ describe('run', () => {
await run();
// No labels should be applied since changed-files labels exceed limit
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('still applies branch-based labels when changed-files limit is exceeded', async () => {
@@ -759,8 +834,8 @@ describe('run', () => {
await run();
// Only the branch-based label should be applied
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -784,8 +859,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -811,12 +886,12 @@ describe('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({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
- labels: ['component-a', 'component-b', 'component-c', 'component-d']
+ labels: ['component-c', 'component-d']
});
});
@@ -838,7 +913,7 @@ describe('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);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('applies labels when new count equals the limit', async () => {
@@ -855,8 +930,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -876,8 +951,8 @@ describe('run', () => {
await run();
// With limit 0, only branch-based labels should be applied
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -902,8 +977,8 @@ describe('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({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -928,8 +1003,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -956,7 +1031,7 @@ describe('run', () => {
await run();
// No labels should be applied since changed files exceed limit
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
it('applies labels when changed files count equals limit', async () => {
@@ -976,8 +1051,8 @@ describe('run', () => {
await run();
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -1002,8 +1077,8 @@ describe('run', () => {
await run();
// Only the branch-based label should be applied
- expect(setLabelsMock).toHaveBeenCalledTimes(1);
- expect(setLabelsMock).toHaveBeenCalledWith({
+ expect(addLabelsMock).toHaveBeenCalledTimes(1);
+ expect(addLabelsMock).toHaveBeenCalledWith({
owner: 'monalisa',
repo: 'helloworld',
issue_number: 123,
@@ -1029,9 +1104,9 @@ describe('run', () => {
await run();
- // No setLabels call because labels should remain unchanged
+ // No mutation because labels should remain unchanged
// (component-a is preserved, not removed by sync-labels)
- expect(setLabelsMock).toHaveBeenCalledTimes(0);
+ expect(addLabelsMock).toHaveBeenCalledTimes(0);
});
});
diff --git a/action.yml b/action.yml
index c71c4e7b7..a1ec955a3 100644
--- a/action.yml
+++ b/action.yml
@@ -11,7 +11,7 @@ inputs:
default: '.github/labeler.yml'
required: false
sync-labels:
- description: 'Whether or not to remove labels when matching files are reverted'
+ description: 'Whether to remove configured labels when they no longer match'
default: false
required: false
dot:
diff --git a/dist/index.js b/dist/index.js
index 4780f4d03..778a5829e 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -2372,1862 +2372,6 @@ class DecodedURL extends URL {
})));
-/***/ }),
-
-/***/ 9471:
-/***/ ((module, exports, __nccwpck_require__) => {
-
-/* module decorator */ module = __nccwpck_require__.nmd(module);
-/**
- * Lodash (Custom Build)
- * Build: `lodash modularize exports="npm" -o ./`
- * Copyright JS Foundation and other contributors
- * Released under MIT license
- * Based on Underscore.js 1.8.3
- * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
- */
-
-/** Used as the size to enable large array optimizations. */
-var LARGE_ARRAY_SIZE = 200;
-
-/** Used to stand-in for `undefined` hash values. */
-var HASH_UNDEFINED = '__lodash_hash_undefined__';
-
-/** Used to compose bitmasks for value comparisons. */
-var COMPARE_PARTIAL_FLAG = 1,
- COMPARE_UNORDERED_FLAG = 2;
-
-/** Used as references for various `Number` constants. */
-var MAX_SAFE_INTEGER = 9007199254740991;
-
-/** `Object#toString` result references. */
-var argsTag = '[object Arguments]',
- arrayTag = '[object Array]',
- asyncTag = '[object AsyncFunction]',
- boolTag = '[object Boolean]',
- dateTag = '[object Date]',
- errorTag = '[object Error]',
- funcTag = '[object Function]',
- genTag = '[object GeneratorFunction]',
- mapTag = '[object Map]',
- numberTag = '[object Number]',
- nullTag = '[object Null]',
- objectTag = '[object Object]',
- promiseTag = '[object Promise]',
- proxyTag = '[object Proxy]',
- regexpTag = '[object RegExp]',
- setTag = '[object Set]',
- stringTag = '[object String]',
- symbolTag = '[object Symbol]',
- undefinedTag = '[object Undefined]',
- weakMapTag = '[object WeakMap]';
-
-var arrayBufferTag = '[object ArrayBuffer]',
- dataViewTag = '[object DataView]',
- float32Tag = '[object Float32Array]',
- float64Tag = '[object Float64Array]',
- int8Tag = '[object Int8Array]',
- int16Tag = '[object Int16Array]',
- int32Tag = '[object Int32Array]',
- uint8Tag = '[object Uint8Array]',
- uint8ClampedTag = '[object Uint8ClampedArray]',
- uint16Tag = '[object Uint16Array]',
- uint32Tag = '[object Uint32Array]';
-
-/**
- * Used to match `RegExp`
- * [syntax characters](http://ecma-international.org/ecma-262/7.0/#sec-patterns).
- */
-var reRegExpChar = /[\\^$.*+?()[\]{}|]/g;
-
-/** Used to detect host constructors (Safari). */
-var reIsHostCtor = /^\[object .+?Constructor\]$/;
-
-/** Used to detect unsigned integer values. */
-var reIsUint = /^(?:0|[1-9]\d*)$/;
-
-/** Used to identify `toStringTag` values of typed arrays. */
-var typedArrayTags = {};
-typedArrayTags[float32Tag] = typedArrayTags[float64Tag] =
-typedArrayTags[int8Tag] = typedArrayTags[int16Tag] =
-typedArrayTags[int32Tag] = typedArrayTags[uint8Tag] =
-typedArrayTags[uint8ClampedTag] = typedArrayTags[uint16Tag] =
-typedArrayTags[uint32Tag] = true;
-typedArrayTags[argsTag] = typedArrayTags[arrayTag] =
-typedArrayTags[arrayBufferTag] = typedArrayTags[boolTag] =
-typedArrayTags[dataViewTag] = typedArrayTags[dateTag] =
-typedArrayTags[errorTag] = typedArrayTags[funcTag] =
-typedArrayTags[mapTag] = typedArrayTags[numberTag] =
-typedArrayTags[objectTag] = typedArrayTags[regexpTag] =
-typedArrayTags[setTag] = typedArrayTags[stringTag] =
-typedArrayTags[weakMapTag] = false;
-
-/** Detect free variable `global` from Node.js. */
-var freeGlobal = typeof global == 'object' && global && global.Object === Object && global;
-
-/** Detect free variable `self`. */
-var freeSelf = typeof self == 'object' && self && self.Object === Object && self;
-
-/** Used as a reference to the global object. */
-var root = freeGlobal || freeSelf || Function('return this')();
-
-/** Detect free variable `exports`. */
-var freeExports = true && exports && !exports.nodeType && exports;
-
-/** Detect free variable `module`. */
-var freeModule = freeExports && "object" == 'object' && module && !module.nodeType && module;
-
-/** Detect the popular CommonJS extension `module.exports`. */
-var moduleExports = freeModule && freeModule.exports === freeExports;
-
-/** Detect free variable `process` from Node.js. */
-var freeProcess = moduleExports && freeGlobal.process;
-
-/** Used to access faster Node.js helpers. */
-var nodeUtil = (function() {
- try {
- return freeProcess && freeProcess.binding && freeProcess.binding('util');
- } catch (e) {}
-}());
-
-/* Node.js helper references. */
-var nodeIsTypedArray = nodeUtil && nodeUtil.isTypedArray;
-
-/**
- * A specialized version of `_.filter` for arrays without support for
- * iteratee shorthands.
- *
- * @private
- * @param {Array} [array] The array to iterate over.
- * @param {Function} predicate The function invoked per iteration.
- * @returns {Array} Returns the new filtered array.
- */
-function arrayFilter(array, predicate) {
- var index = -1,
- length = array == null ? 0 : array.length,
- resIndex = 0,
- result = [];
-
- while (++index < length) {
- var value = array[index];
- if (predicate(value, index, array)) {
- result[resIndex++] = value;
- }
- }
- return result;
-}
-
-/**
- * Appends the elements of `values` to `array`.
- *
- * @private
- * @param {Array} array The array to modify.
- * @param {Array} values The values to append.
- * @returns {Array} Returns `array`.
- */
-function arrayPush(array, values) {
- var index = -1,
- length = values.length,
- offset = array.length;
-
- while (++index < length) {
- array[offset + index] = values[index];
- }
- return array;
-}
-
-/**
- * A specialized version of `_.some` for arrays without support for iteratee
- * shorthands.
- *
- * @private
- * @param {Array} [array] The array to iterate over.
- * @param {Function} predicate The function invoked per iteration.
- * @returns {boolean} Returns `true` if any element passes the predicate check,
- * else `false`.
- */
-function arraySome(array, predicate) {
- var index = -1,
- length = array == null ? 0 : array.length;
-
- while (++index < length) {
- if (predicate(array[index], index, array)) {
- return true;
- }
- }
- return false;
-}
-
-/**
- * The base implementation of `_.times` without support for iteratee shorthands
- * or max array length checks.
- *
- * @private
- * @param {number} n The number of times to invoke `iteratee`.
- * @param {Function} iteratee The function invoked per iteration.
- * @returns {Array} Returns the array of results.
- */
-function baseTimes(n, iteratee) {
- var index = -1,
- result = Array(n);
-
- while (++index < n) {
- result[index] = iteratee(index);
- }
- return result;
-}
-
-/**
- * The base implementation of `_.unary` without support for storing metadata.
- *
- * @private
- * @param {Function} func The function to cap arguments for.
- * @returns {Function} Returns the new capped function.
- */
-function baseUnary(func) {
- return function(value) {
- return func(value);
- };
-}
-
-/**
- * Checks if a `cache` value for `key` exists.
- *
- * @private
- * @param {Object} cache The cache to query.
- * @param {string} key The key of the entry to check.
- * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
- */
-function cacheHas(cache, key) {
- return cache.has(key);
-}
-
-/**
- * Gets the value at `key` of `object`.
- *
- * @private
- * @param {Object} [object] The object to query.
- * @param {string} key The key of the property to get.
- * @returns {*} Returns the property value.
- */
-function getValue(object, key) {
- return object == null ? undefined : object[key];
-}
-
-/**
- * Converts `map` to its key-value pairs.
- *
- * @private
- * @param {Object} map The map to convert.
- * @returns {Array} Returns the key-value pairs.
- */
-function mapToArray(map) {
- var index = -1,
- result = Array(map.size);
-
- map.forEach(function(value, key) {
- result[++index] = [key, value];
- });
- return result;
-}
-
-/**
- * Creates a unary function that invokes `func` with its argument transformed.
- *
- * @private
- * @param {Function} func The function to wrap.
- * @param {Function} transform The argument transform.
- * @returns {Function} Returns the new function.
- */
-function overArg(func, transform) {
- return function(arg) {
- return func(transform(arg));
- };
-}
-
-/**
- * Converts `set` to an array of its values.
- *
- * @private
- * @param {Object} set The set to convert.
- * @returns {Array} Returns the values.
- */
-function setToArray(set) {
- var index = -1,
- result = Array(set.size);
-
- set.forEach(function(value) {
- result[++index] = value;
- });
- return result;
-}
-
-/** Used for built-in method references. */
-var arrayProto = Array.prototype,
- funcProto = Function.prototype,
- objectProto = Object.prototype;
-
-/** Used to detect overreaching core-js shims. */
-var coreJsData = root['__core-js_shared__'];
-
-/** Used to resolve the decompiled source of functions. */
-var funcToString = funcProto.toString;
-
-/** Used to check objects for own properties. */
-var hasOwnProperty = objectProto.hasOwnProperty;
-
-/** Used to detect methods masquerading as native. */
-var maskSrcKey = (function() {
- var uid = /[^.]+$/.exec(coreJsData && coreJsData.keys && coreJsData.keys.IE_PROTO || '');
- return uid ? ('Symbol(src)_1.' + uid) : '';
-}());
-
-/**
- * Used to resolve the
- * [`toStringTag`](http://ecma-international.org/ecma-262/7.0/#sec-object.prototype.tostring)
- * of values.
- */
-var nativeObjectToString = objectProto.toString;
-
-/** Used to detect if a method is native. */
-var reIsNative = RegExp('^' +
- funcToString.call(hasOwnProperty).replace(reRegExpChar, '\\$&')
- .replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g, '$1.*?') + '$'
-);
-
-/** Built-in value references. */
-var Buffer = moduleExports ? root.Buffer : undefined,
- Symbol = root.Symbol,
- Uint8Array = root.Uint8Array,
- propertyIsEnumerable = objectProto.propertyIsEnumerable,
- splice = arrayProto.splice,
- symToStringTag = Symbol ? Symbol.toStringTag : undefined;
-
-/* Built-in method references for those with the same name as other `lodash` methods. */
-var nativeGetSymbols = Object.getOwnPropertySymbols,
- nativeIsBuffer = Buffer ? Buffer.isBuffer : undefined,
- nativeKeys = overArg(Object.keys, Object);
-
-/* Built-in method references that are verified to be native. */
-var DataView = getNative(root, 'DataView'),
- Map = getNative(root, 'Map'),
- Promise = getNative(root, 'Promise'),
- Set = getNative(root, 'Set'),
- WeakMap = getNative(root, 'WeakMap'),
- nativeCreate = getNative(Object, 'create');
-
-/** Used to detect maps, sets, and weakmaps. */
-var dataViewCtorString = toSource(DataView),
- mapCtorString = toSource(Map),
- promiseCtorString = toSource(Promise),
- setCtorString = toSource(Set),
- weakMapCtorString = toSource(WeakMap);
-
-/** Used to convert symbols to primitives and strings. */
-var symbolProto = Symbol ? Symbol.prototype : undefined,
- symbolValueOf = symbolProto ? symbolProto.valueOf : undefined;
-
-/**
- * Creates a hash object.
- *
- * @private
- * @constructor
- * @param {Array} [entries] The key-value pairs to cache.
- */
-function Hash(entries) {
- var index = -1,
- length = entries == null ? 0 : entries.length;
-
- this.clear();
- while (++index < length) {
- var entry = entries[index];
- this.set(entry[0], entry[1]);
- }
-}
-
-/**
- * Removes all key-value entries from the hash.
- *
- * @private
- * @name clear
- * @memberOf Hash
- */
-function hashClear() {
- this.__data__ = nativeCreate ? nativeCreate(null) : {};
- this.size = 0;
-}
-
-/**
- * Removes `key` and its value from the hash.
- *
- * @private
- * @name delete
- * @memberOf Hash
- * @param {Object} hash The hash to modify.
- * @param {string} key The key of the value to remove.
- * @returns {boolean} Returns `true` if the entry was removed, else `false`.
- */
-function hashDelete(key) {
- var result = this.has(key) && delete this.__data__[key];
- this.size -= result ? 1 : 0;
- return result;
-}
-
-/**
- * Gets the hash value for `key`.
- *
- * @private
- * @name get
- * @memberOf Hash
- * @param {string} key The key of the value to get.
- * @returns {*} Returns the entry value.
- */
-function hashGet(key) {
- var data = this.__data__;
- if (nativeCreate) {
- var result = data[key];
- return result === HASH_UNDEFINED ? undefined : result;
- }
- return hasOwnProperty.call(data, key) ? data[key] : undefined;
-}
-
-/**
- * Checks if a hash value for `key` exists.
- *
- * @private
- * @name has
- * @memberOf Hash
- * @param {string} key The key of the entry to check.
- * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
- */
-function hashHas(key) {
- var data = this.__data__;
- return nativeCreate ? (data[key] !== undefined) : hasOwnProperty.call(data, key);
-}
-
-/**
- * Sets the hash `key` to `value`.
- *
- * @private
- * @name set
- * @memberOf Hash
- * @param {string} key The key of the value to set.
- * @param {*} value The value to set.
- * @returns {Object} Returns the hash instance.
- */
-function hashSet(key, value) {
- var data = this.__data__;
- this.size += this.has(key) ? 0 : 1;
- data[key] = (nativeCreate && value === undefined) ? HASH_UNDEFINED : value;
- return this;
-}
-
-// Add methods to `Hash`.
-Hash.prototype.clear = hashClear;
-Hash.prototype['delete'] = hashDelete;
-Hash.prototype.get = hashGet;
-Hash.prototype.has = hashHas;
-Hash.prototype.set = hashSet;
-
-/**
- * Creates an list cache object.
- *
- * @private
- * @constructor
- * @param {Array} [entries] The key-value pairs to cache.
- */
-function ListCache(entries) {
- var index = -1,
- length = entries == null ? 0 : entries.length;
-
- this.clear();
- while (++index < length) {
- var entry = entries[index];
- this.set(entry[0], entry[1]);
- }
-}
-
-/**
- * Removes all key-value entries from the list cache.
- *
- * @private
- * @name clear
- * @memberOf ListCache
- */
-function listCacheClear() {
- this.__data__ = [];
- this.size = 0;
-}
-
-/**
- * Removes `key` and its value from the list cache.
- *
- * @private
- * @name delete
- * @memberOf ListCache
- * @param {string} key The key of the value to remove.
- * @returns {boolean} Returns `true` if the entry was removed, else `false`.
- */
-function listCacheDelete(key) {
- var data = this.__data__,
- index = assocIndexOf(data, key);
-
- if (index < 0) {
- return false;
- }
- var lastIndex = data.length - 1;
- if (index == lastIndex) {
- data.pop();
- } else {
- splice.call(data, index, 1);
- }
- --this.size;
- return true;
-}
-
-/**
- * Gets the list cache value for `key`.
- *
- * @private
- * @name get
- * @memberOf ListCache
- * @param {string} key The key of the value to get.
- * @returns {*} Returns the entry value.
- */
-function listCacheGet(key) {
- var data = this.__data__,
- index = assocIndexOf(data, key);
-
- return index < 0 ? undefined : data[index][1];
-}
-
-/**
- * Checks if a list cache value for `key` exists.
- *
- * @private
- * @name has
- * @memberOf ListCache
- * @param {string} key The key of the entry to check.
- * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
- */
-function listCacheHas(key) {
- return assocIndexOf(this.__data__, key) > -1;
-}
-
-/**
- * Sets the list cache `key` to `value`.
- *
- * @private
- * @name set
- * @memberOf ListCache
- * @param {string} key The key of the value to set.
- * @param {*} value The value to set.
- * @returns {Object} Returns the list cache instance.
- */
-function listCacheSet(key, value) {
- var data = this.__data__,
- index = assocIndexOf(data, key);
-
- if (index < 0) {
- ++this.size;
- data.push([key, value]);
- } else {
- data[index][1] = value;
- }
- return this;
-}
-
-// Add methods to `ListCache`.
-ListCache.prototype.clear = listCacheClear;
-ListCache.prototype['delete'] = listCacheDelete;
-ListCache.prototype.get = listCacheGet;
-ListCache.prototype.has = listCacheHas;
-ListCache.prototype.set = listCacheSet;
-
-/**
- * Creates a map cache object to store key-value pairs.
- *
- * @private
- * @constructor
- * @param {Array} [entries] The key-value pairs to cache.
- */
-function MapCache(entries) {
- var index = -1,
- length = entries == null ? 0 : entries.length;
-
- this.clear();
- while (++index < length) {
- var entry = entries[index];
- this.set(entry[0], entry[1]);
- }
-}
-
-/**
- * Removes all key-value entries from the map.
- *
- * @private
- * @name clear
- * @memberOf MapCache
- */
-function mapCacheClear() {
- this.size = 0;
- this.__data__ = {
- 'hash': new Hash,
- 'map': new (Map || ListCache),
- 'string': new Hash
- };
-}
-
-/**
- * Removes `key` and its value from the map.
- *
- * @private
- * @name delete
- * @memberOf MapCache
- * @param {string} key The key of the value to remove.
- * @returns {boolean} Returns `true` if the entry was removed, else `false`.
- */
-function mapCacheDelete(key) {
- var result = getMapData(this, key)['delete'](key);
- this.size -= result ? 1 : 0;
- return result;
-}
-
-/**
- * Gets the map value for `key`.
- *
- * @private
- * @name get
- * @memberOf MapCache
- * @param {string} key The key of the value to get.
- * @returns {*} Returns the entry value.
- */
-function mapCacheGet(key) {
- return getMapData(this, key).get(key);
-}
-
-/**
- * Checks if a map value for `key` exists.
- *
- * @private
- * @name has
- * @memberOf MapCache
- * @param {string} key The key of the entry to check.
- * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
- */
-function mapCacheHas(key) {
- return getMapData(this, key).has(key);
-}
-
-/**
- * Sets the map `key` to `value`.
- *
- * @private
- * @name set
- * @memberOf MapCache
- * @param {string} key The key of the value to set.
- * @param {*} value The value to set.
- * @returns {Object} Returns the map cache instance.
- */
-function mapCacheSet(key, value) {
- var data = getMapData(this, key),
- size = data.size;
-
- data.set(key, value);
- this.size += data.size == size ? 0 : 1;
- return this;
-}
-
-// Add methods to `MapCache`.
-MapCache.prototype.clear = mapCacheClear;
-MapCache.prototype['delete'] = mapCacheDelete;
-MapCache.prototype.get = mapCacheGet;
-MapCache.prototype.has = mapCacheHas;
-MapCache.prototype.set = mapCacheSet;
-
-/**
- *
- * Creates an array cache object to store unique values.
- *
- * @private
- * @constructor
- * @param {Array} [values] The values to cache.
- */
-function SetCache(values) {
- var index = -1,
- length = values == null ? 0 : values.length;
-
- this.__data__ = new MapCache;
- while (++index < length) {
- this.add(values[index]);
- }
-}
-
-/**
- * Adds `value` to the array cache.
- *
- * @private
- * @name add
- * @memberOf SetCache
- * @alias push
- * @param {*} value The value to cache.
- * @returns {Object} Returns the cache instance.
- */
-function setCacheAdd(value) {
- this.__data__.set(value, HASH_UNDEFINED);
- return this;
-}
-
-/**
- * Checks if `value` is in the array cache.
- *
- * @private
- * @name has
- * @memberOf SetCache
- * @param {*} value The value to search for.
- * @returns {number} Returns `true` if `value` is found, else `false`.
- */
-function setCacheHas(value) {
- return this.__data__.has(value);
-}
-
-// Add methods to `SetCache`.
-SetCache.prototype.add = SetCache.prototype.push = setCacheAdd;
-SetCache.prototype.has = setCacheHas;
-
-/**
- * Creates a stack cache object to store key-value pairs.
- *
- * @private
- * @constructor
- * @param {Array} [entries] The key-value pairs to cache.
- */
-function Stack(entries) {
- var data = this.__data__ = new ListCache(entries);
- this.size = data.size;
-}
-
-/**
- * Removes all key-value entries from the stack.
- *
- * @private
- * @name clear
- * @memberOf Stack
- */
-function stackClear() {
- this.__data__ = new ListCache;
- this.size = 0;
-}
-
-/**
- * Removes `key` and its value from the stack.
- *
- * @private
- * @name delete
- * @memberOf Stack
- * @param {string} key The key of the value to remove.
- * @returns {boolean} Returns `true` if the entry was removed, else `false`.
- */
-function stackDelete(key) {
- var data = this.__data__,
- result = data['delete'](key);
-
- this.size = data.size;
- return result;
-}
-
-/**
- * Gets the stack value for `key`.
- *
- * @private
- * @name get
- * @memberOf Stack
- * @param {string} key The key of the value to get.
- * @returns {*} Returns the entry value.
- */
-function stackGet(key) {
- return this.__data__.get(key);
-}
-
-/**
- * Checks if a stack value for `key` exists.
- *
- * @private
- * @name has
- * @memberOf Stack
- * @param {string} key The key of the entry to check.
- * @returns {boolean} Returns `true` if an entry for `key` exists, else `false`.
- */
-function stackHas(key) {
- return this.__data__.has(key);
-}
-
-/**
- * Sets the stack `key` to `value`.
- *
- * @private
- * @name set
- * @memberOf Stack
- * @param {string} key The key of the value to set.
- * @param {*} value The value to set.
- * @returns {Object} Returns the stack cache instance.
- */
-function stackSet(key, value) {
- var data = this.__data__;
- if (data instanceof ListCache) {
- var pairs = data.__data__;
- if (!Map || (pairs.length < LARGE_ARRAY_SIZE - 1)) {
- pairs.push([key, value]);
- this.size = ++data.size;
- return this;
- }
- data = this.__data__ = new MapCache(pairs);
- }
- data.set(key, value);
- this.size = data.size;
- return this;
-}
-
-// Add methods to `Stack`.
-Stack.prototype.clear = stackClear;
-Stack.prototype['delete'] = stackDelete;
-Stack.prototype.get = stackGet;
-Stack.prototype.has = stackHas;
-Stack.prototype.set = stackSet;
-
-/**
- * Creates an array of the enumerable property names of the array-like `value`.
- *
- * @private
- * @param {*} value The value to query.
- * @param {boolean} inherited Specify returning inherited property names.
- * @returns {Array} Returns the array of property names.
- */
-function arrayLikeKeys(value, inherited) {
- var isArr = isArray(value),
- isArg = !isArr && isArguments(value),
- isBuff = !isArr && !isArg && isBuffer(value),
- isType = !isArr && !isArg && !isBuff && isTypedArray(value),
- skipIndexes = isArr || isArg || isBuff || isType,
- result = skipIndexes ? baseTimes(value.length, String) : [],
- length = result.length;
-
- for (var key in value) {
- if ((inherited || hasOwnProperty.call(value, key)) &&
- !(skipIndexes && (
- // Safari 9 has enumerable `arguments.length` in strict mode.
- key == 'length' ||
- // Node.js 0.10 has enumerable non-index properties on buffers.
- (isBuff && (key == 'offset' || key == 'parent')) ||
- // PhantomJS 2 has enumerable non-index properties on typed arrays.
- (isType && (key == 'buffer' || key == 'byteLength' || key == 'byteOffset')) ||
- // Skip index properties.
- isIndex(key, length)
- ))) {
- result.push(key);
- }
- }
- return result;
-}
-
-/**
- * Gets the index at which the `key` is found in `array` of key-value pairs.
- *
- * @private
- * @param {Array} array The array to inspect.
- * @param {*} key The key to search for.
- * @returns {number} Returns the index of the matched value, else `-1`.
- */
-function assocIndexOf(array, key) {
- var length = array.length;
- while (length--) {
- if (eq(array[length][0], key)) {
- return length;
- }
- }
- return -1;
-}
-
-/**
- * The base implementation of `getAllKeys` and `getAllKeysIn` which uses
- * `keysFunc` and `symbolsFunc` to get the enumerable property names and
- * symbols of `object`.
- *
- * @private
- * @param {Object} object The object to query.
- * @param {Function} keysFunc The function to get the keys of `object`.
- * @param {Function} symbolsFunc The function to get the symbols of `object`.
- * @returns {Array} Returns the array of property names and symbols.
- */
-function baseGetAllKeys(object, keysFunc, symbolsFunc) {
- var result = keysFunc(object);
- return isArray(object) ? result : arrayPush(result, symbolsFunc(object));
-}
-
-/**
- * The base implementation of `getTag` without fallbacks for buggy environments.
- *
- * @private
- * @param {*} value The value to query.
- * @returns {string} Returns the `toStringTag`.
- */
-function baseGetTag(value) {
- if (value == null) {
- return value === undefined ? undefinedTag : nullTag;
- }
- return (symToStringTag && symToStringTag in Object(value))
- ? getRawTag(value)
- : objectToString(value);
-}
-
-/**
- * The base implementation of `_.isArguments`.
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is an `arguments` object,
- */
-function baseIsArguments(value) {
- return isObjectLike(value) && baseGetTag(value) == argsTag;
-}
-
-/**
- * The base implementation of `_.isEqual` which supports partial comparisons
- * and tracks traversed objects.
- *
- * @private
- * @param {*} value The value to compare.
- * @param {*} other The other value to compare.
- * @param {boolean} bitmask The bitmask flags.
- * 1 - Unordered comparison
- * 2 - Partial comparison
- * @param {Function} [customizer] The function to customize comparisons.
- * @param {Object} [stack] Tracks traversed `value` and `other` objects.
- * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
- */
-function baseIsEqual(value, other, bitmask, customizer, stack) {
- if (value === other) {
- return true;
- }
- if (value == null || other == null || (!isObjectLike(value) && !isObjectLike(other))) {
- return value !== value && other !== other;
- }
- return baseIsEqualDeep(value, other, bitmask, customizer, baseIsEqual, stack);
-}
-
-/**
- * A specialized version of `baseIsEqual` for arrays and objects which performs
- * deep comparisons and tracks traversed objects enabling objects with circular
- * references to be compared.
- *
- * @private
- * @param {Object} object The object to compare.
- * @param {Object} other The other object to compare.
- * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
- * @param {Function} customizer The function to customize comparisons.
- * @param {Function} equalFunc The function to determine equivalents of values.
- * @param {Object} [stack] Tracks traversed `object` and `other` objects.
- * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
- */
-function baseIsEqualDeep(object, other, bitmask, customizer, equalFunc, stack) {
- var objIsArr = isArray(object),
- othIsArr = isArray(other),
- objTag = objIsArr ? arrayTag : getTag(object),
- othTag = othIsArr ? arrayTag : getTag(other);
-
- objTag = objTag == argsTag ? objectTag : objTag;
- othTag = othTag == argsTag ? objectTag : othTag;
-
- var objIsObj = objTag == objectTag,
- othIsObj = othTag == objectTag,
- isSameTag = objTag == othTag;
-
- if (isSameTag && isBuffer(object)) {
- if (!isBuffer(other)) {
- return false;
- }
- objIsArr = true;
- objIsObj = false;
- }
- if (isSameTag && !objIsObj) {
- stack || (stack = new Stack);
- return (objIsArr || isTypedArray(object))
- ? equalArrays(object, other, bitmask, customizer, equalFunc, stack)
- : equalByTag(object, other, objTag, bitmask, customizer, equalFunc, stack);
- }
- if (!(bitmask & COMPARE_PARTIAL_FLAG)) {
- var objIsWrapped = objIsObj && hasOwnProperty.call(object, '__wrapped__'),
- othIsWrapped = othIsObj && hasOwnProperty.call(other, '__wrapped__');
-
- if (objIsWrapped || othIsWrapped) {
- var objUnwrapped = objIsWrapped ? object.value() : object,
- othUnwrapped = othIsWrapped ? other.value() : other;
-
- stack || (stack = new Stack);
- return equalFunc(objUnwrapped, othUnwrapped, bitmask, customizer, stack);
- }
- }
- if (!isSameTag) {
- return false;
- }
- stack || (stack = new Stack);
- return equalObjects(object, other, bitmask, customizer, equalFunc, stack);
-}
-
-/**
- * The base implementation of `_.isNative` without bad shim checks.
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a native function,
- * else `false`.
- */
-function baseIsNative(value) {
- if (!isObject(value) || isMasked(value)) {
- return false;
- }
- var pattern = isFunction(value) ? reIsNative : reIsHostCtor;
- return pattern.test(toSource(value));
-}
-
-/**
- * The base implementation of `_.isTypedArray` without Node.js optimizations.
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
- */
-function baseIsTypedArray(value) {
- return isObjectLike(value) &&
- isLength(value.length) && !!typedArrayTags[baseGetTag(value)];
-}
-
-/**
- * The base implementation of `_.keys` which doesn't treat sparse arrays as dense.
- *
- * @private
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of property names.
- */
-function baseKeys(object) {
- if (!isPrototype(object)) {
- return nativeKeys(object);
- }
- var result = [];
- for (var key in Object(object)) {
- if (hasOwnProperty.call(object, key) && key != 'constructor') {
- result.push(key);
- }
- }
- return result;
-}
-
-/**
- * A specialized version of `baseIsEqualDeep` for arrays with support for
- * partial deep comparisons.
- *
- * @private
- * @param {Array} array The array to compare.
- * @param {Array} other The other array to compare.
- * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
- * @param {Function} customizer The function to customize comparisons.
- * @param {Function} equalFunc The function to determine equivalents of values.
- * @param {Object} stack Tracks traversed `array` and `other` objects.
- * @returns {boolean} Returns `true` if the arrays are equivalent, else `false`.
- */
-function equalArrays(array, other, bitmask, customizer, equalFunc, stack) {
- var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
- arrLength = array.length,
- othLength = other.length;
-
- if (arrLength != othLength && !(isPartial && othLength > arrLength)) {
- return false;
- }
- // Assume cyclic values are equal.
- var stacked = stack.get(array);
- if (stacked && stack.get(other)) {
- return stacked == other;
- }
- var index = -1,
- result = true,
- seen = (bitmask & COMPARE_UNORDERED_FLAG) ? new SetCache : undefined;
-
- stack.set(array, other);
- stack.set(other, array);
-
- // Ignore non-index properties.
- while (++index < arrLength) {
- var arrValue = array[index],
- othValue = other[index];
-
- if (customizer) {
- var compared = isPartial
- ? customizer(othValue, arrValue, index, other, array, stack)
- : customizer(arrValue, othValue, index, array, other, stack);
- }
- if (compared !== undefined) {
- if (compared) {
- continue;
- }
- result = false;
- break;
- }
- // Recursively compare arrays (susceptible to call stack limits).
- if (seen) {
- if (!arraySome(other, function(othValue, othIndex) {
- if (!cacheHas(seen, othIndex) &&
- (arrValue === othValue || equalFunc(arrValue, othValue, bitmask, customizer, stack))) {
- return seen.push(othIndex);
- }
- })) {
- result = false;
- break;
- }
- } else if (!(
- arrValue === othValue ||
- equalFunc(arrValue, othValue, bitmask, customizer, stack)
- )) {
- result = false;
- break;
- }
- }
- stack['delete'](array);
- stack['delete'](other);
- return result;
-}
-
-/**
- * A specialized version of `baseIsEqualDeep` for comparing objects of
- * the same `toStringTag`.
- *
- * **Note:** This function only supports comparing values with tags of
- * `Boolean`, `Date`, `Error`, `Number`, `RegExp`, or `String`.
- *
- * @private
- * @param {Object} object The object to compare.
- * @param {Object} other The other object to compare.
- * @param {string} tag The `toStringTag` of the objects to compare.
- * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
- * @param {Function} customizer The function to customize comparisons.
- * @param {Function} equalFunc The function to determine equivalents of values.
- * @param {Object} stack Tracks traversed `object` and `other` objects.
- * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
- */
-function equalByTag(object, other, tag, bitmask, customizer, equalFunc, stack) {
- switch (tag) {
- case dataViewTag:
- if ((object.byteLength != other.byteLength) ||
- (object.byteOffset != other.byteOffset)) {
- return false;
- }
- object = object.buffer;
- other = other.buffer;
-
- case arrayBufferTag:
- if ((object.byteLength != other.byteLength) ||
- !equalFunc(new Uint8Array(object), new Uint8Array(other))) {
- return false;
- }
- return true;
-
- case boolTag:
- case dateTag:
- case numberTag:
- // Coerce booleans to `1` or `0` and dates to milliseconds.
- // Invalid dates are coerced to `NaN`.
- return eq(+object, +other);
-
- case errorTag:
- return object.name == other.name && object.message == other.message;
-
- case regexpTag:
- case stringTag:
- // Coerce regexes to strings and treat strings, primitives and objects,
- // as equal. See http://www.ecma-international.org/ecma-262/7.0/#sec-regexp.prototype.tostring
- // for more details.
- return object == (other + '');
-
- case mapTag:
- var convert = mapToArray;
-
- case setTag:
- var isPartial = bitmask & COMPARE_PARTIAL_FLAG;
- convert || (convert = setToArray);
-
- if (object.size != other.size && !isPartial) {
- return false;
- }
- // Assume cyclic values are equal.
- var stacked = stack.get(object);
- if (stacked) {
- return stacked == other;
- }
- bitmask |= COMPARE_UNORDERED_FLAG;
-
- // Recursively compare objects (susceptible to call stack limits).
- stack.set(object, other);
- var result = equalArrays(convert(object), convert(other), bitmask, customizer, equalFunc, stack);
- stack['delete'](object);
- return result;
-
- case symbolTag:
- if (symbolValueOf) {
- return symbolValueOf.call(object) == symbolValueOf.call(other);
- }
- }
- return false;
-}
-
-/**
- * A specialized version of `baseIsEqualDeep` for objects with support for
- * partial deep comparisons.
- *
- * @private
- * @param {Object} object The object to compare.
- * @param {Object} other The other object to compare.
- * @param {number} bitmask The bitmask flags. See `baseIsEqual` for more details.
- * @param {Function} customizer The function to customize comparisons.
- * @param {Function} equalFunc The function to determine equivalents of values.
- * @param {Object} stack Tracks traversed `object` and `other` objects.
- * @returns {boolean} Returns `true` if the objects are equivalent, else `false`.
- */
-function equalObjects(object, other, bitmask, customizer, equalFunc, stack) {
- var isPartial = bitmask & COMPARE_PARTIAL_FLAG,
- objProps = getAllKeys(object),
- objLength = objProps.length,
- othProps = getAllKeys(other),
- othLength = othProps.length;
-
- if (objLength != othLength && !isPartial) {
- return false;
- }
- var index = objLength;
- while (index--) {
- var key = objProps[index];
- if (!(isPartial ? key in other : hasOwnProperty.call(other, key))) {
- return false;
- }
- }
- // Assume cyclic values are equal.
- var stacked = stack.get(object);
- if (stacked && stack.get(other)) {
- return stacked == other;
- }
- var result = true;
- stack.set(object, other);
- stack.set(other, object);
-
- var skipCtor = isPartial;
- while (++index < objLength) {
- key = objProps[index];
- var objValue = object[key],
- othValue = other[key];
-
- if (customizer) {
- var compared = isPartial
- ? customizer(othValue, objValue, key, other, object, stack)
- : customizer(objValue, othValue, key, object, other, stack);
- }
- // Recursively compare objects (susceptible to call stack limits).
- if (!(compared === undefined
- ? (objValue === othValue || equalFunc(objValue, othValue, bitmask, customizer, stack))
- : compared
- )) {
- result = false;
- break;
- }
- skipCtor || (skipCtor = key == 'constructor');
- }
- if (result && !skipCtor) {
- var objCtor = object.constructor,
- othCtor = other.constructor;
-
- // Non `Object` object instances with different constructors are not equal.
- if (objCtor != othCtor &&
- ('constructor' in object && 'constructor' in other) &&
- !(typeof objCtor == 'function' && objCtor instanceof objCtor &&
- typeof othCtor == 'function' && othCtor instanceof othCtor)) {
- result = false;
- }
- }
- stack['delete'](object);
- stack['delete'](other);
- return result;
-}
-
-/**
- * Creates an array of own enumerable property names and symbols of `object`.
- *
- * @private
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of property names and symbols.
- */
-function getAllKeys(object) {
- return baseGetAllKeys(object, keys, getSymbols);
-}
-
-/**
- * Gets the data for `map`.
- *
- * @private
- * @param {Object} map The map to query.
- * @param {string} key The reference key.
- * @returns {*} Returns the map data.
- */
-function getMapData(map, key) {
- var data = map.__data__;
- return isKeyable(key)
- ? data[typeof key == 'string' ? 'string' : 'hash']
- : data.map;
-}
-
-/**
- * Gets the native function at `key` of `object`.
- *
- * @private
- * @param {Object} object The object to query.
- * @param {string} key The key of the method to get.
- * @returns {*} Returns the function if it's native, else `undefined`.
- */
-function getNative(object, key) {
- var value = getValue(object, key);
- return baseIsNative(value) ? value : undefined;
-}
-
-/**
- * A specialized version of `baseGetTag` which ignores `Symbol.toStringTag` values.
- *
- * @private
- * @param {*} value The value to query.
- * @returns {string} Returns the raw `toStringTag`.
- */
-function getRawTag(value) {
- var isOwn = hasOwnProperty.call(value, symToStringTag),
- tag = value[symToStringTag];
-
- try {
- value[symToStringTag] = undefined;
- var unmasked = true;
- } catch (e) {}
-
- var result = nativeObjectToString.call(value);
- if (unmasked) {
- if (isOwn) {
- value[symToStringTag] = tag;
- } else {
- delete value[symToStringTag];
- }
- }
- return result;
-}
-
-/**
- * Creates an array of the own enumerable symbols of `object`.
- *
- * @private
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of symbols.
- */
-var getSymbols = !nativeGetSymbols ? stubArray : function(object) {
- if (object == null) {
- return [];
- }
- object = Object(object);
- return arrayFilter(nativeGetSymbols(object), function(symbol) {
- return propertyIsEnumerable.call(object, symbol);
- });
-};
-
-/**
- * Gets the `toStringTag` of `value`.
- *
- * @private
- * @param {*} value The value to query.
- * @returns {string} Returns the `toStringTag`.
- */
-var getTag = baseGetTag;
-
-// Fallback for data views, maps, sets, and weak maps in IE 11 and promises in Node.js < 6.
-if ((DataView && getTag(new DataView(new ArrayBuffer(1))) != dataViewTag) ||
- (Map && getTag(new Map) != mapTag) ||
- (Promise && getTag(Promise.resolve()) != promiseTag) ||
- (Set && getTag(new Set) != setTag) ||
- (WeakMap && getTag(new WeakMap) != weakMapTag)) {
- getTag = function(value) {
- var result = baseGetTag(value),
- Ctor = result == objectTag ? value.constructor : undefined,
- ctorString = Ctor ? toSource(Ctor) : '';
-
- if (ctorString) {
- switch (ctorString) {
- case dataViewCtorString: return dataViewTag;
- case mapCtorString: return mapTag;
- case promiseCtorString: return promiseTag;
- case setCtorString: return setTag;
- case weakMapCtorString: return weakMapTag;
- }
- }
- return result;
- };
-}
-
-/**
- * Checks if `value` is a valid array-like index.
- *
- * @private
- * @param {*} value The value to check.
- * @param {number} [length=MAX_SAFE_INTEGER] The upper bounds of a valid index.
- * @returns {boolean} Returns `true` if `value` is a valid index, else `false`.
- */
-function isIndex(value, length) {
- length = length == null ? MAX_SAFE_INTEGER : length;
- return !!length &&
- (typeof value == 'number' || reIsUint.test(value)) &&
- (value > -1 && value % 1 == 0 && value < length);
-}
-
-/**
- * Checks if `value` is suitable for use as unique object key.
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is suitable, else `false`.
- */
-function isKeyable(value) {
- var type = typeof value;
- return (type == 'string' || type == 'number' || type == 'symbol' || type == 'boolean')
- ? (value !== '__proto__')
- : (value === null);
-}
-
-/**
- * Checks if `func` has its source masked.
- *
- * @private
- * @param {Function} func The function to check.
- * @returns {boolean} Returns `true` if `func` is masked, else `false`.
- */
-function isMasked(func) {
- return !!maskSrcKey && (maskSrcKey in func);
-}
-
-/**
- * Checks if `value` is likely a prototype object.
- *
- * @private
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a prototype, else `false`.
- */
-function isPrototype(value) {
- var Ctor = value && value.constructor,
- proto = (typeof Ctor == 'function' && Ctor.prototype) || objectProto;
-
- return value === proto;
-}
-
-/**
- * Converts `value` to a string using `Object.prototype.toString`.
- *
- * @private
- * @param {*} value The value to convert.
- * @returns {string} Returns the converted string.
- */
-function objectToString(value) {
- return nativeObjectToString.call(value);
-}
-
-/**
- * Converts `func` to its source code.
- *
- * @private
- * @param {Function} func The function to convert.
- * @returns {string} Returns the source code.
- */
-function toSource(func) {
- if (func != null) {
- try {
- return funcToString.call(func);
- } catch (e) {}
- try {
- return (func + '');
- } catch (e) {}
- }
- return '';
-}
-
-/**
- * Performs a
- * [`SameValueZero`](http://ecma-international.org/ecma-262/7.0/#sec-samevaluezero)
- * comparison between two values to determine if they are equivalent.
- *
- * @static
- * @memberOf _
- * @since 4.0.0
- * @category Lang
- * @param {*} value The value to compare.
- * @param {*} other The other value to compare.
- * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
- * @example
- *
- * var object = { 'a': 1 };
- * var other = { 'a': 1 };
- *
- * _.eq(object, object);
- * // => true
- *
- * _.eq(object, other);
- * // => false
- *
- * _.eq('a', 'a');
- * // => true
- *
- * _.eq('a', Object('a'));
- * // => false
- *
- * _.eq(NaN, NaN);
- * // => true
- */
-function eq(value, other) {
- return value === other || (value !== value && other !== other);
-}
-
-/**
- * Checks if `value` is likely an `arguments` object.
- *
- * @static
- * @memberOf _
- * @since 0.1.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is an `arguments` object,
- * else `false`.
- * @example
- *
- * _.isArguments(function() { return arguments; }());
- * // => true
- *
- * _.isArguments([1, 2, 3]);
- * // => false
- */
-var isArguments = baseIsArguments(function() { return arguments; }()) ? baseIsArguments : function(value) {
- return isObjectLike(value) && hasOwnProperty.call(value, 'callee') &&
- !propertyIsEnumerable.call(value, 'callee');
-};
-
-/**
- * Checks if `value` is classified as an `Array` object.
- *
- * @static
- * @memberOf _
- * @since 0.1.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is an array, else `false`.
- * @example
- *
- * _.isArray([1, 2, 3]);
- * // => true
- *
- * _.isArray(document.body.children);
- * // => false
- *
- * _.isArray('abc');
- * // => false
- *
- * _.isArray(_.noop);
- * // => false
- */
-var isArray = Array.isArray;
-
-/**
- * Checks if `value` is array-like. A value is considered array-like if it's
- * not a function and has a `value.length` that's an integer greater than or
- * equal to `0` and less than or equal to `Number.MAX_SAFE_INTEGER`.
- *
- * @static
- * @memberOf _
- * @since 4.0.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is array-like, else `false`.
- * @example
- *
- * _.isArrayLike([1, 2, 3]);
- * // => true
- *
- * _.isArrayLike(document.body.children);
- * // => true
- *
- * _.isArrayLike('abc');
- * // => true
- *
- * _.isArrayLike(_.noop);
- * // => false
- */
-function isArrayLike(value) {
- return value != null && isLength(value.length) && !isFunction(value);
-}
-
-/**
- * Checks if `value` is a buffer.
- *
- * @static
- * @memberOf _
- * @since 4.3.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a buffer, else `false`.
- * @example
- *
- * _.isBuffer(new Buffer(2));
- * // => true
- *
- * _.isBuffer(new Uint8Array(2));
- * // => false
- */
-var isBuffer = nativeIsBuffer || stubFalse;
-
-/**
- * Performs a deep comparison between two values to determine if they are
- * equivalent.
- *
- * **Note:** This method supports comparing arrays, array buffers, booleans,
- * date objects, error objects, maps, numbers, `Object` objects, regexes,
- * sets, strings, symbols, and typed arrays. `Object` objects are compared
- * by their own, not inherited, enumerable properties. Functions and DOM
- * nodes are compared by strict equality, i.e. `===`.
- *
- * @static
- * @memberOf _
- * @since 0.1.0
- * @category Lang
- * @param {*} value The value to compare.
- * @param {*} other The other value to compare.
- * @returns {boolean} Returns `true` if the values are equivalent, else `false`.
- * @example
- *
- * var object = { 'a': 1 };
- * var other = { 'a': 1 };
- *
- * _.isEqual(object, other);
- * // => true
- *
- * object === other;
- * // => false
- */
-function isEqual(value, other) {
- return baseIsEqual(value, other);
-}
-
-/**
- * Checks if `value` is classified as a `Function` object.
- *
- * @static
- * @memberOf _
- * @since 0.1.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a function, else `false`.
- * @example
- *
- * _.isFunction(_);
- * // => true
- *
- * _.isFunction(/abc/);
- * // => false
- */
-function isFunction(value) {
- if (!isObject(value)) {
- return false;
- }
- // The use of `Object#toString` avoids issues with the `typeof` operator
- // in Safari 9 which returns 'object' for typed arrays and other constructors.
- var tag = baseGetTag(value);
- return tag == funcTag || tag == genTag || tag == asyncTag || tag == proxyTag;
-}
-
-/**
- * Checks if `value` is a valid array-like length.
- *
- * **Note:** This method is loosely based on
- * [`ToLength`](http://ecma-international.org/ecma-262/7.0/#sec-tolength).
- *
- * @static
- * @memberOf _
- * @since 4.0.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a valid length, else `false`.
- * @example
- *
- * _.isLength(3);
- * // => true
- *
- * _.isLength(Number.MIN_VALUE);
- * // => false
- *
- * _.isLength(Infinity);
- * // => false
- *
- * _.isLength('3');
- * // => false
- */
-function isLength(value) {
- return typeof value == 'number' &&
- value > -1 && value % 1 == 0 && value <= MAX_SAFE_INTEGER;
-}
-
-/**
- * Checks if `value` is the
- * [language type](http://www.ecma-international.org/ecma-262/7.0/#sec-ecmascript-language-types)
- * of `Object`. (e.g. arrays, functions, objects, regexes, `new Number(0)`, and `new String('')`)
- *
- * @static
- * @memberOf _
- * @since 0.1.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is an object, else `false`.
- * @example
- *
- * _.isObject({});
- * // => true
- *
- * _.isObject([1, 2, 3]);
- * // => true
- *
- * _.isObject(_.noop);
- * // => true
- *
- * _.isObject(null);
- * // => false
- */
-function isObject(value) {
- var type = typeof value;
- return value != null && (type == 'object' || type == 'function');
-}
-
-/**
- * Checks if `value` is object-like. A value is object-like if it's not `null`
- * and has a `typeof` result of "object".
- *
- * @static
- * @memberOf _
- * @since 4.0.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is object-like, else `false`.
- * @example
- *
- * _.isObjectLike({});
- * // => true
- *
- * _.isObjectLike([1, 2, 3]);
- * // => true
- *
- * _.isObjectLike(_.noop);
- * // => false
- *
- * _.isObjectLike(null);
- * // => false
- */
-function isObjectLike(value) {
- return value != null && typeof value == 'object';
-}
-
-/**
- * Checks if `value` is classified as a typed array.
- *
- * @static
- * @memberOf _
- * @since 3.0.0
- * @category Lang
- * @param {*} value The value to check.
- * @returns {boolean} Returns `true` if `value` is a typed array, else `false`.
- * @example
- *
- * _.isTypedArray(new Uint8Array);
- * // => true
- *
- * _.isTypedArray([]);
- * // => false
- */
-var isTypedArray = nodeIsTypedArray ? baseUnary(nodeIsTypedArray) : baseIsTypedArray;
-
-/**
- * Creates an array of the own enumerable property names of `object`.
- *
- * **Note:** Non-object values are coerced to objects. See the
- * [ES spec](http://ecma-international.org/ecma-262/7.0/#sec-object.keys)
- * for more details.
- *
- * @static
- * @since 0.1.0
- * @memberOf _
- * @category Object
- * @param {Object} object The object to query.
- * @returns {Array} Returns the array of property names.
- * @example
- *
- * function Foo() {
- * this.a = 1;
- * this.b = 2;
- * }
- *
- * Foo.prototype.c = 3;
- *
- * _.keys(new Foo);
- * // => ['a', 'b'] (iteration order is not guaranteed)
- *
- * _.keys('hi');
- * // => ['0', '1']
- */
-function keys(object) {
- return isArrayLike(object) ? arrayLikeKeys(object) : baseKeys(object);
-}
-
-/**
- * This method returns a new empty array.
- *
- * @static
- * @memberOf _
- * @since 4.13.0
- * @category Util
- * @returns {Array} Returns the new empty array.
- * @example
- *
- * var arrays = _.times(2, _.stubArray);
- *
- * console.log(arrays);
- * // => [[], []]
- *
- * console.log(arrays[0] === arrays[1]);
- * // => false
- */
-function stubArray() {
- return [];
-}
-
-/**
- * This method returns `false`.
- *
- * @static
- * @memberOf _
- * @since 4.13.0
- * @category Util
- * @returns {boolean} Returns `false`.
- * @example
- *
- * _.times(2, _.stubFalse);
- * // => [false, false]
- */
-function stubFalse() {
- return false;
-}
-
-module.exports = isEqual;
-
-
/***/ }),
/***/ 770:
@@ -32490,8 +30634,8 @@ function qstring(str) {
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = __webpack_module_cache__[moduleId] = {
-/******/ id: moduleId,
-/******/ loaded: false,
+/******/ // no module.id needed
+/******/ // no module.loaded needed
/******/ exports: {}
/******/ };
/******/
@@ -32504,23 +30648,11 @@ function qstring(str) {
/******/ if(threw) delete __webpack_module_cache__[moduleId];
/******/ }
/******/
-/******/ // Flag the module as loaded
-/******/ module.loaded = true;
-/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/************************************************************************/
-/******/ /* webpack/runtime/node module decorator */
-/******/ (() => {
-/******/ __nccwpck_require__.nmd = (module) => {
-/******/ module.paths = [];
-/******/ if (!module.children) module.children = [];
-/******/ return module;
-/******/ };
-/******/ })();
-/******/
/******/ /* webpack/runtime/compat */
/******/
/******/ if (typeof __nccwpck_require__ !== 'undefined') __nccwpck_require__.ab = new URL('.', import.meta.url).pathname.slice(import.meta.url.match(/^file:\/\/\/\w:/) ? 1 : 0, -1) + "/";
@@ -39874,6 +38006,50 @@ function retry(octokit, octokitOptions) {
retry.VERSION = plugin_retry_dist_bundle_VERSION;
+;// CONCATENATED MODULE: ./lib/api/add-labels.js
+
+const isServerError = (error) => typeof error === 'object' &&
+ error !== null &&
+ 'status' in error &&
+ typeof error.status === 'number' &&
+ error.status >= 500 &&
+ error.status < 600;
+const addLabels = async (client, prNumber, labels) => {
+ const request = {
+ owner: github_context.repo.owner,
+ repo: github_context.repo.repo,
+ issue_number: prNumber
+ };
+ try {
+ await client.rest.issues.addLabels({
+ ...request,
+ labels,
+ request: { retries: 0 }
+ });
+ }
+ catch (error) {
+ if (!isServerError(error)) {
+ throw error;
+ }
+ let currentLabels;
+ try {
+ currentLabels = await client.rest.issues.listLabelsOnIssue({
+ ...request,
+ per_page: 100,
+ request: { retries: 0 }
+ });
+ }
+ catch {
+ throw error;
+ }
+ const currentLabelNames = new Set(currentLabels.data.map(label => label.name.toLowerCase()));
+ if (labels.every(label => currentLabelNames.has(label.toLowerCase()))) {
+ return;
+ }
+ throw error;
+ }
+};
+
;// CONCATENATED MODULE: ./lib/api/get-changed-files.js
@@ -45922,15 +44098,18 @@ function configUsesChangedFiles(matchConfigs) {
return false;
}
-;// CONCATENATED MODULE: ./lib/api/set-labels.js
-
-const setLabels = async (client, prNumber, labels) => {
- await client.rest.issues.setLabels({
- owner: github_context.repo.owner,
- repo: github_context.repo.repo,
- issue_number: prNumber,
- labels: labels
- });
+;// CONCATENATED MODULE: ./lib/api/remove-labels.js
+const REMOVE_LABELS_MUTATION = `
+ mutation RemoveLabels($labelableId: ID!, $labelIds: [ID!]!) {
+ removeLabelsFromLabelable(
+ input: {labelableId: $labelableId, labelIds: $labelIds}
+ ) {
+ clientMutationId
+ }
+ }
+`;
+const removeLabels = async (client, labelableId, labelIds) => {
+ await client.graphql(REMOVE_LABELS_MUTATION, { labelableId, labelIds });
};
;// CONCATENATED MODULE: ./lib/api/index.js
@@ -45941,8 +44120,7 @@ const setLabels = async (client, prNumber, labels) => {
-// EXTERNAL MODULE: ./node_modules/lodash.isequal/index.js
-var lodash_isequal = __nccwpck_require__(9471);
+
;// CONCATENATED MODULE: ./lib/get-inputs/get-pr-numbers.js
@@ -45995,7 +44173,6 @@ const getInputs = () => ({
-
// GitHub Issues cannot have more than 100 labels
const GITHUB_MAX_LABELS = 100;
const run = () => labeler().catch(error => {
@@ -46054,46 +44231,44 @@ async function labeler() {
}
const labelsToApply = [...allLabels].slice(0, GITHUB_MAX_LABELS);
const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
- let finalLabels = labelsToApply;
- let newLabels = [];
+ const finalLabels = labelsToApply;
+ const newLabels = labelsToApply.filter(label => !preexistingLabels.includes(label));
+ const staleLabels = pullRequest.data.labels.filter(label => labelConfigs.has(label.name) && !allLabels.has(label.name));
try {
- if (!lodash_isequal(labelsToApply, preexistingLabels)) {
- // Fetch the latest labels for the PR
- const latestLabels = [];
- // Skip fetching real labels when running tests (uses mock data instead)
- if (process.env.NODE_ENV !== 'test') {
- const pr = await client.rest.pulls.get({
- ...github_context.repo,
- pull_number: pullRequest.number
- });
- latestLabels.push(...pr.data.labels.map(l => l.name).filter(Boolean));
+ if (staleLabels.length) {
+ const labelableId = pullRequest.data.node_id;
+ const missingNodeId = staleLabels.find(label => !label.node_id);
+ if (!labelableId || missingNodeId) {
+ throw new Error(`Failed to resolve node IDs while removing configured labels from PR #${pullRequest.number}`);
}
- // Labels added manually during the run (not in first snapshot)
- const manualAddedDuringRun = latestLabels.filter(l => !preexistingLabels.includes(l));
- // Preserve manual labels first, then apply config-based labels, respecting GitHub's 100-label limit
- finalLabels = [
- ...new Set([...manualAddedDuringRun, ...labelsToApply])
- ].slice(0, GITHUB_MAX_LABELS);
- await setLabels(client, pullRequest.number, finalLabels);
- newLabels = finalLabels.filter(l => !preexistingLabels.includes(l));
+ try {
+ await removeLabels(client, labelableId, staleLabels.map(label => label.node_id));
+ }
+ catch (error) {
+ throw new Error(`Failed to remove configured labels '${staleLabels.map(label => label.name).join("', '")}' from PR #${pullRequest.number}`, { cause: error });
+ }
+ }
+ if (newLabels.length) {
+ await addLabels(client, pullRequest.number, newLabels);
}
}
catch (error) {
- if (error.name === 'HttpError' &&
- error.status === 403 &&
- error.message.toLowerCase().includes('unauthorized')) {
- throw new Error(`Failed to set labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
+ const apiError = error.cause ?? error;
+ if (apiError.name === 'HttpError' &&
+ apiError.status === 403 &&
+ apiError.message.toLowerCase().includes('unauthorized')) {
+ throw new Error(`Failed to update labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`, { cause: error });
}
- else if (error.name !== 'HttpError' ||
- error.message !== 'Resource not accessible by integration') {
+ else if (apiError.name !== 'HttpError' ||
+ apiError.message !== 'Resource not accessible by integration') {
throw error;
}
warning(`The action requires 'issues: write' permission to create new labels or 'pull-requests: write' permission to add existing labels to pull requests. ` +
`For more information, refer to the action documentation: https://github.com/actions/labeler#recommended-permissions`, {
title: `${process.env['GITHUB_ACTION_REPOSITORY']} running under '${github_context.eventName}' is misconfigured`
});
- setFailed(error.message);
+ setFailed(apiError.message);
return;
}
setOutput('new-labels', newLabels.join(','));
diff --git a/package-lock.json b/package-lock.json
index 9b163cc89..ec66259f8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,13 +13,11 @@
"@actions/github": "^9.1.1",
"@octokit/plugin-retry": "^8.1.0",
"js-yaml": "^5.1.0",
- "lodash.isequal": "^4.5.0",
"minimatch": "^10.2.5"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
- "@types/lodash.isequal": "^4.5.8",
"@types/node": "^24.13.2",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
@@ -1668,21 +1666,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/lodash": {
- "version": "4.14.200",
- "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.200.tgz",
- "integrity": "sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==",
- "dev": true
- },
- "node_modules/@types/lodash.isequal": {
- "version": "4.5.8",
- "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.8.tgz",
- "integrity": "sha512-uput6pg4E/tj2LGxCZo9+y27JNyB2OZuuI/T5F+ylVDYuqICLG2/ktjxx0v6GvVntAf8TvEzeQLcV0ffRirXuA==",
- "dev": true,
- "dependencies": {
- "@types/lodash": "*"
- }
- },
"node_modules/@types/node": {
"version": "24.13.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz",
@@ -4700,11 +4683,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/lodash.isequal": {
- "version": "4.5.0",
- "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
- "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="
- },
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
diff --git a/package.json b/package.json
index 6c2a694b9..a112779cd 100644
--- a/package.json
+++ b/package.json
@@ -32,13 +32,11 @@
"@actions/github": "^9.1.1",
"@octokit/plugin-retry": "^8.1.0",
"js-yaml": "^5.1.0",
- "lodash.isequal": "^4.5.0",
"minimatch": "^10.2.5"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@jest/globals": "^30.4.1",
- "@types/lodash.isequal": "^4.5.8",
"@types/node": "^24.13.2",
"@typescript-eslint/eslint-plugin": "^8.62.0",
"@typescript-eslint/parser": "^8.62.0",
diff --git a/src/api/add-labels.ts b/src/api/add-labels.ts
new file mode 100644
index 000000000..c7ed57669
--- /dev/null
+++ b/src/api/add-labels.ts
@@ -0,0 +1,54 @@
+import * as github from '@actions/github';
+import {ClientType} from './types.js';
+
+const isServerError = (error: unknown): error is {status: number} =>
+ typeof error === 'object' &&
+ error !== null &&
+ 'status' in error &&
+ typeof error.status === 'number' &&
+ error.status >= 500 &&
+ error.status < 600;
+
+export const addLabels = async (
+ client: ClientType,
+ prNumber: number,
+ labels: string[]
+) => {
+ const request = {
+ owner: github.context.repo.owner,
+ repo: github.context.repo.repo,
+ issue_number: prNumber
+ };
+
+ try {
+ await client.rest.issues.addLabels({
+ ...request,
+ labels,
+ request: {retries: 0}
+ });
+ } catch (error: unknown) {
+ if (!isServerError(error)) {
+ throw error;
+ }
+
+ let currentLabels;
+ try {
+ currentLabels = await client.rest.issues.listLabelsOnIssue({
+ ...request,
+ per_page: 100,
+ request: {retries: 0}
+ });
+ } catch {
+ throw error;
+ }
+
+ const currentLabelNames = new Set(
+ currentLabels.data.map(label => label.name.toLowerCase())
+ );
+ if (labels.every(label => currentLabelNames.has(label.toLowerCase()))) {
+ return;
+ }
+
+ throw error;
+ }
+};
diff --git a/src/api/index.ts b/src/api/index.ts
index 1d96fc5c3..4507cff1e 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1,6 +1,7 @@
+export * from './add-labels.js';
export * from './get-changed-files.js';
export * from './get-changed-pull-requests.js';
export * from './get-content.js';
export * from './get-label-configs.js';
-export * from './set-labels.js';
+export * from './remove-labels.js';
export * from './types.js';
diff --git a/src/api/remove-labels.ts b/src/api/remove-labels.ts
new file mode 100644
index 000000000..9d7fa04bd
--- /dev/null
+++ b/src/api/remove-labels.ts
@@ -0,0 +1,19 @@
+import {ClientType} from './types.js';
+
+const REMOVE_LABELS_MUTATION = `
+ mutation RemoveLabels($labelableId: ID!, $labelIds: [ID!]!) {
+ removeLabelsFromLabelable(
+ input: {labelableId: $labelableId, labelIds: $labelIds}
+ ) {
+ clientMutationId
+ }
+ }
+`;
+
+export const removeLabels = async (
+ client: ClientType,
+ labelableId: string,
+ labelIds: string[]
+) => {
+ await client.graphql(REMOVE_LABELS_MUTATION, {labelableId, labelIds});
+};
diff --git a/src/api/set-labels.ts b/src/api/set-labels.ts
deleted file mode 100644
index d45fadad0..000000000
--- a/src/api/set-labels.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import * as github from '@actions/github';
-import {ClientType} from './types.js';
-
-export const setLabels = async (
- client: ClientType,
- prNumber: number,
- labels: string[]
-) => {
- await client.rest.issues.setLabels({
- owner: github.context.repo.owner,
- repo: github.context.repo.repo,
- issue_number: prNumber,
- labels: labels
- });
-};
diff --git a/src/labeler.ts b/src/labeler.ts
index bcd32b5d9..fe336d27a 100644
--- a/src/labeler.ts
+++ b/src/labeler.ts
@@ -2,7 +2,6 @@ import * as core from '@actions/core';
import * as github from '@actions/github';
import * as pluginRetry from '@octokit/plugin-retry';
import * as api from './api/index.js';
-import isEqual from 'lodash.isequal';
import {getInputs} from './get-inputs/index.js';
import {
@@ -104,50 +103,56 @@ export async function labeler() {
const labelsToApply = [...allLabels].slice(0, GITHUB_MAX_LABELS);
const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS);
- let finalLabels = labelsToApply;
- let newLabels: string[] = [];
+ const finalLabels = labelsToApply;
+ const newLabels = labelsToApply.filter(
+ label => !preexistingLabels.includes(label)
+ );
+ const staleLabels = pullRequest.data.labels.filter(
+ label => labelConfigs.has(label.name) && !allLabels.has(label.name)
+ );
try {
- if (!isEqual(labelsToApply, preexistingLabels)) {
- // Fetch the latest labels for the PR
- const latestLabels: string[] = [];
- // Skip fetching real labels when running tests (uses mock data instead)
- if (process.env.NODE_ENV !== 'test') {
- const pr = await client.rest.pulls.get({
- ...github.context.repo,
- pull_number: pullRequest.number
- });
- latestLabels.push(...pr.data.labels.map(l => l.name).filter(Boolean));
+ if (staleLabels.length) {
+ const labelableId = pullRequest.data.node_id;
+ const missingNodeId = staleLabels.find(label => !label.node_id);
+ if (!labelableId || missingNodeId) {
+ throw new Error(
+ `Failed to resolve node IDs while removing configured labels from PR #${pullRequest.number}`
+ );
}
- // Labels added manually during the run (not in first snapshot)
- const manualAddedDuringRun = latestLabels.filter(
- l => !preexistingLabels.includes(l)
- );
-
- // Preserve manual labels first, then apply config-based labels, respecting GitHub's 100-label limit
- finalLabels = [
- ...new Set([...manualAddedDuringRun, ...labelsToApply])
- ].slice(0, GITHUB_MAX_LABELS);
-
- await api.setLabels(client, pullRequest.number, finalLabels);
+ try {
+ await api.removeLabels(
+ client,
+ labelableId,
+ staleLabels.map(label => label.node_id)
+ );
+ } catch (error: any) {
+ throw new Error(
+ `Failed to remove configured labels '${staleLabels.map(label => label.name).join("', '")}' from PR #${pullRequest.number}`,
+ {cause: error}
+ );
+ }
+ }
- newLabels = finalLabels.filter(l => !preexistingLabels.includes(l));
+ if (newLabels.length) {
+ await api.addLabels(client, pullRequest.number, newLabels);
}
} catch (error: any) {
+ const apiError = error.cause ?? error;
if (
- error.name === 'HttpError' &&
- error.status === 403 &&
- error.message.toLowerCase().includes('unauthorized')
+ apiError.name === 'HttpError' &&
+ apiError.status === 403 &&
+ apiError.message.toLowerCase().includes('unauthorized')
) {
throw new Error(
- `Failed to set labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
+ `Failed to update labels for PR #${pullRequest.number}. The workflow does not have permission to create labels. ` +
`Ensure the 'issues: write' permission is granted in the workflow file or manually create the missing labels in the repository before running the action.`,
{cause: error}
);
} else if (
- error.name !== 'HttpError' ||
- error.message !== 'Resource not accessible by integration'
+ apiError.name !== 'HttpError' ||
+ apiError.message !== 'Resource not accessible by integration'
) {
throw error;
}
@@ -160,7 +165,7 @@ export async function labeler() {
}
);
- core.setFailed(error.message);
+ core.setFailed(apiError.message);
return;
}