From 756f0bd85e70eee586582c98776d348e5d12f7ec Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Thu, 2 Feb 2023 16:32:43 -0500 Subject: [PATCH 01/20] push to excess labels to avoid reaching the limit --- src/labeler.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/labeler.ts b/src/labeler.ts index b33073adc..b7ad516ff 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -11,6 +11,9 @@ interface MatchConfig { type StringOrMatchConfig = string | MatchConfig; type ClientType = ReturnType; +// Github Issues cannot have more than 100 labels +const GITHUB_MAX_LABELS = 100; + export async function run() { try { const token = core.getInput('repo-token', {required: true}); @@ -40,21 +43,30 @@ export async function run() { const labels: string[] = []; const labelsToRemove: string[] = []; + const excessLabels: string[] = []; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs)) { - labels.push(label); + if (labels.length >= GITHUB_MAX_LABELS) { + excessLabels.push(label); + } else { + labels.push(label); + } } else if (pullRequest.labels.find(l => l.name === label)) { labelsToRemove.push(label); } } + if (syncLabels && labelsToRemove.length) { + await removeLabels(client, prNumber, labelsToRemove); + } + if (labels.length > 0) { await addLabels(client, prNumber, labels); } - if (syncLabels && labelsToRemove.length) { - await removeLabels(client, prNumber, labelsToRemove); + if (excessLabels.length > 0) { + core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); } } catch (error: any) { core.error(error); From e4e6956d06daccaaad67d03d6cf1d63f77059c76 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Thu, 2 Feb 2023 16:44:30 -0500 Subject: [PATCH 02/20] build dist --- dist/index.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/dist/index.js b/dist/index.js index 4b521af64..31088df4d 100644 --- a/dist/index.js +++ b/dist/index.js @@ -44,6 +44,8 @@ const core = __importStar(__nccwpck_require__(2186)); const github = __importStar(__nccwpck_require__(5438)); const yaml = __importStar(__nccwpck_require__(1917)); const minimatch_1 = __nccwpck_require__(3973); +// Github Issues cannot have more than 100 labels +const GITHUB_MAX_LABELS = 100; function run() { return __awaiter(this, void 0, void 0, function* () { try { @@ -66,20 +68,29 @@ function run() { const labelGlobs = yield getLabelGlobs(client, configPath); const labels = []; const labelsToRemove = []; + const excessLabels = []; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs)) { - labels.push(label); + if (labels.length >= GITHUB_MAX_LABELS) { + excessLabels.push(label); + } + else { + labels.push(label); + } } else if (pullRequest.labels.find(l => l.name === label)) { labelsToRemove.push(label); } } + if (syncLabels && labelsToRemove.length) { + yield removeLabels(client, prNumber, labelsToRemove); + } if (labels.length > 0) { yield addLabels(client, prNumber, labels); } - if (syncLabels && labelsToRemove.length) { - yield removeLabels(client, prNumber, labelsToRemove); + if (excessLabels.length > 0) { + core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); } } catch (error) { From 7429fbb1059adc0c2ce7bfb5854dc0f42ff7cb71 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Tue, 21 Mar 2023 19:36:51 -0400 Subject: [PATCH 03/20] never set more than 100 labels --- dist/index.js | 31 ++++++++++++++----------------- src/labeler.ts | 45 ++++++++++++++++++++------------------------- 2 files changed, 34 insertions(+), 42 deletions(-) diff --git a/dist/index.js b/dist/index.js index 31088df4d..dd318ecee 100644 --- a/dist/index.js +++ b/dist/index.js @@ -66,30 +66,27 @@ function run() { core.debug(`fetching changed files for pr #${prNumber}`); const changedFiles = yield getChangedFiles(client, prNumber); const labelGlobs = yield getLabelGlobs(client, configPath); - const labels = []; - const labelsToRemove = []; + const pullRequestLabels = pullRequest.labels.map(label => label.name); + const labels = new Set(syncLabels ? [] : pullRequestLabels); const excessLabels = []; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs)) { - if (labels.length >= GITHUB_MAX_LABELS) { - excessLabels.push(label); + if (labels.size < GITHUB_MAX_LABELS) { + labels.add(label); } else { - labels.push(label); + excessLabels.push(label); } } - else if (pullRequest.labels.find(l => l.name === label)) { - labelsToRemove.push(label); - } } - if (syncLabels && labelsToRemove.length) { - yield removeLabels(client, prNumber, labelsToRemove); + if (syncLabels) { + yield setLabels(client, prNumber, Array.from(labels)); } - if (labels.length > 0) { - yield addLabels(client, prNumber, labels); + else { + yield addLabels(client, prNumber, Array.from(labels)); } - if (excessLabels.length > 0) { + if (excessLabels.length) { core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); } } @@ -241,14 +238,14 @@ function addLabels(client, prNumber, labels) { }); }); } -function removeLabels(client, prNumber, labels) { +function setLabels(client, prNumber, labels) { return __awaiter(this, void 0, void 0, function* () { - yield Promise.all(labels.map(label => client.rest.issues.removeLabel({ + yield client.rest.issues.setLabels({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, - name: label - }))); + labels: labels + }); }); } diff --git a/src/labeler.ts b/src/labeler.ts index b7ad516ff..e6c8828b3 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -41,31 +41,30 @@ export async function run() { configPath ); - const labels: string[] = []; - const labelsToRemove: string[] = []; + const pullRequestLabels: string[] = pullRequest.labels.map( + label => label.name + ); + const labels: Set = new Set(syncLabels ? [] : pullRequestLabels); const excessLabels: string[] = []; + for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs)) { - if (labels.length >= GITHUB_MAX_LABELS) { - excessLabels.push(label); + if (labels.size < GITHUB_MAX_LABELS) { + labels.add(label); } else { - labels.push(label); + excessLabels.push(label); } - } else if (pullRequest.labels.find(l => l.name === label)) { - labelsToRemove.push(label); } } - if (syncLabels && labelsToRemove.length) { - await removeLabels(client, prNumber, labelsToRemove); - } - - if (labels.length > 0) { - await addLabels(client, prNumber, labels); + if (syncLabels) { + await setLabels(client, prNumber, Array.from(labels)); + } else { + await addLabels(client, prNumber, Array.from(labels)); } - if (excessLabels.length > 0) { + if (excessLabels.length) { core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); } } catch (error: any) { @@ -254,19 +253,15 @@ async function addLabels( }); } -async function removeLabels( +async function setLabels( client: ClientType, prNumber: number, labels: string[] ) { - await Promise.all( - labels.map(label => - client.rest.issues.removeLabel({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - issue_number: prNumber, - name: label - }) - ) - ); + await client.rest.issues.setLabels({ + owner: github.context.repo.owner, + repo: github.context.repo.repo, + issue_number: prNumber, + labels: labels + }); } From e173de71f36d2e6a4cdcd86b0a94639a684c9422 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Tue, 21 Mar 2023 19:56:10 -0400 Subject: [PATCH 04/20] use splice instead of set --- dist/index.js | 15 ++++++--------- src/labeler.ts | 19 ++++++++----------- 2 files changed, 14 insertions(+), 20 deletions(-) diff --git a/dist/index.js b/dist/index.js index dd318ecee..4aa0b5aa9 100644 --- a/dist/index.js +++ b/dist/index.js @@ -67,19 +67,16 @@ function run() { const changedFiles = yield getChangedFiles(client, prNumber); const labelGlobs = yield getLabelGlobs(client, configPath); const pullRequestLabels = pullRequest.labels.map(label => label.name); - const labels = new Set(syncLabels ? [] : pullRequestLabels); - const excessLabels = []; + const labels = syncLabels ? [] : pullRequestLabels; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs)) { - if (labels.size < GITHUB_MAX_LABELS) { - labels.add(label); - } - else { - excessLabels.push(label); - } + if (checkGlobs(changedFiles, globs) && !labels.includes(label)) { + labels.push(label); } } + // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, + // and extract the excess into `excessLabels` + const excessLabels = labels.splice(GITHUB_MAX_LABELS); if (syncLabels) { yield setLabels(client, prNumber, Array.from(labels)); } diff --git a/src/labeler.ts b/src/labeler.ts index e6c8828b3..22ea764ac 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -41,23 +41,20 @@ export async function run() { configPath ); - const pullRequestLabels: string[] = pullRequest.labels.map( - label => label.name - ); - const labels: Set = new Set(syncLabels ? [] : pullRequestLabels); - const excessLabels: string[] = []; + const pullRequestLabels = pullRequest.labels.map(label => label.name); + const labels = syncLabels ? [] : pullRequestLabels; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs)) { - if (labels.size < GITHUB_MAX_LABELS) { - labels.add(label); - } else { - excessLabels.push(label); - } + if (checkGlobs(changedFiles, globs) && !labels.includes(label)) { + labels.push(label); } } + // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, + // and extract the excess into `excessLabels` + const excessLabels = labels.splice(GITHUB_MAX_LABELS); + if (syncLabels) { await setLabels(client, prNumber, Array.from(labels)); } else { From 0e55d23d979858def3f4427ccef98702b4fbe846 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Wed, 7 Jun 2023 20:57:25 -0400 Subject: [PATCH 05/20] ignore IDE folders --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 867d24a55..c18d04cad 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .DS_Store node_modules/ -lib/ \ No newline at end of file +lib/ +.vscode/ +.idea/ \ No newline at end of file From 4beee0fc76ff24c354cc0c3f9dd01554c92d72e1 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Wed, 7 Jun 2023 21:02:01 -0400 Subject: [PATCH 06/20] install @octokit/plugin-retry --- package-lock.json | 63 +++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 64 insertions(+) diff --git a/package-lock.json b/package-lock.json index 0b9b0cbd7..e9129389b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", + "@octokit/plugin-retry": "^5.0.2", "js-yaml": "^4.1.0", "minimatch": "^7.4.3" }, @@ -1223,6 +1224,34 @@ "@octokit/core": ">=3" } }, + "node_modules/@octokit/plugin-retry": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.2.tgz", + "integrity": "sha512-/Z7rWLCfjwmaVdyFuMkZoAnhfrvYgtvDrbO2d6lv7XrvJa8gFGB5tLUMngfuyMBfDCc5B9+EVu7IkQx5ebVlMg==", + "dependencies": { + "@octokit/types": "^9.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/openapi-types": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-17.2.0.tgz", + "integrity": "sha512-MazrFNx4plbLsGl+LFesMo96eIXkFgEtaKbnNpdh4aQ0VM10aoylFsTYP1AEjkeoRNZiiPe3T6Gl2Hr8dJWdlQ==" + }, + "node_modules/@octokit/plugin-retry/node_modules/@octokit/types": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.2.3.tgz", + "integrity": "sha512-MMeLdHyFIALioycq+LFcA71v0S2xpQUX2cw6pPbHQjaibcHYwLnmK/kMZaWuGfGfjBJZ3wRUq+dOaWsvrPJVvA==", + "dependencies": { + "@octokit/openapi-types": "^17.2.0" + } + }, "node_modules/@octokit/request": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", @@ -1978,6 +2007,11 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -6799,6 +6833,30 @@ "deprecation": "^2.3.1" } }, + "@octokit/plugin-retry": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-5.0.2.tgz", + "integrity": "sha512-/Z7rWLCfjwmaVdyFuMkZoAnhfrvYgtvDrbO2d6lv7XrvJa8gFGB5tLUMngfuyMBfDCc5B9+EVu7IkQx5ebVlMg==", + "requires": { + "@octokit/types": "^9.0.0", + "bottleneck": "^2.15.3" + }, + "dependencies": { + "@octokit/openapi-types": { + "version": "17.2.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-17.2.0.tgz", + "integrity": "sha512-MazrFNx4plbLsGl+LFesMo96eIXkFgEtaKbnNpdh4aQ0VM10aoylFsTYP1AEjkeoRNZiiPe3T6Gl2Hr8dJWdlQ==" + }, + "@octokit/types": { + "version": "9.2.3", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.2.3.tgz", + "integrity": "sha512-MMeLdHyFIALioycq+LFcA71v0S2xpQUX2cw6pPbHQjaibcHYwLnmK/kMZaWuGfGfjBJZ3wRUq+dOaWsvrPJVvA==", + "requires": { + "@octokit/openapi-types": "^17.2.0" + } + } + } + }, "@octokit/request": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", @@ -7374,6 +7432,11 @@ "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, + "bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==" + }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", diff --git a/package.json b/package.json index 2ebca4083..a6419f114 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "dependencies": { "@actions/core": "^1.10.0", "@actions/github": "^5.1.1", + "@octokit/plugin-retry": "^5.0.2", "js-yaml": "^4.1.0", "minimatch": "^7.4.3" }, From 267e6babb74b7d45cf7b59da5b744d2d90399166 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Wed, 7 Jun 2023 21:06:29 -0400 Subject: [PATCH 07/20] always setLabels --- dist/index.js | 1693 +++++++++++++++++++++++++++++++++++++++++++++++- src/labeler.ts | 51 +- 2 files changed, 1688 insertions(+), 56 deletions(-) diff --git a/dist/index.js b/dist/index.js index f3695826a..025098e23 100644 --- a/dist/index.js +++ b/dist/index.js @@ -42,21 +42,24 @@ Object.defineProperty(exports, "__esModule", ({ value: true })); exports.checkGlobs = exports.run = void 0; const core = __importStar(__nccwpck_require__(2186)); const github = __importStar(__nccwpck_require__(5438)); +const pluginRetry = __importStar(__nccwpck_require__(6298)); const yaml = __importStar(__nccwpck_require__(1917)); const minimatch_1 = __nccwpck_require__(2002); +// Github Issues cannot have more than 100 labels +const GITHUB_MAX_LABELS = 100; function run() { return __awaiter(this, void 0, void 0, function* () { try { const token = core.getInput('repo-token'); const configPath = core.getInput('configuration-path', { required: true }); - const syncLabels = !!core.getInput('sync-labels'); + const syncLabels = core.getBooleanInput('sync-labels'); const dot = core.getBooleanInput('dot'); const prNumber = getPrNumber(); if (!prNumber) { core.info('Could not get pull request number from context, exiting'); return; } - const client = github.getOctokit(token); + const client = github.getOctokit(token, {}, pluginRetry.retry); const { data: pullRequest } = yield client.rest.pulls.get({ owner: github.context.repo.owner, repo: github.context.repo.repo, @@ -65,22 +68,21 @@ function run() { core.debug(`fetching changed files for pr #${prNumber}`); const changedFiles = yield getChangedFiles(client, prNumber); const labelGlobs = yield getLabelGlobs(client, configPath); - const labels = []; - const labelsToRemove = []; + const pullRequestLabels = pullRequest.labels.map(label => label.name); + const labels = syncLabels ? [] : pullRequestLabels; for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs, dot)) { + if (checkGlobs(changedFiles, globs, dot) && !labels.includes(label)) { labels.push(label); } - else if (pullRequest.labels.find(l => l.name === label)) { - labelsToRemove.push(label); - } - } - if (labels.length > 0) { - yield addLabels(client, prNumber, labels); } - if (syncLabels && labelsToRemove.length) { - yield removeLabels(client, prNumber, labelsToRemove); + // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, + // and extract the excess into `excessLabels` + const excessLabels = labels.splice(GITHUB_MAX_LABELS); + // set labels regardless if array has a length or not + yield setLabels(client, prNumber, labels); + if (excessLabels.length) { + core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); } } catch (error) { @@ -221,9 +223,9 @@ function checkMatch(changedFiles, matchConfig, dot) { } return true; } -function addLabels(client, prNumber, labels) { +function setLabels(client, prNumber, labels) { return __awaiter(this, void 0, void 0, function* () { - yield client.rest.issues.addLabels({ + yield client.rest.issues.setLabels({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, @@ -231,16 +233,6 @@ function addLabels(client, prNumber, labels) { }); }); } -function removeLabels(client, prNumber, labels) { - return __awaiter(this, void 0, void 0, function* () { - yield Promise.all(labels.map(label => client.rest.issues.removeLabel({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - issue_number: prNumber, - name: label - }))); - }); -} /***/ }), @@ -4319,6 +4311,127 @@ exports.restEndpointMethods = restEndpointMethods; //# sourceMappingURL=index.js.map +/***/ }), + +/***/ 6298: +/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { + +"use strict"; + +var __create = Object.create; +var __defProp = Object.defineProperty; +var __getOwnPropDesc = Object.getOwnPropertyDescriptor; +var __getOwnPropNames = Object.getOwnPropertyNames; +var __getProtoOf = Object.getPrototypeOf; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __export = (target, all) => { + for (var name in all) + __defProp(target, name, { get: all[name], enumerable: true }); +}; +var __copyProps = (to, from, except, desc) => { + if (from && typeof from === "object" || typeof from === "function") { + for (let key of __getOwnPropNames(from)) + if (!__hasOwnProp.call(to, key) && key !== except) + __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); + } + return to; +}; +var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( + // If the importer is in node compatibility mode or this is not an ESM + // file that has been converted to a CommonJS file using a Babel- + // compatible transform (i.e. "__esModule" has not been set), then set + // "default" to the CommonJS "module.exports" for node compatibility. + isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, + mod +)); +var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); + +// pkg/dist-src/index.js +var dist_src_exports = {}; +__export(dist_src_exports, { + VERSION: () => VERSION, + retry: () => retry +}); +module.exports = __toCommonJS(dist_src_exports); + +// pkg/dist-src/error-request.js +async function errorRequest(state, octokit, error, options) { + if (!error.request || !error.request.request) { + throw error; + } + if (error.status >= 400 && !state.doNotRetry.includes(error.status)) { + const retries = options.request.retries != null ? options.request.retries : state.retries; + const retryAfter = Math.pow((options.request.retryCount || 0) + 1, 2); + throw octokit.retry.retryRequest(error, retries, retryAfter); + } + throw error; +} + +// pkg/dist-src/wrap-request.js +var import_light = __toESM(__nccwpck_require__(1174)); +var import_request_error = __nccwpck_require__(537); +async function wrapRequest(state, octokit, request, options) { + const limiter = new import_light.default(); + limiter.on("failed", function(error, info) { + const maxRetries = ~~error.request.request.retries; + const after = ~~error.request.request.retryAfter; + options.request.retryCount = info.retryCount + 1; + if (maxRetries > info.retryCount) { + return after * state.retryAfterBaseValue; + } + }); + return limiter.schedule( + requestWithGraphqlErrorHandling.bind(null, state, octokit, request), + options + ); +} +async function requestWithGraphqlErrorHandling(state, octokit, request, options) { + const response = await request(request, options); + if (response.data && response.data.errors && /Something went wrong while executing your query/.test( + response.data.errors[0].message + )) { + const error = new import_request_error.RequestError(response.data.errors[0].message, 500, { + request: options, + response + }); + return errorRequest(state, octokit, error, options); + } + return response; +} + +// pkg/dist-src/index.js +var VERSION = "5.0.2"; +function retry(octokit, octokitOptions) { + const state = Object.assign( + { + enabled: true, + retryAfterBaseValue: 1e3, + doNotRetry: [400, 401, 403, 404, 422], + retries: 3 + }, + octokitOptions.retry + ); + if (state.enabled) { + octokit.hook.error("request", errorRequest.bind(null, state, octokit)); + octokit.hook.wrap("request", wrapRequest.bind(null, state, octokit)); + } + return { + retry: { + retryRequest: (error, retries, retryAfter) => { + error.request.request = Object.assign({}, error.request.request, { + retries, + retryAfter + }); + return error; + } + } + }; +} +retry.VERSION = VERSION; +// Annotate the CommonJS export names for ESM import in node: +0 && (0); + + /***/ }), /***/ 537: @@ -4837,6 +4950,1536 @@ function removeHook(state, name, method) { } +/***/ }), + +/***/ 1174: +/***/ (function(module) { + +/** + * This file contains the Bottleneck library (MIT), compiled to ES2017, and without Clustering support. + * https://github.com/SGrondin/bottleneck + */ +(function (global, factory) { + true ? module.exports = factory() : + 0; +}(this, (function () { 'use strict'; + + var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; + + function getCjsExportFromNamespace (n) { + return n && n['default'] || n; + } + + var load = function(received, defaults, onto = {}) { + var k, ref, v; + for (k in defaults) { + v = defaults[k]; + onto[k] = (ref = received[k]) != null ? ref : v; + } + return onto; + }; + + var overwrite = function(received, defaults, onto = {}) { + var k, v; + for (k in received) { + v = received[k]; + if (defaults[k] !== void 0) { + onto[k] = v; + } + } + return onto; + }; + + var parser = { + load: load, + overwrite: overwrite + }; + + var DLList; + + DLList = class DLList { + constructor(incr, decr) { + this.incr = incr; + this.decr = decr; + this._first = null; + this._last = null; + this.length = 0; + } + + push(value) { + var node; + this.length++; + if (typeof this.incr === "function") { + this.incr(); + } + node = { + value, + prev: this._last, + next: null + }; + if (this._last != null) { + this._last.next = node; + this._last = node; + } else { + this._first = this._last = node; + } + return void 0; + } + + shift() { + var value; + if (this._first == null) { + return; + } else { + this.length--; + if (typeof this.decr === "function") { + this.decr(); + } + } + value = this._first.value; + if ((this._first = this._first.next) != null) { + this._first.prev = null; + } else { + this._last = null; + } + return value; + } + + first() { + if (this._first != null) { + return this._first.value; + } + } + + getArray() { + var node, ref, results; + node = this._first; + results = []; + while (node != null) { + results.push((ref = node, node = node.next, ref.value)); + } + return results; + } + + forEachShift(cb) { + var node; + node = this.shift(); + while (node != null) { + (cb(node), node = this.shift()); + } + return void 0; + } + + debug() { + var node, ref, ref1, ref2, results; + node = this._first; + results = []; + while (node != null) { + results.push((ref = node, node = node.next, { + value: ref.value, + prev: (ref1 = ref.prev) != null ? ref1.value : void 0, + next: (ref2 = ref.next) != null ? ref2.value : void 0 + })); + } + return results; + } + + }; + + var DLList_1 = DLList; + + var Events; + + Events = class Events { + constructor(instance) { + this.instance = instance; + this._events = {}; + if ((this.instance.on != null) || (this.instance.once != null) || (this.instance.removeAllListeners != null)) { + throw new Error("An Emitter already exists for this object"); + } + this.instance.on = (name, cb) => { + return this._addListener(name, "many", cb); + }; + this.instance.once = (name, cb) => { + return this._addListener(name, "once", cb); + }; + this.instance.removeAllListeners = (name = null) => { + if (name != null) { + return delete this._events[name]; + } else { + return this._events = {}; + } + }; + } + + _addListener(name, status, cb) { + var base; + if ((base = this._events)[name] == null) { + base[name] = []; + } + this._events[name].push({cb, status}); + return this.instance; + } + + listenerCount(name) { + if (this._events[name] != null) { + return this._events[name].length; + } else { + return 0; + } + } + + async trigger(name, ...args) { + var e, promises; + try { + if (name !== "debug") { + this.trigger("debug", `Event triggered: ${name}`, args); + } + if (this._events[name] == null) { + return; + } + this._events[name] = this._events[name].filter(function(listener) { + return listener.status !== "none"; + }); + promises = this._events[name].map(async(listener) => { + var e, returned; + if (listener.status === "none") { + return; + } + if (listener.status === "once") { + listener.status = "none"; + } + try { + returned = typeof listener.cb === "function" ? listener.cb(...args) : void 0; + if (typeof (returned != null ? returned.then : void 0) === "function") { + return (await returned); + } else { + return returned; + } + } catch (error) { + e = error; + { + this.trigger("error", e); + } + return null; + } + }); + return ((await Promise.all(promises))).find(function(x) { + return x != null; + }); + } catch (error) { + e = error; + { + this.trigger("error", e); + } + return null; + } + } + + }; + + var Events_1 = Events; + + var DLList$1, Events$1, Queues; + + DLList$1 = DLList_1; + + Events$1 = Events_1; + + Queues = class Queues { + constructor(num_priorities) { + var i; + this.Events = new Events$1(this); + this._length = 0; + this._lists = (function() { + var j, ref, results; + results = []; + for (i = j = 1, ref = num_priorities; (1 <= ref ? j <= ref : j >= ref); i = 1 <= ref ? ++j : --j) { + results.push(new DLList$1((() => { + return this.incr(); + }), (() => { + return this.decr(); + }))); + } + return results; + }).call(this); + } + + incr() { + if (this._length++ === 0) { + return this.Events.trigger("leftzero"); + } + } + + decr() { + if (--this._length === 0) { + return this.Events.trigger("zero"); + } + } + + push(job) { + return this._lists[job.options.priority].push(job); + } + + queued(priority) { + if (priority != null) { + return this._lists[priority].length; + } else { + return this._length; + } + } + + shiftAll(fn) { + return this._lists.forEach(function(list) { + return list.forEachShift(fn); + }); + } + + getFirst(arr = this._lists) { + var j, len, list; + for (j = 0, len = arr.length; j < len; j++) { + list = arr[j]; + if (list.length > 0) { + return list; + } + } + return []; + } + + shiftLastFrom(priority) { + return this.getFirst(this._lists.slice(priority).reverse()).shift(); + } + + }; + + var Queues_1 = Queues; + + var BottleneckError; + + BottleneckError = class BottleneckError extends Error {}; + + var BottleneckError_1 = BottleneckError; + + var BottleneckError$1, DEFAULT_PRIORITY, Job, NUM_PRIORITIES, parser$1; + + NUM_PRIORITIES = 10; + + DEFAULT_PRIORITY = 5; + + parser$1 = parser; + + BottleneckError$1 = BottleneckError_1; + + Job = class Job { + constructor(task, args, options, jobDefaults, rejectOnDrop, Events, _states, Promise) { + this.task = task; + this.args = args; + this.rejectOnDrop = rejectOnDrop; + this.Events = Events; + this._states = _states; + this.Promise = Promise; + this.options = parser$1.load(options, jobDefaults); + this.options.priority = this._sanitizePriority(this.options.priority); + if (this.options.id === jobDefaults.id) { + this.options.id = `${this.options.id}-${this._randomIndex()}`; + } + this.promise = new this.Promise((_resolve, _reject) => { + this._resolve = _resolve; + this._reject = _reject; + }); + this.retryCount = 0; + } + + _sanitizePriority(priority) { + var sProperty; + sProperty = ~~priority !== priority ? DEFAULT_PRIORITY : priority; + if (sProperty < 0) { + return 0; + } else if (sProperty > NUM_PRIORITIES - 1) { + return NUM_PRIORITIES - 1; + } else { + return sProperty; + } + } + + _randomIndex() { + return Math.random().toString(36).slice(2); + } + + doDrop({error, message = "This job has been dropped by Bottleneck"} = {}) { + if (this._states.remove(this.options.id)) { + if (this.rejectOnDrop) { + this._reject(error != null ? error : new BottleneckError$1(message)); + } + this.Events.trigger("dropped", {args: this.args, options: this.options, task: this.task, promise: this.promise}); + return true; + } else { + return false; + } + } + + _assertStatus(expected) { + var status; + status = this._states.jobStatus(this.options.id); + if (!(status === expected || (expected === "DONE" && status === null))) { + throw new BottleneckError$1(`Invalid job status ${status}, expected ${expected}. Please open an issue at https://github.com/SGrondin/bottleneck/issues`); + } + } + + doReceive() { + this._states.start(this.options.id); + return this.Events.trigger("received", {args: this.args, options: this.options}); + } + + doQueue(reachedHWM, blocked) { + this._assertStatus("RECEIVED"); + this._states.next(this.options.id); + return this.Events.trigger("queued", {args: this.args, options: this.options, reachedHWM, blocked}); + } + + doRun() { + if (this.retryCount === 0) { + this._assertStatus("QUEUED"); + this._states.next(this.options.id); + } else { + this._assertStatus("EXECUTING"); + } + return this.Events.trigger("scheduled", {args: this.args, options: this.options}); + } + + async doExecute(chained, clearGlobalState, run, free) { + var error, eventInfo, passed; + if (this.retryCount === 0) { + this._assertStatus("RUNNING"); + this._states.next(this.options.id); + } else { + this._assertStatus("EXECUTING"); + } + eventInfo = {args: this.args, options: this.options, retryCount: this.retryCount}; + this.Events.trigger("executing", eventInfo); + try { + passed = (await (chained != null ? chained.schedule(this.options, this.task, ...this.args) : this.task(...this.args))); + if (clearGlobalState()) { + this.doDone(eventInfo); + await free(this.options, eventInfo); + this._assertStatus("DONE"); + return this._resolve(passed); + } + } catch (error1) { + error = error1; + return this._onFailure(error, eventInfo, clearGlobalState, run, free); + } + } + + doExpire(clearGlobalState, run, free) { + var error, eventInfo; + if (this._states.jobStatus(this.options.id === "RUNNING")) { + this._states.next(this.options.id); + } + this._assertStatus("EXECUTING"); + eventInfo = {args: this.args, options: this.options, retryCount: this.retryCount}; + error = new BottleneckError$1(`This job timed out after ${this.options.expiration} ms.`); + return this._onFailure(error, eventInfo, clearGlobalState, run, free); + } + + async _onFailure(error, eventInfo, clearGlobalState, run, free) { + var retry, retryAfter; + if (clearGlobalState()) { + retry = (await this.Events.trigger("failed", error, eventInfo)); + if (retry != null) { + retryAfter = ~~retry; + this.Events.trigger("retry", `Retrying ${this.options.id} after ${retryAfter} ms`, eventInfo); + this.retryCount++; + return run(retryAfter); + } else { + this.doDone(eventInfo); + await free(this.options, eventInfo); + this._assertStatus("DONE"); + return this._reject(error); + } + } + } + + doDone(eventInfo) { + this._assertStatus("EXECUTING"); + this._states.next(this.options.id); + return this.Events.trigger("done", eventInfo); + } + + }; + + var Job_1 = Job; + + var BottleneckError$2, LocalDatastore, parser$2; + + parser$2 = parser; + + BottleneckError$2 = BottleneckError_1; + + LocalDatastore = class LocalDatastore { + constructor(instance, storeOptions, storeInstanceOptions) { + this.instance = instance; + this.storeOptions = storeOptions; + this.clientId = this.instance._randomIndex(); + parser$2.load(storeInstanceOptions, storeInstanceOptions, this); + this._nextRequest = this._lastReservoirRefresh = this._lastReservoirIncrease = Date.now(); + this._running = 0; + this._done = 0; + this._unblockTime = 0; + this.ready = this.Promise.resolve(); + this.clients = {}; + this._startHeartbeat(); + } + + _startHeartbeat() { + var base; + if ((this.heartbeat == null) && (((this.storeOptions.reservoirRefreshInterval != null) && (this.storeOptions.reservoirRefreshAmount != null)) || ((this.storeOptions.reservoirIncreaseInterval != null) && (this.storeOptions.reservoirIncreaseAmount != null)))) { + return typeof (base = (this.heartbeat = setInterval(() => { + var amount, incr, maximum, now, reservoir; + now = Date.now(); + if ((this.storeOptions.reservoirRefreshInterval != null) && now >= this._lastReservoirRefresh + this.storeOptions.reservoirRefreshInterval) { + this._lastReservoirRefresh = now; + this.storeOptions.reservoir = this.storeOptions.reservoirRefreshAmount; + this.instance._drainAll(this.computeCapacity()); + } + if ((this.storeOptions.reservoirIncreaseInterval != null) && now >= this._lastReservoirIncrease + this.storeOptions.reservoirIncreaseInterval) { + ({ + reservoirIncreaseAmount: amount, + reservoirIncreaseMaximum: maximum, + reservoir + } = this.storeOptions); + this._lastReservoirIncrease = now; + incr = maximum != null ? Math.min(amount, maximum - reservoir) : amount; + if (incr > 0) { + this.storeOptions.reservoir += incr; + return this.instance._drainAll(this.computeCapacity()); + } + } + }, this.heartbeatInterval))).unref === "function" ? base.unref() : void 0; + } else { + return clearInterval(this.heartbeat); + } + } + + async __publish__(message) { + await this.yieldLoop(); + return this.instance.Events.trigger("message", message.toString()); + } + + async __disconnect__(flush) { + await this.yieldLoop(); + clearInterval(this.heartbeat); + return this.Promise.resolve(); + } + + yieldLoop(t = 0) { + return new this.Promise(function(resolve, reject) { + return setTimeout(resolve, t); + }); + } + + computePenalty() { + var ref; + return (ref = this.storeOptions.penalty) != null ? ref : (15 * this.storeOptions.minTime) || 5000; + } + + async __updateSettings__(options) { + await this.yieldLoop(); + parser$2.overwrite(options, options, this.storeOptions); + this._startHeartbeat(); + this.instance._drainAll(this.computeCapacity()); + return true; + } + + async __running__() { + await this.yieldLoop(); + return this._running; + } + + async __queued__() { + await this.yieldLoop(); + return this.instance.queued(); + } + + async __done__() { + await this.yieldLoop(); + return this._done; + } + + async __groupCheck__(time) { + await this.yieldLoop(); + return (this._nextRequest + this.timeout) < time; + } + + computeCapacity() { + var maxConcurrent, reservoir; + ({maxConcurrent, reservoir} = this.storeOptions); + if ((maxConcurrent != null) && (reservoir != null)) { + return Math.min(maxConcurrent - this._running, reservoir); + } else if (maxConcurrent != null) { + return maxConcurrent - this._running; + } else if (reservoir != null) { + return reservoir; + } else { + return null; + } + } + + conditionsCheck(weight) { + var capacity; + capacity = this.computeCapacity(); + return (capacity == null) || weight <= capacity; + } + + async __incrementReservoir__(incr) { + var reservoir; + await this.yieldLoop(); + reservoir = this.storeOptions.reservoir += incr; + this.instance._drainAll(this.computeCapacity()); + return reservoir; + } + + async __currentReservoir__() { + await this.yieldLoop(); + return this.storeOptions.reservoir; + } + + isBlocked(now) { + return this._unblockTime >= now; + } + + check(weight, now) { + return this.conditionsCheck(weight) && (this._nextRequest - now) <= 0; + } + + async __check__(weight) { + var now; + await this.yieldLoop(); + now = Date.now(); + return this.check(weight, now); + } + + async __register__(index, weight, expiration) { + var now, wait; + await this.yieldLoop(); + now = Date.now(); + if (this.conditionsCheck(weight)) { + this._running += weight; + if (this.storeOptions.reservoir != null) { + this.storeOptions.reservoir -= weight; + } + wait = Math.max(this._nextRequest - now, 0); + this._nextRequest = now + wait + this.storeOptions.minTime; + return { + success: true, + wait, + reservoir: this.storeOptions.reservoir + }; + } else { + return { + success: false + }; + } + } + + strategyIsBlock() { + return this.storeOptions.strategy === 3; + } + + async __submit__(queueLength, weight) { + var blocked, now, reachedHWM; + await this.yieldLoop(); + if ((this.storeOptions.maxConcurrent != null) && weight > this.storeOptions.maxConcurrent) { + throw new BottleneckError$2(`Impossible to add a job having a weight of ${weight} to a limiter having a maxConcurrent setting of ${this.storeOptions.maxConcurrent}`); + } + now = Date.now(); + reachedHWM = (this.storeOptions.highWater != null) && queueLength === this.storeOptions.highWater && !this.check(weight, now); + blocked = this.strategyIsBlock() && (reachedHWM || this.isBlocked(now)); + if (blocked) { + this._unblockTime = now + this.computePenalty(); + this._nextRequest = this._unblockTime + this.storeOptions.minTime; + this.instance._dropAllQueued(); + } + return { + reachedHWM, + blocked, + strategy: this.storeOptions.strategy + }; + } + + async __free__(index, weight) { + await this.yieldLoop(); + this._running -= weight; + this._done += weight; + this.instance._drainAll(this.computeCapacity()); + return { + running: this._running + }; + } + + }; + + var LocalDatastore_1 = LocalDatastore; + + var BottleneckError$3, States; + + BottleneckError$3 = BottleneckError_1; + + States = class States { + constructor(status1) { + this.status = status1; + this._jobs = {}; + this.counts = this.status.map(function() { + return 0; + }); + } + + next(id) { + var current, next; + current = this._jobs[id]; + next = current + 1; + if ((current != null) && next < this.status.length) { + this.counts[current]--; + this.counts[next]++; + return this._jobs[id]++; + } else if (current != null) { + this.counts[current]--; + return delete this._jobs[id]; + } + } + + start(id) { + var initial; + initial = 0; + this._jobs[id] = initial; + return this.counts[initial]++; + } + + remove(id) { + var current; + current = this._jobs[id]; + if (current != null) { + this.counts[current]--; + delete this._jobs[id]; + } + return current != null; + } + + jobStatus(id) { + var ref; + return (ref = this.status[this._jobs[id]]) != null ? ref : null; + } + + statusJobs(status) { + var k, pos, ref, results, v; + if (status != null) { + pos = this.status.indexOf(status); + if (pos < 0) { + throw new BottleneckError$3(`status must be one of ${this.status.join(', ')}`); + } + ref = this._jobs; + results = []; + for (k in ref) { + v = ref[k]; + if (v === pos) { + results.push(k); + } + } + return results; + } else { + return Object.keys(this._jobs); + } + } + + statusCounts() { + return this.counts.reduce(((acc, v, i) => { + acc[this.status[i]] = v; + return acc; + }), {}); + } + + }; + + var States_1 = States; + + var DLList$2, Sync; + + DLList$2 = DLList_1; + + Sync = class Sync { + constructor(name, Promise) { + this.schedule = this.schedule.bind(this); + this.name = name; + this.Promise = Promise; + this._running = 0; + this._queue = new DLList$2(); + } + + isEmpty() { + return this._queue.length === 0; + } + + async _tryToRun() { + var args, cb, error, reject, resolve, returned, task; + if ((this._running < 1) && this._queue.length > 0) { + this._running++; + ({task, args, resolve, reject} = this._queue.shift()); + cb = (await (async function() { + try { + returned = (await task(...args)); + return function() { + return resolve(returned); + }; + } catch (error1) { + error = error1; + return function() { + return reject(error); + }; + } + })()); + this._running--; + this._tryToRun(); + return cb(); + } + } + + schedule(task, ...args) { + var promise, reject, resolve; + resolve = reject = null; + promise = new this.Promise(function(_resolve, _reject) { + resolve = _resolve; + return reject = _reject; + }); + this._queue.push({task, args, resolve, reject}); + this._tryToRun(); + return promise; + } + + }; + + var Sync_1 = Sync; + + var version = "2.19.5"; + var version$1 = { + version: version + }; + + var version$2 = /*#__PURE__*/Object.freeze({ + version: version, + default: version$1 + }); + + var require$$2 = () => console.log('You must import the full version of Bottleneck in order to use this feature.'); + + var require$$3 = () => console.log('You must import the full version of Bottleneck in order to use this feature.'); + + var require$$4 = () => console.log('You must import the full version of Bottleneck in order to use this feature.'); + + var Events$2, Group, IORedisConnection$1, RedisConnection$1, Scripts$1, parser$3; + + parser$3 = parser; + + Events$2 = Events_1; + + RedisConnection$1 = require$$2; + + IORedisConnection$1 = require$$3; + + Scripts$1 = require$$4; + + Group = (function() { + class Group { + constructor(limiterOptions = {}) { + this.deleteKey = this.deleteKey.bind(this); + this.limiterOptions = limiterOptions; + parser$3.load(this.limiterOptions, this.defaults, this); + this.Events = new Events$2(this); + this.instances = {}; + this.Bottleneck = Bottleneck_1; + this._startAutoCleanup(); + this.sharedConnection = this.connection != null; + if (this.connection == null) { + if (this.limiterOptions.datastore === "redis") { + this.connection = new RedisConnection$1(Object.assign({}, this.limiterOptions, {Events: this.Events})); + } else if (this.limiterOptions.datastore === "ioredis") { + this.connection = new IORedisConnection$1(Object.assign({}, this.limiterOptions, {Events: this.Events})); + } + } + } + + key(key = "") { + var ref; + return (ref = this.instances[key]) != null ? ref : (() => { + var limiter; + limiter = this.instances[key] = new this.Bottleneck(Object.assign(this.limiterOptions, { + id: `${this.id}-${key}`, + timeout: this.timeout, + connection: this.connection + })); + this.Events.trigger("created", limiter, key); + return limiter; + })(); + } + + async deleteKey(key = "") { + var deleted, instance; + instance = this.instances[key]; + if (this.connection) { + deleted = (await this.connection.__runCommand__(['del', ...Scripts$1.allKeys(`${this.id}-${key}`)])); + } + if (instance != null) { + delete this.instances[key]; + await instance.disconnect(); + } + return (instance != null) || deleted > 0; + } + + limiters() { + var k, ref, results, v; + ref = this.instances; + results = []; + for (k in ref) { + v = ref[k]; + results.push({ + key: k, + limiter: v + }); + } + return results; + } + + keys() { + return Object.keys(this.instances); + } + + async clusterKeys() { + var cursor, end, found, i, k, keys, len, next, start; + if (this.connection == null) { + return this.Promise.resolve(this.keys()); + } + keys = []; + cursor = null; + start = `b_${this.id}-`.length; + end = "_settings".length; + while (cursor !== 0) { + [next, found] = (await this.connection.__runCommand__(["scan", cursor != null ? cursor : 0, "match", `b_${this.id}-*_settings`, "count", 10000])); + cursor = ~~next; + for (i = 0, len = found.length; i < len; i++) { + k = found[i]; + keys.push(k.slice(start, -end)); + } + } + return keys; + } + + _startAutoCleanup() { + var base; + clearInterval(this.interval); + return typeof (base = (this.interval = setInterval(async() => { + var e, k, ref, results, time, v; + time = Date.now(); + ref = this.instances; + results = []; + for (k in ref) { + v = ref[k]; + try { + if ((await v._store.__groupCheck__(time))) { + results.push(this.deleteKey(k)); + } else { + results.push(void 0); + } + } catch (error) { + e = error; + results.push(v.Events.trigger("error", e)); + } + } + return results; + }, this.timeout / 2))).unref === "function" ? base.unref() : void 0; + } + + updateSettings(options = {}) { + parser$3.overwrite(options, this.defaults, this); + parser$3.overwrite(options, options, this.limiterOptions); + if (options.timeout != null) { + return this._startAutoCleanup(); + } + } + + disconnect(flush = true) { + var ref; + if (!this.sharedConnection) { + return (ref = this.connection) != null ? ref.disconnect(flush) : void 0; + } + } + + } + Group.prototype.defaults = { + timeout: 1000 * 60 * 5, + connection: null, + Promise: Promise, + id: "group-key" + }; + + return Group; + + }).call(commonjsGlobal); + + var Group_1 = Group; + + var Batcher, Events$3, parser$4; + + parser$4 = parser; + + Events$3 = Events_1; + + Batcher = (function() { + class Batcher { + constructor(options = {}) { + this.options = options; + parser$4.load(this.options, this.defaults, this); + this.Events = new Events$3(this); + this._arr = []; + this._resetPromise(); + this._lastFlush = Date.now(); + } + + _resetPromise() { + return this._promise = new this.Promise((res, rej) => { + return this._resolve = res; + }); + } + + _flush() { + clearTimeout(this._timeout); + this._lastFlush = Date.now(); + this._resolve(); + this.Events.trigger("batch", this._arr); + this._arr = []; + return this._resetPromise(); + } + + add(data) { + var ret; + this._arr.push(data); + ret = this._promise; + if (this._arr.length === this.maxSize) { + this._flush(); + } else if ((this.maxTime != null) && this._arr.length === 1) { + this._timeout = setTimeout(() => { + return this._flush(); + }, this.maxTime); + } + return ret; + } + + } + Batcher.prototype.defaults = { + maxTime: null, + maxSize: null, + Promise: Promise + }; + + return Batcher; + + }).call(commonjsGlobal); + + var Batcher_1 = Batcher; + + var require$$4$1 = () => console.log('You must import the full version of Bottleneck in order to use this feature.'); + + var require$$8 = getCjsExportFromNamespace(version$2); + + var Bottleneck, DEFAULT_PRIORITY$1, Events$4, Job$1, LocalDatastore$1, NUM_PRIORITIES$1, Queues$1, RedisDatastore$1, States$1, Sync$1, parser$5, + splice = [].splice; + + NUM_PRIORITIES$1 = 10; + + DEFAULT_PRIORITY$1 = 5; + + parser$5 = parser; + + Queues$1 = Queues_1; + + Job$1 = Job_1; + + LocalDatastore$1 = LocalDatastore_1; + + RedisDatastore$1 = require$$4$1; + + Events$4 = Events_1; + + States$1 = States_1; + + Sync$1 = Sync_1; + + Bottleneck = (function() { + class Bottleneck { + constructor(options = {}, ...invalid) { + var storeInstanceOptions, storeOptions; + this._addToQueue = this._addToQueue.bind(this); + this._validateOptions(options, invalid); + parser$5.load(options, this.instanceDefaults, this); + this._queues = new Queues$1(NUM_PRIORITIES$1); + this._scheduled = {}; + this._states = new States$1(["RECEIVED", "QUEUED", "RUNNING", "EXECUTING"].concat(this.trackDoneStatus ? ["DONE"] : [])); + this._limiter = null; + this.Events = new Events$4(this); + this._submitLock = new Sync$1("submit", this.Promise); + this._registerLock = new Sync$1("register", this.Promise); + storeOptions = parser$5.load(options, this.storeDefaults, {}); + this._store = (function() { + if (this.datastore === "redis" || this.datastore === "ioredis" || (this.connection != null)) { + storeInstanceOptions = parser$5.load(options, this.redisStoreDefaults, {}); + return new RedisDatastore$1(this, storeOptions, storeInstanceOptions); + } else if (this.datastore === "local") { + storeInstanceOptions = parser$5.load(options, this.localStoreDefaults, {}); + return new LocalDatastore$1(this, storeOptions, storeInstanceOptions); + } else { + throw new Bottleneck.prototype.BottleneckError(`Invalid datastore type: ${this.datastore}`); + } + }).call(this); + this._queues.on("leftzero", () => { + var ref; + return (ref = this._store.heartbeat) != null ? typeof ref.ref === "function" ? ref.ref() : void 0 : void 0; + }); + this._queues.on("zero", () => { + var ref; + return (ref = this._store.heartbeat) != null ? typeof ref.unref === "function" ? ref.unref() : void 0 : void 0; + }); + } + + _validateOptions(options, invalid) { + if (!((options != null) && typeof options === "object" && invalid.length === 0)) { + throw new Bottleneck.prototype.BottleneckError("Bottleneck v2 takes a single object argument. Refer to https://github.com/SGrondin/bottleneck#upgrading-to-v2 if you're upgrading from Bottleneck v1."); + } + } + + ready() { + return this._store.ready; + } + + clients() { + return this._store.clients; + } + + channel() { + return `b_${this.id}`; + } + + channel_client() { + return `b_${this.id}_${this._store.clientId}`; + } + + publish(message) { + return this._store.__publish__(message); + } + + disconnect(flush = true) { + return this._store.__disconnect__(flush); + } + + chain(_limiter) { + this._limiter = _limiter; + return this; + } + + queued(priority) { + return this._queues.queued(priority); + } + + clusterQueued() { + return this._store.__queued__(); + } + + empty() { + return this.queued() === 0 && this._submitLock.isEmpty(); + } + + running() { + return this._store.__running__(); + } + + done() { + return this._store.__done__(); + } + + jobStatus(id) { + return this._states.jobStatus(id); + } + + jobs(status) { + return this._states.statusJobs(status); + } + + counts() { + return this._states.statusCounts(); + } + + _randomIndex() { + return Math.random().toString(36).slice(2); + } + + check(weight = 1) { + return this._store.__check__(weight); + } + + _clearGlobalState(index) { + if (this._scheduled[index] != null) { + clearTimeout(this._scheduled[index].expiration); + delete this._scheduled[index]; + return true; + } else { + return false; + } + } + + async _free(index, job, options, eventInfo) { + var e, running; + try { + ({running} = (await this._store.__free__(index, options.weight))); + this.Events.trigger("debug", `Freed ${options.id}`, eventInfo); + if (running === 0 && this.empty()) { + return this.Events.trigger("idle"); + } + } catch (error1) { + e = error1; + return this.Events.trigger("error", e); + } + } + + _run(index, job, wait) { + var clearGlobalState, free, run; + job.doRun(); + clearGlobalState = this._clearGlobalState.bind(this, index); + run = this._run.bind(this, index, job); + free = this._free.bind(this, index, job); + return this._scheduled[index] = { + timeout: setTimeout(() => { + return job.doExecute(this._limiter, clearGlobalState, run, free); + }, wait), + expiration: job.options.expiration != null ? setTimeout(function() { + return job.doExpire(clearGlobalState, run, free); + }, wait + job.options.expiration) : void 0, + job: job + }; + } + + _drainOne(capacity) { + return this._registerLock.schedule(() => { + var args, index, next, options, queue; + if (this.queued() === 0) { + return this.Promise.resolve(null); + } + queue = this._queues.getFirst(); + ({options, args} = next = queue.first()); + if ((capacity != null) && options.weight > capacity) { + return this.Promise.resolve(null); + } + this.Events.trigger("debug", `Draining ${options.id}`, {args, options}); + index = this._randomIndex(); + return this._store.__register__(index, options.weight, options.expiration).then(({success, wait, reservoir}) => { + var empty; + this.Events.trigger("debug", `Drained ${options.id}`, {success, args, options}); + if (success) { + queue.shift(); + empty = this.empty(); + if (empty) { + this.Events.trigger("empty"); + } + if (reservoir === 0) { + this.Events.trigger("depleted", empty); + } + this._run(index, next, wait); + return this.Promise.resolve(options.weight); + } else { + return this.Promise.resolve(null); + } + }); + }); + } + + _drainAll(capacity, total = 0) { + return this._drainOne(capacity).then((drained) => { + var newCapacity; + if (drained != null) { + newCapacity = capacity != null ? capacity - drained : capacity; + return this._drainAll(newCapacity, total + drained); + } else { + return this.Promise.resolve(total); + } + }).catch((e) => { + return this.Events.trigger("error", e); + }); + } + + _dropAllQueued(message) { + return this._queues.shiftAll(function(job) { + return job.doDrop({message}); + }); + } + + stop(options = {}) { + var done, waitForExecuting; + options = parser$5.load(options, this.stopDefaults); + waitForExecuting = (at) => { + var finished; + finished = () => { + var counts; + counts = this._states.counts; + return (counts[0] + counts[1] + counts[2] + counts[3]) === at; + }; + return new this.Promise((resolve, reject) => { + if (finished()) { + return resolve(); + } else { + return this.on("done", () => { + if (finished()) { + this.removeAllListeners("done"); + return resolve(); + } + }); + } + }); + }; + done = options.dropWaitingJobs ? (this._run = function(index, next) { + return next.doDrop({ + message: options.dropErrorMessage + }); + }, this._drainOne = () => { + return this.Promise.resolve(null); + }, this._registerLock.schedule(() => { + return this._submitLock.schedule(() => { + var k, ref, v; + ref = this._scheduled; + for (k in ref) { + v = ref[k]; + if (this.jobStatus(v.job.options.id) === "RUNNING") { + clearTimeout(v.timeout); + clearTimeout(v.expiration); + v.job.doDrop({ + message: options.dropErrorMessage + }); + } + } + this._dropAllQueued(options.dropErrorMessage); + return waitForExecuting(0); + }); + })) : this.schedule({ + priority: NUM_PRIORITIES$1 - 1, + weight: 0 + }, () => { + return waitForExecuting(1); + }); + this._receive = function(job) { + return job._reject(new Bottleneck.prototype.BottleneckError(options.enqueueErrorMessage)); + }; + this.stop = () => { + return this.Promise.reject(new Bottleneck.prototype.BottleneckError("stop() has already been called")); + }; + return done; + } + + async _addToQueue(job) { + var args, blocked, error, options, reachedHWM, shifted, strategy; + ({args, options} = job); + try { + ({reachedHWM, blocked, strategy} = (await this._store.__submit__(this.queued(), options.weight))); + } catch (error1) { + error = error1; + this.Events.trigger("debug", `Could not queue ${options.id}`, {args, options, error}); + job.doDrop({error}); + return false; + } + if (blocked) { + job.doDrop(); + return true; + } else if (reachedHWM) { + shifted = strategy === Bottleneck.prototype.strategy.LEAK ? this._queues.shiftLastFrom(options.priority) : strategy === Bottleneck.prototype.strategy.OVERFLOW_PRIORITY ? this._queues.shiftLastFrom(options.priority + 1) : strategy === Bottleneck.prototype.strategy.OVERFLOW ? job : void 0; + if (shifted != null) { + shifted.doDrop(); + } + if ((shifted == null) || strategy === Bottleneck.prototype.strategy.OVERFLOW) { + if (shifted == null) { + job.doDrop(); + } + return reachedHWM; + } + } + job.doQueue(reachedHWM, blocked); + this._queues.push(job); + await this._drainAll(); + return reachedHWM; + } + + _receive(job) { + if (this._states.jobStatus(job.options.id) != null) { + job._reject(new Bottleneck.prototype.BottleneckError(`A job with the same id already exists (id=${job.options.id})`)); + return false; + } else { + job.doReceive(); + return this._submitLock.schedule(this._addToQueue, job); + } + } + + submit(...args) { + var cb, fn, job, options, ref, ref1, task; + if (typeof args[0] === "function") { + ref = args, [fn, ...args] = ref, [cb] = splice.call(args, -1); + options = parser$5.load({}, this.jobDefaults); + } else { + ref1 = args, [options, fn, ...args] = ref1, [cb] = splice.call(args, -1); + options = parser$5.load(options, this.jobDefaults); + } + task = (...args) => { + return new this.Promise(function(resolve, reject) { + return fn(...args, function(...args) { + return (args[0] != null ? reject : resolve)(args); + }); + }); + }; + job = new Job$1(task, args, options, this.jobDefaults, this.rejectOnDrop, this.Events, this._states, this.Promise); + job.promise.then(function(args) { + return typeof cb === "function" ? cb(...args) : void 0; + }).catch(function(args) { + if (Array.isArray(args)) { + return typeof cb === "function" ? cb(...args) : void 0; + } else { + return typeof cb === "function" ? cb(args) : void 0; + } + }); + return this._receive(job); + } + + schedule(...args) { + var job, options, task; + if (typeof args[0] === "function") { + [task, ...args] = args; + options = {}; + } else { + [options, task, ...args] = args; + } + job = new Job$1(task, args, options, this.jobDefaults, this.rejectOnDrop, this.Events, this._states, this.Promise); + this._receive(job); + return job.promise; + } + + wrap(fn) { + var schedule, wrapped; + schedule = this.schedule.bind(this); + wrapped = function(...args) { + return schedule(fn.bind(this), ...args); + }; + wrapped.withOptions = function(options, ...args) { + return schedule(options, fn, ...args); + }; + return wrapped; + } + + async updateSettings(options = {}) { + await this._store.__updateSettings__(parser$5.overwrite(options, this.storeDefaults)); + parser$5.overwrite(options, this.instanceDefaults, this); + return this; + } + + currentReservoir() { + return this._store.__currentReservoir__(); + } + + incrementReservoir(incr = 0) { + return this._store.__incrementReservoir__(incr); + } + + } + Bottleneck.default = Bottleneck; + + Bottleneck.Events = Events$4; + + Bottleneck.version = Bottleneck.prototype.version = require$$8.version; + + Bottleneck.strategy = Bottleneck.prototype.strategy = { + LEAK: 1, + OVERFLOW: 2, + OVERFLOW_PRIORITY: 4, + BLOCK: 3 + }; + + Bottleneck.BottleneckError = Bottleneck.prototype.BottleneckError = BottleneckError_1; + + Bottleneck.Group = Bottleneck.prototype.Group = Group_1; + + Bottleneck.RedisConnection = Bottleneck.prototype.RedisConnection = require$$2; + + Bottleneck.IORedisConnection = Bottleneck.prototype.IORedisConnection = require$$3; + + Bottleneck.Batcher = Bottleneck.prototype.Batcher = Batcher_1; + + Bottleneck.prototype.jobDefaults = { + priority: DEFAULT_PRIORITY$1, + weight: 1, + expiration: null, + id: "" + }; + + Bottleneck.prototype.storeDefaults = { + maxConcurrent: null, + minTime: 0, + highWater: null, + strategy: Bottleneck.prototype.strategy.LEAK, + penalty: null, + reservoir: null, + reservoirRefreshInterval: null, + reservoirRefreshAmount: null, + reservoirIncreaseInterval: null, + reservoirIncreaseAmount: null, + reservoirIncreaseMaximum: null + }; + + Bottleneck.prototype.localStoreDefaults = { + Promise: Promise, + timeout: null, + heartbeatInterval: 250 + }; + + Bottleneck.prototype.redisStoreDefaults = { + Promise: Promise, + timeout: null, + heartbeatInterval: 5000, + clientTimeout: 10000, + Redis: null, + clientOptions: {}, + clusterNodes: null, + clearDatastore: false, + connection: null + }; + + Bottleneck.prototype.instanceDefaults = { + datastore: "local", + connection: null, + id: "", + rejectOnDrop: true, + trackDoneStatus: false, + Promise: Promise + }; + + Bottleneck.prototype.stopDefaults = { + enqueueErrorMessage: "This limiter has been stopped and cannot accept new jobs.", + dropWaitingJobs: true, + dropErrorMessage: "This limiter has been stopped." + }; + + return Bottleneck; + + }).call(commonjsGlobal); + + var Bottleneck_1 = Bottleneck; + + var lib = Bottleneck_1; + + return lib; + +}))); + + /***/ }), /***/ 3717: diff --git a/src/labeler.ts b/src/labeler.ts index 1d7f4633e..bc050d6de 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core'; import * as github from '@actions/github'; +import * as pluginRetry from '@octokit/plugin-retry'; import * as yaml from 'js-yaml'; import {Minimatch} from 'minimatch'; @@ -11,11 +12,14 @@ interface MatchConfig { type StringOrMatchConfig = string | MatchConfig; type ClientType = ReturnType; +// Github Issues cannot have more than 100 labels +const GITHUB_MAX_LABELS = 100; + export async function run() { try { const token = core.getInput('repo-token'); const configPath = core.getInput('configuration-path', {required: true}); - const syncLabels = !!core.getInput('sync-labels'); + const syncLabels = core.getBooleanInput('sync-labels'); const dot = core.getBooleanInput('dot'); const prNumber = getPrNumber(); @@ -24,7 +28,7 @@ export async function run() { return; } - const client: ClientType = github.getOctokit(token); + const client: ClientType = github.getOctokit(token, {}, pluginRetry.retry); const {data: pullRequest} = await client.rest.pulls.get({ owner: github.context.repo.owner, @@ -39,23 +43,25 @@ export async function run() { configPath ); - const labels: string[] = []; - const labelsToRemove: string[] = []; + const pullRequestLabels = pullRequest.labels.map(label => label.name); + const labels: string[] = syncLabels ? [] : pullRequestLabels; + for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs, dot)) { + if (checkGlobs(changedFiles, globs, dot) && !labels.includes(label)) { labels.push(label); - } else if (pullRequest.labels.find(l => l.name === label)) { - labelsToRemove.push(label); } } - if (labels.length > 0) { - await addLabels(client, prNumber, labels); - } + // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, + // and extract the excess into `excessLabels` + const excessLabels = labels.splice(GITHUB_MAX_LABELS); - if (syncLabels && labelsToRemove.length) { - await removeLabels(client, prNumber, labelsToRemove); + // set labels regardless if array has a length or not + await setLabels(client, prNumber, labels); + + if (excessLabels.length) { + core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); } } catch (error: any) { core.error(error); @@ -243,32 +249,15 @@ function checkMatch( return true; } -async function addLabels( +async function setLabels( client: ClientType, prNumber: number, labels: string[] ) { - await client.rest.issues.addLabels({ + await client.rest.issues.setLabels({ owner: github.context.repo.owner, repo: github.context.repo.repo, issue_number: prNumber, labels: labels }); } - -async function removeLabels( - client: ClientType, - prNumber: number, - labels: string[] -) { - await Promise.all( - labels.map(label => - client.rest.issues.removeLabel({ - owner: github.context.repo.owner, - repo: github.context.repo.repo, - issue_number: prNumber, - name: label - }) - ) - ); -} From b35f25a7ace9bafd8c0ef875179b220189a1ad88 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Wed, 7 Jun 2023 21:11:58 -0400 Subject: [PATCH 08/20] fix indentations --- src/labeler.ts | 54 +++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/src/labeler.ts b/src/labeler.ts index bc050d6de..f42b839f7 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -39,8 +39,8 @@ export async function run() { core.debug(`fetching changed files for pr #${prNumber}`); const changedFiles: string[] = await getChangedFiles(client, prNumber); const labelGlobs: Map = await getLabelGlobs( - client, - configPath + client, + configPath ); const pullRequestLabels = pullRequest.labels.map(label => label.name); @@ -79,8 +79,8 @@ function getPrNumber(): number | undefined { } async function getChangedFiles( - client: ClientType, - prNumber: number + client: ClientType, + prNumber: number ): Promise { const listFilesOptions = client.rest.pulls.listFiles.endpoint.merge({ owner: github.context.repo.owner, @@ -100,12 +100,12 @@ async function getChangedFiles( } async function getLabelGlobs( - client: ClientType, - configurationPath: string + client: ClientType, + configurationPath: string ): Promise> { const configurationContent: string = await fetchContent( - client, - configurationPath + client, + configurationPath ); // loads (hopefully) a `{[label:string]: string | StringOrMatchConfig[]}`, but is `any`: @@ -116,8 +116,8 @@ async function getLabelGlobs( } async function fetchContent( - client: ClientType, - repoPath: string + client: ClientType, + repoPath: string ): Promise { const response: any = await client.rest.repos.getContent({ owner: github.context.repo.owner, @@ -130,7 +130,7 @@ async function fetchContent( } function getLabelGlobMapFromObject( - configObject: any + configObject: any ): Map { const labelGlobs: Map = new Map(); for (const label in configObject) { @@ -140,7 +140,7 @@ function getLabelGlobMapFromObject( labelGlobs.set(label, configObject[label]); } else { throw Error( - `found unexpected type for label ${label} (should be string or array of globs)` + `found unexpected type for label ${label} (should be string or array of globs)` ); } } @@ -163,9 +163,9 @@ function printPattern(matcher: Minimatch): string { } export function checkGlobs( - changedFiles: string[], - globs: StringOrMatchConfig[], - dot: boolean + changedFiles: string[], + globs: StringOrMatchConfig[], + dot: boolean ): boolean { for (const glob of globs) { core.debug(` checking pattern ${JSON.stringify(glob)}`); @@ -193,9 +193,9 @@ function isMatch(changedFile: string, matchers: Minimatch[]): boolean { // equivalent to "Array.some()" but expanded for debugging and clarity function checkAny( - changedFiles: string[], - globs: string[], - dot: boolean + changedFiles: string[], + globs: string[], + dot: boolean ): boolean { const matchers = globs.map(g => new Minimatch(g, {dot})); core.debug(` checking "any" patterns`); @@ -212,9 +212,9 @@ function checkAny( // equivalent to "Array.every()" but expanded for debugging and clarity function checkAll( - changedFiles: string[], - globs: string[], - dot: boolean + changedFiles: string[], + globs: string[], + dot: boolean ): boolean { const matchers = globs.map(g => new Minimatch(g, {dot})); core.debug(` checking "all" patterns`); @@ -230,9 +230,9 @@ function checkAll( } function checkMatch( - changedFiles: string[], - matchConfig: MatchConfig, - dot: boolean + changedFiles: string[], + matchConfig: MatchConfig, + dot: boolean ): boolean { if (matchConfig.all !== undefined) { if (!checkAll(changedFiles, matchConfig.all, dot)) { @@ -250,9 +250,9 @@ function checkMatch( } async function setLabels( - client: ClientType, - prNumber: number, - labels: string[] + client: ClientType, + prNumber: number, + labels: string[] ) { await client.rest.issues.setLabels({ owner: github.context.repo.owner, From 5baeff5235b0fd783cc6c13509f69ad0c18b1138 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Wed, 7 Jun 2023 21:43:20 -0400 Subject: [PATCH 09/20] fix specs --- __mocks__/@actions/github.ts | 3 +- __tests__/main.test.ts | 62 +++++++++++++++++++++++++----------- 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/__mocks__/@actions/github.ts b/__mocks__/@actions/github.ts index ea8319f63..ff41ffb55 100644 --- a/__mocks__/@actions/github.ts +++ b/__mocks__/@actions/github.ts @@ -13,8 +13,7 @@ export const context = { const mockApi = { rest: { issues: { - addLabels: jest.fn(), - removeLabel: jest.fn() + setLabels: jest.fn() }, pulls: { get: jest.fn().mockResolvedValue({}), diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index c4b77bc99..7901aca28 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -8,8 +8,7 @@ jest.mock('@actions/core'); jest.mock('@actions/github'); const gh = github.getOctokit('_'); -const addLabelsMock = jest.spyOn(gh.rest.issues, 'addLabels'); -const removeLabelMock = jest.spyOn(gh.rest.issues, 'removeLabel'); +const setLabelsMock = jest.spyOn(gh.rest.issues, 'setLabels'); const reposMock = jest.spyOn(gh.rest.repos, 'getContent'); const paginateMock = jest.spyOn(gh, 'paginate'); const getPullMock = jest.spyOn(gh.rest.pulls, 'get'); @@ -41,12 +40,16 @@ describe('run', () => { configureInput({}); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('foo.pdf'); + getPullMock.mockResolvedValue({ + data: { + labels: [] + } + }); await run(); - expect(removeLabelMock).toHaveBeenCalledTimes(0); - expect(addLabelsMock).toHaveBeenCalledTimes(1); - expect(addLabelsMock).toHaveBeenCalledWith({ + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ owner: 'monalisa', repo: 'helloworld', issue_number: 123, @@ -58,12 +61,16 @@ describe('run', () => { configureInput({dot: true}); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('.foo.pdf'); + getPullMock.mockResolvedValue({ + data: { + labels: [] + } + }); await run(); - expect(removeLabelMock).toHaveBeenCalledTimes(0); - expect(addLabelsMock).toHaveBeenCalledTimes(1); - expect(addLabelsMock).toHaveBeenCalledWith({ + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ owner: 'monalisa', repo: 'helloworld', issue_number: 123, @@ -75,11 +82,21 @@ describe('run', () => { configureInput({}); usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('.foo.pdf'); + getPullMock.mockResolvedValue({ + data: { + labels: [] + } + }); await run(); - expect(removeLabelMock).toHaveBeenCalledTimes(0); - expect(addLabelsMock).toHaveBeenCalledTimes(0); + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + issue_number: 123, + labels: [] + }); }); it('(with dot: true) does not add labels to PRs that do not match our glob patterns', async () => { @@ -89,8 +106,13 @@ describe('run', () => { await run(); - expect(removeLabelMock).toHaveBeenCalledTimes(0); - expect(addLabelsMock).toHaveBeenCalledTimes(0); + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + issue_number: 123, + labels: [] + }); }); it('(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern', async () => { @@ -110,13 +132,12 @@ describe('run', () => { await run(); - expect(addLabelsMock).toHaveBeenCalledTimes(0); - expect(removeLabelMock).toHaveBeenCalledTimes(1); - expect(removeLabelMock).toHaveBeenCalledWith({ + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ owner: 'monalisa', repo: 'helloworld', issue_number: 123, - name: 'touched-a-pdf-file' + labels: [] }); }); @@ -137,8 +158,13 @@ describe('run', () => { await run(); - expect(addLabelsMock).toHaveBeenCalledTimes(0); - expect(removeLabelMock).toHaveBeenCalledTimes(0); + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + issue_number: 123, + labels: ['touched-a-pdf-file'] + }); }); }); From d65a48daec07cb8732ea6bf4fd01fa85a2b8d436 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Wed, 7 Jun 2023 21:53:14 -0400 Subject: [PATCH 10/20] add spec for excess labels --- __tests__/main.test.ts | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 7901aca28..63afe1edf 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -12,6 +12,7 @@ const setLabelsMock = jest.spyOn(gh.rest.issues, 'setLabels'); const reposMock = jest.spyOn(gh.rest.repos, 'getContent'); const paginateMock = jest.spyOn(gh, 'paginate'); const getPullMock = jest.spyOn(gh.rest.pulls, 'get'); +const coreWarningMock = jest.spyOn(core, 'warning'); const yamlFixtures = { 'only_pdfs.yml': fs.readFileSync('__tests__/fixtures/only_pdfs.yml') @@ -166,6 +167,37 @@ describe('run', () => { labels: ['touched-a-pdf-file'] }); }); + + it('(with sync-labels: false) it sets only 100 labels and logs the rest', async () => { + configureInput({ + 'repo-token': 'foo', + 'configuration-path': 'bar', + 'sync-labels': false + }); + + usingLabelerConfigYaml('only_pdfs.yml'); + mockGitHubResponseChangedFiles('foo.pdf'); + + const existingLabels = Array.from({ length: 100 }).map((_, idx) => ({name: `existing-label-${idx}`})); + getPullMock.mockResolvedValue({ + data: { + labels: existingLabels + } + }); + + await run(); + + expect(setLabelsMock).toHaveBeenCalledTimes(1); + expect(setLabelsMock).toHaveBeenCalledWith({ + owner: 'monalisa', + repo: 'helloworld', + issue_number: 123, + labels: existingLabels.map((label) => label.name) + }); + + expect(coreWarningMock).toHaveBeenCalledTimes(1); + expect(coreWarningMock).toHaveBeenCalledWith('failed to add excess labels touched-a-pdf-file'); + }); }); function usingLabelerConfigYaml(fixtureName: keyof typeof yamlFixtures): void { From 4dd2b812ced3a2e067b18951c97e7e96f5708d8f Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Mon, 12 Jun 2023 08:34:14 -0400 Subject: [PATCH 11/20] prettier --- __tests__/main.test.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 63afe1edf..0b839bf13 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -178,7 +178,9 @@ describe('run', () => { usingLabelerConfigYaml('only_pdfs.yml'); mockGitHubResponseChangedFiles('foo.pdf'); - const existingLabels = Array.from({ length: 100 }).map((_, idx) => ({name: `existing-label-${idx}`})); + const existingLabels = Array.from({length: 100}).map((_, idx) => ({ + name: `existing-label-${idx}` + })); getPullMock.mockResolvedValue({ data: { labels: existingLabels @@ -192,11 +194,13 @@ describe('run', () => { owner: 'monalisa', repo: 'helloworld', issue_number: 123, - labels: existingLabels.map((label) => label.name) + labels: existingLabels.map(label => label.name) }); expect(coreWarningMock).toHaveBeenCalledTimes(1); - expect(coreWarningMock).toHaveBeenCalledWith('failed to add excess labels touched-a-pdf-file'); + expect(coreWarningMock).toHaveBeenCalledWith( + 'failed to add excess labels touched-a-pdf-file' + ); }); }); From 2f9caa01cea02bd13f44f5e04fb78eb9cc2239bb Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Mon, 12 Jun 2023 08:47:01 -0400 Subject: [PATCH 12/20] licensed cache --- ....dep.yml => openapi-types-12.11.0.dep.yml} | 2 +- .../npm/@octokit/openapi-types-17.2.0.dep.yml | 20 +++++++++++ .licenses/npm/@octokit/plugin-retry.dep.yml | 34 +++++++++++++++++++ .../{types.dep.yml => types-6.41.0.dep.yml} | 2 +- .licenses/npm/@octokit/types-9.2.3.dep.yml | 20 +++++++++++ .licenses/npm/bottleneck.dep.yml | 31 +++++++++++++++++ 6 files changed, 107 insertions(+), 2 deletions(-) rename .licenses/npm/@octokit/{openapi-types.dep.yml => openapi-types-12.11.0.dep.yml} (99%) create mode 100644 .licenses/npm/@octokit/openapi-types-17.2.0.dep.yml create mode 100644 .licenses/npm/@octokit/plugin-retry.dep.yml rename .licenses/npm/@octokit/{types.dep.yml => types-6.41.0.dep.yml} (99%) create mode 100644 .licenses/npm/@octokit/types-9.2.3.dep.yml create mode 100644 .licenses/npm/bottleneck.dep.yml diff --git a/.licenses/npm/@octokit/openapi-types.dep.yml b/.licenses/npm/@octokit/openapi-types-12.11.0.dep.yml similarity index 99% rename from .licenses/npm/@octokit/openapi-types.dep.yml rename to .licenses/npm/@octokit/openapi-types-12.11.0.dep.yml index f2891938f..91531481c 100644 --- a/.licenses/npm/@octokit/openapi-types.dep.yml +++ b/.licenses/npm/@octokit/openapi-types-12.11.0.dep.yml @@ -3,7 +3,7 @@ name: "@octokit/openapi-types" version: 12.11.0 type: npm summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com -homepage: +homepage: license: mit licenses: - sources: LICENSE diff --git a/.licenses/npm/@octokit/openapi-types-17.2.0.dep.yml b/.licenses/npm/@octokit/openapi-types-17.2.0.dep.yml new file mode 100644 index 000000000..b3c083241 --- /dev/null +++ b/.licenses/npm/@octokit/openapi-types-17.2.0.dep.yml @@ -0,0 +1,20 @@ +--- +name: "@octokit/openapi-types" +version: 17.2.0 +type: npm +summary: Generated TypeScript definitions based on GitHub's OpenAPI spec for api.github.com +homepage: +license: mit +licenses: +- sources: LICENSE + text: |- + Copyright 2020 Gregor Martynus + + 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. +- sources: README.md + text: "[MIT](LICENSE)" +notices: [] diff --git a/.licenses/npm/@octokit/plugin-retry.dep.yml b/.licenses/npm/@octokit/plugin-retry.dep.yml new file mode 100644 index 000000000..ca0bd41fb --- /dev/null +++ b/.licenses/npm/@octokit/plugin-retry.dep.yml @@ -0,0 +1,34 @@ +--- +name: "@octokit/plugin-retry" +version: 5.0.2 +type: npm +summary: Automatic retry plugin for octokit +homepage: +license: mit +licenses: +- sources: LICENSE + text: | + MIT License + + Copyright (c) 2018 Octokit contributors + + 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. +- sources: README.md + text: "[MIT](LICENSE)" +notices: [] diff --git a/.licenses/npm/@octokit/types.dep.yml b/.licenses/npm/@octokit/types-6.41.0.dep.yml similarity index 99% rename from .licenses/npm/@octokit/types.dep.yml rename to .licenses/npm/@octokit/types-6.41.0.dep.yml index d13289b29..c5efe95e6 100644 --- a/.licenses/npm/@octokit/types.dep.yml +++ b/.licenses/npm/@octokit/types-6.41.0.dep.yml @@ -3,7 +3,7 @@ name: "@octokit/types" version: 6.41.0 type: npm summary: Shared TypeScript definitions for Octokit projects -homepage: +homepage: license: mit licenses: - sources: LICENSE diff --git a/.licenses/npm/@octokit/types-9.2.3.dep.yml b/.licenses/npm/@octokit/types-9.2.3.dep.yml new file mode 100644 index 000000000..f8fff6183 --- /dev/null +++ b/.licenses/npm/@octokit/types-9.2.3.dep.yml @@ -0,0 +1,20 @@ +--- +name: "@octokit/types" +version: 9.2.3 +type: npm +summary: Shared TypeScript definitions for Octokit projects +homepage: +license: mit +licenses: +- sources: LICENSE + text: | + MIT License Copyright (c) 2019 Octokit contributors + + 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 (including the next paragraph) 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. +- sources: README.md + text: "[MIT](LICENSE)" +notices: [] diff --git a/.licenses/npm/bottleneck.dep.yml b/.licenses/npm/bottleneck.dep.yml new file mode 100644 index 000000000..af9f462b3 --- /dev/null +++ b/.licenses/npm/bottleneck.dep.yml @@ -0,0 +1,31 @@ +--- +name: bottleneck +version: 2.19.5 +type: npm +summary: Distributed task scheduler and rate limiter +homepage: +license: mit +licenses: +- sources: LICENSE + text: | + The MIT License (MIT) + + Copyright (c) 2014 Simon Grondin + + 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. +notices: [] From 112b189678b2068975ebdfa6ca77b8ca18f0f62d Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Mon, 12 Jun 2023 23:52:55 -0400 Subject: [PATCH 13/20] revert to !!core.getInput('sync-labels') --- dist/index.js | 2 +- src/labeler.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dist/index.js b/dist/index.js index 025098e23..7285077ec 100644 --- a/dist/index.js +++ b/dist/index.js @@ -52,7 +52,7 @@ function run() { try { const token = core.getInput('repo-token'); const configPath = core.getInput('configuration-path', { required: true }); - const syncLabels = core.getBooleanInput('sync-labels'); + const syncLabels = !!core.getInput('sync-labels'); const dot = core.getBooleanInput('dot'); const prNumber = getPrNumber(); if (!prNumber) { diff --git a/src/labeler.ts b/src/labeler.ts index f42b839f7..2c9440cc1 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -19,7 +19,7 @@ export async function run() { try { const token = core.getInput('repo-token'); const configPath = core.getInput('configuration-path', {required: true}); - const syncLabels = core.getBooleanInput('sync-labels'); + const syncLabels = !!core.getInput('sync-labels'); const dot = core.getBooleanInput('dot'); const prNumber = getPrNumber(); From 3d7acab824d6cfd5dbeacc8fc155787310488483 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Tue, 13 Jun 2023 00:00:46 -0400 Subject: [PATCH 14/20] better warning for exceeded labels --- __tests__/main.test.ts | 3 ++- src/labeler.ts | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 0b839bf13..4222285eb 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -199,7 +199,8 @@ describe('run', () => { expect(coreWarningMock).toHaveBeenCalledTimes(1); expect(coreWarningMock).toHaveBeenCalledWith( - 'failed to add excess labels touched-a-pdf-file' + 'Maximum of 100 labels allowed. Excess labels: touched-a-pdf-file', + {title: 'Label limit for a PR exceeded'} ); }); }); diff --git a/src/labeler.ts b/src/labeler.ts index 2c9440cc1..650729ba6 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -61,7 +61,12 @@ export async function run() { await setLabels(client, prNumber, labels); if (excessLabels.length) { - core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); + core.warning( + `Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join( + ', ' + )}`, + {title: 'Label limit for a PR exceeded'} + ); } } catch (error: any) { core.error(error); From 92a11d88fa7a053b6f42d039f99bd4b41240abb2 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Tue, 13 Jun 2023 00:29:56 -0400 Subject: [PATCH 15/20] keep manually-added labels --- __tests__/main.test.ts | 8 ++++---- dist/index.js | 14 +++++++++++--- src/labeler.ts | 12 ++++++++++-- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 4222285eb..15849ce2a 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -127,7 +127,7 @@ describe('run', () => { mockGitHubResponseChangedFiles('foo.txt'); getPullMock.mockResolvedValue({ data: { - labels: [{name: 'touched-a-pdf-file'}] + labels: [{name: 'touched-a-pdf-file'}, {name: 'manually-added'}] } }); @@ -138,7 +138,7 @@ describe('run', () => { owner: 'monalisa', repo: 'helloworld', issue_number: 123, - labels: [] + labels: ['manually-added'] }); }); @@ -153,7 +153,7 @@ describe('run', () => { mockGitHubResponseChangedFiles('foo.txt'); getPullMock.mockResolvedValue({ data: { - labels: [{name: 'touched-a-pdf-file'}] + labels: [{name: 'touched-a-pdf-file'}, {name: 'manually-added'}] } }); @@ -164,7 +164,7 @@ describe('run', () => { owner: 'monalisa', repo: 'helloworld', issue_number: 123, - labels: ['touched-a-pdf-file'] + labels: ['touched-a-pdf-file', 'manually-added'] }); }); diff --git a/dist/index.js b/dist/index.js index 7285077ec..0e7f038ec 100644 --- a/dist/index.js +++ b/dist/index.js @@ -68,13 +68,15 @@ function run() { core.debug(`fetching changed files for pr #${prNumber}`); const changedFiles = yield getChangedFiles(client, prNumber); const labelGlobs = yield getLabelGlobs(client, configPath); - const pullRequestLabels = pullRequest.labels.map(label => label.name); - const labels = syncLabels ? [] : pullRequestLabels; + const labels = pullRequest.labels.map(label => label.name); for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot) && !labels.includes(label)) { labels.push(label); } + else if (syncLabels) { + removeLabel(labels, label); + } } // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, // and extract the excess into `excessLabels` @@ -82,7 +84,7 @@ function run() { // set labels regardless if array has a length or not yield setLabels(client, prNumber, labels); if (excessLabels.length) { - core.warning(`failed to add excess labels ${excessLabels.join(', ')}`); + core.warning(`Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join(', ')}`, { title: 'Label limit for a PR exceeded' }); } } catch (error) { @@ -223,6 +225,12 @@ function checkMatch(changedFiles, matchConfig, dot) { } return true; } +function removeLabel(labels, label) { + const labelIndex = labels.indexOf(label); + if (labelIndex > -1) { + labels.splice(labelIndex, 1); + } +} function setLabels(client, prNumber, labels) { return __awaiter(this, void 0, void 0, function* () { yield client.rest.issues.setLabels({ diff --git a/src/labeler.ts b/src/labeler.ts index 650729ba6..5344b0531 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -43,13 +43,14 @@ export async function run() { configPath ); - const pullRequestLabels = pullRequest.labels.map(label => label.name); - const labels: string[] = syncLabels ? [] : pullRequestLabels; + const labels: string[] = pullRequest.labels.map(label => label.name); for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot) && !labels.includes(label)) { labels.push(label); + } else if (syncLabels) { + removeLabel(labels, label); } } @@ -254,6 +255,13 @@ function checkMatch( return true; } +function removeLabel(labels: string[], label: string): void { + const labelIndex = labels.indexOf(label); + if (labelIndex > -1) { + labels.splice(labelIndex, 1); + } +} + async function setLabels( client: ClientType, prNumber: number, From ffc8ca71660259ba998185fbc272b7650a25ca43 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Tue, 13 Jun 2023 08:11:27 -0400 Subject: [PATCH 16/20] nest the dedupe logic --- dist/index.js | 6 ++++-- src/labeler.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/dist/index.js b/dist/index.js index 0e7f038ec..fb2fbc31a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -71,8 +71,10 @@ function run() { const labels = pullRequest.labels.map(label => label.name); for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs, dot) && !labels.includes(label)) { - labels.push(label); + if (checkGlobs(changedFiles, globs, dot)) { + if (!labels.includes(label)) { + labels.push(label); + } } else if (syncLabels) { removeLabel(labels, label); diff --git a/src/labeler.ts b/src/labeler.ts index 5344b0531..74284ac36 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -47,8 +47,10 @@ export async function run() { for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); - if (checkGlobs(changedFiles, globs, dot) && !labels.includes(label)) { - labels.push(label); + if (checkGlobs(changedFiles, globs, dot)) { + if (!labels.includes(label)) { + labels.push(label); + } } else if (syncLabels) { removeLabel(labels, label); } From 3c2f1346467fbe0c7d12c50b485ecc08ba2e36ba Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Thu, 15 Jun 2023 11:21:18 -0400 Subject: [PATCH 17/20] rename `removeLabel` to `removeLabelFromList` to avoid confusion --- dist/index.js | 4 ++-- src/labeler.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dist/index.js b/dist/index.js index fb2fbc31a..a40667c00 100644 --- a/dist/index.js +++ b/dist/index.js @@ -77,7 +77,7 @@ function run() { } } else if (syncLabels) { - removeLabel(labels, label); + removeLabelFromList(labels, label); } } // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, @@ -227,7 +227,7 @@ function checkMatch(changedFiles, matchConfig, dot) { } return true; } -function removeLabel(labels, label) { +function removeLabelFromList(labels, label) { const labelIndex = labels.indexOf(label); if (labelIndex > -1) { labels.splice(labelIndex, 1); diff --git a/src/labeler.ts b/src/labeler.ts index 74284ac36..8853fb8b8 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -52,7 +52,7 @@ export async function run() { labels.push(label); } } else if (syncLabels) { - removeLabel(labels, label); + removeLabelFromList(labels, label); } } @@ -257,7 +257,7 @@ function checkMatch( return true; } -function removeLabel(labels: string[], label: string): void { +function removeLabelFromList(labels: string[], label: string): void { const labelIndex = labels.indexOf(label); if (labelIndex > -1) { labels.splice(labelIndex, 1); From 287ce905fbd124b3beff7e6efaf80170d63a49c7 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Mon, 19 Jun 2023 20:16:24 -0400 Subject: [PATCH 18/20] use Sets, and issue a call only if labels have actually changed --- __tests__/main.test.ts | 34 +++++----------------------------- dist/index.js | 26 ++++++++++++-------------- src/labeler.ts | 26 ++++++++++++-------------- 3 files changed, 29 insertions(+), 57 deletions(-) diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 15849ce2a..0c5f7ec68 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -91,13 +91,7 @@ describe('run', () => { await run(); - expect(setLabelsMock).toHaveBeenCalledTimes(1); - expect(setLabelsMock).toHaveBeenCalledWith({ - owner: 'monalisa', - repo: 'helloworld', - issue_number: 123, - labels: [] - }); + expect(setLabelsMock).toHaveBeenCalledTimes(0); }); it('(with dot: true) does not add labels to PRs that do not match our glob patterns', async () => { @@ -107,13 +101,7 @@ describe('run', () => { await run(); - expect(setLabelsMock).toHaveBeenCalledTimes(1); - expect(setLabelsMock).toHaveBeenCalledWith({ - owner: 'monalisa', - repo: 'helloworld', - issue_number: 123, - labels: [] - }); + expect(setLabelsMock).toHaveBeenCalledTimes(0); }); it('(with sync-labels: true) it deletes preexisting PR labels that no longer match the glob pattern', async () => { @@ -159,16 +147,10 @@ describe('run', () => { await run(); - expect(setLabelsMock).toHaveBeenCalledTimes(1); - expect(setLabelsMock).toHaveBeenCalledWith({ - owner: 'monalisa', - repo: 'helloworld', - issue_number: 123, - labels: ['touched-a-pdf-file', 'manually-added'] - }); + expect(setLabelsMock).toHaveBeenCalledTimes(0); }); - it('(with sync-labels: false) it sets only 100 labels and logs the rest', async () => { + it('(with sync-labels: false) it only logs the excess labels', async () => { configureInput({ 'repo-token': 'foo', 'configuration-path': 'bar', @@ -189,13 +171,7 @@ describe('run', () => { await run(); - expect(setLabelsMock).toHaveBeenCalledTimes(1); - expect(setLabelsMock).toHaveBeenCalledWith({ - owner: 'monalisa', - repo: 'helloworld', - issue_number: 123, - labels: existingLabels.map(label => label.name) - }); + expect(setLabelsMock).toHaveBeenCalledTimes(0); expect(coreWarningMock).toHaveBeenCalledTimes(1); expect(coreWarningMock).toHaveBeenCalledWith( diff --git a/dist/index.js b/dist/index.js index 0acdb27dd..8df9a8658 100644 --- a/dist/index.js +++ b/dist/index.js @@ -68,24 +68,25 @@ function run() { core.debug(`fetching changed files for pr #${prNumber}`); const changedFiles = yield getChangedFiles(client, prNumber); const labelGlobs = yield getLabelGlobs(client, configPath); - const labels = pullRequest.labels.map(label => label.name); + const prLabels = pullRequest.labels.map(label => label.name); + const allLabels = new Set(prLabels); for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot)) { - if (!labels.includes(label)) { - labels.push(label); + if (!allLabels.has(label)) { + allLabels.add(label); } } else if (syncLabels) { - removeLabelFromList(labels, label); + allLabels.delete(label); } } - // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, - // and extract the excess into `excessLabels` - const excessLabels = labels.splice(GITHUB_MAX_LABELS); + const labels = [...allLabels].slice(0, GITHUB_MAX_LABELS); + const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS); try { - // set labels regardless if array has a length or not - yield setLabels(client, prNumber, labels); + if (!isListEqual(prLabels, labels)) { + yield setLabels(client, prNumber, labels); + } if (excessLabels.length) { core.warning(`Maximum of ${GITHUB_MAX_LABELS} labels allowed. Excess labels: ${excessLabels.join(', ')}`, { title: 'Label limit for a PR exceeded' }); } @@ -241,11 +242,8 @@ function checkMatch(changedFiles, matchConfig, dot) { } return true; } -function removeLabelFromList(labels, label) { - const labelIndex = labels.indexOf(label); - if (labelIndex > -1) { - labels.splice(labelIndex, 1); - } +function isListEqual(listA, listB) { + return listA.length === listB.length && listA.every(el => listB.includes(el)); } function setLabels(client, prNumber, labels) { return __awaiter(this, void 0, void 0, function* () { diff --git a/src/labeler.ts b/src/labeler.ts index b0b128031..e483666c9 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -43,26 +43,27 @@ export async function run() { configPath ); - const labels: string[] = pullRequest.labels.map(label => label.name); + const prLabels: string[] = pullRequest.labels.map(label => label.name); + const allLabels: Set = new Set(prLabels); for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot)) { - if (!labels.includes(label)) { - labels.push(label); + if (!allLabels.has(label)) { + allLabels.add(label); } } else if (syncLabels) { - removeLabelFromList(labels, label); + allLabels.delete(label); } } - // this will mutate the `labels` array at a length of GITHUB_MAX_LABELS, - // and extract the excess into `excessLabels` - const excessLabels = labels.splice(GITHUB_MAX_LABELS); + const labels = [...allLabels].slice(0, GITHUB_MAX_LABELS); + const excessLabels = [...allLabels].slice(GITHUB_MAX_LABELS); try { - // set labels regardless if array has a length or not - await setLabels(client, prNumber, labels); + if (!isListEqual(prLabels, labels)) { + await setLabels(client, prNumber, labels); + } if (excessLabels.length) { core.warning( @@ -274,11 +275,8 @@ function checkMatch( return true; } -function removeLabelFromList(labels: string[], label: string): void { - const labelIndex = labels.indexOf(label); - if (labelIndex > -1) { - labels.splice(labelIndex, 1); - } +function isListEqual(listA: string[], listB: string[]): boolean { + return listA.length === listB.length && listA.every(el => listB.includes(el)); } async function setLabels( From 05a4b4bbbe87727643618c57cbcfd0c9698112c6 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Mon, 19 Jun 2023 20:18:23 -0400 Subject: [PATCH 19/20] remove IDE config folders from gitignore --- .gitignore | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index c18d04cad..867d24a55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,3 @@ .DS_Store node_modules/ -lib/ -.vscode/ -.idea/ \ No newline at end of file +lib/ \ No newline at end of file From c9ee1b4743fa4355018375139355c9908ac7b1e6 Mon Sep 17 00:00:00 2001 From: Mark Massoud Date: Mon, 19 Jun 2023 20:38:50 -0400 Subject: [PATCH 20/20] remove obsolete duplucation check --- dist/index.js | 4 +--- src/labeler.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/dist/index.js b/dist/index.js index 8df9a8658..076fac072 100644 --- a/dist/index.js +++ b/dist/index.js @@ -73,9 +73,7 @@ function run() { for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot)) { - if (!allLabels.has(label)) { - allLabels.add(label); - } + allLabels.add(label); } else if (syncLabels) { allLabels.delete(label); diff --git a/src/labeler.ts b/src/labeler.ts index e483666c9..d23fe4d61 100644 --- a/src/labeler.ts +++ b/src/labeler.ts @@ -49,9 +49,7 @@ export async function run() { for (const [label, globs] of labelGlobs.entries()) { core.debug(`processing ${label}`); if (checkGlobs(changedFiles, globs, dot)) { - if (!allLabels.has(label)) { - allLabels.add(label); - } + allLabels.add(label); } else if (syncLabels) { allLabels.delete(label); }