From 8dd7ce34091f25e315c3c2c90742af5faeac2818 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Wed, 19 Jul 2023 10:29:14 -0500 Subject: [PATCH] feat(approvals-satisfied): support collective approvals for shared code ownership (#392) * progress * base tests * finish all test cases * update test * maintain support for team slug * copyright header * add more test cases * refactor --- .github/CODEOWNERS | 5 +- dist/153.index.js | 56 +++-- dist/153.index.js.map | 2 +- dist/499.index.js | 87 ++++++-- dist/499.index.js.map | 2 +- dist/905.index.js | 56 +++-- dist/905.index.js.map | 2 +- src/helpers/approvals-satisfied.ts | 37 +++- src/utils/convert-to-team-slug.ts | 14 ++ src/utils/get-core-member-logins.ts | 26 +-- test/helpers/approvals-satisfied.test.ts | 251 ++++++++++++++-------- test/utils/get-core-member-logins.test.ts | 44 +++- 12 files changed, 404 insertions(+), 178 deletions(-) create mode 100644 src/utils/convert-to-team-slug.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 81a408340..9747c61ba 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,5 +4,6 @@ * @ExpediaGroup/github-helpers-committers -/file/path/1 @ExpediaGroup/test-owners-1 -/file/path/2 @ExpediaGroup/test-owners-2 +/file/path/1 @ExpediaGroup/test-owners-1 +/file/path/2 @ExpediaGroup/test-owners-2 +/file/path/shared @ExpediaGroup/test-shared-owners-1 @ExpediaGroup/test-shared-owners-2 diff --git a/dist/153.index.js b/dist/153.index.js index 61f8ff724..ad9303049 100644 --- a/dist/153.index.js +++ b/dist/153.index.js @@ -208,6 +208,29 @@ class HelperInputs { } +/***/ }), + +/***/ 489: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "$": () => (/* binding */ convertToTeamSlug) +/* harmony export */ }); +/* +Copyright 2021 Expedia, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const convertToTeamSlug = (codeOwner) => codeOwner.substring(codeOwner.indexOf('/') + 1); + + /***/ }), /***/ 9180: @@ -261,8 +284,8 @@ const paginateAllChangedFilepaths = (pull_number, page = 1) => __awaiter(void 0, /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "F": () => (/* binding */ getCoreTeamsAndLogins), -/* harmony export */ "c": () => (/* binding */ getCoreMemberLogins) +/* harmony export */ "c": () => (/* binding */ getCoreMemberLogins), +/* harmony export */ "q": () => (/* binding */ getRequiredCodeOwnersEntries) /* harmony export */ }); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2186); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_core__WEBPACK_IMPORTED_MODULE_0__); @@ -276,6 +299,7 @@ const paginateAllChangedFilepaths = (pull_number, page = 1) => __awaiter(void 0, /* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8710); /* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(bluebird__WEBPACK_IMPORTED_MODULE_5__); /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(6161); +/* harmony import */ var _convert_to_team_slug__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(489); /* Copyright 2021 Expedia, Inc. Licensed under the Apache License, Version 2.0 (the "License"); @@ -304,12 +328,19 @@ var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _argume + const getCoreMemberLogins = (pull_number, teams) => __awaiter(void 0, void 0, void 0, function* () { - const teamsAndLogins = yield getCoreTeamsAndLogins(pull_number, teams); + const codeOwners = teams !== null && teams !== void 0 ? teams : getCodeOwnersFromEntries(yield getRequiredCodeOwnersEntries(pull_number)); + const teamsAndLogins = yield getCoreTeamsAndLogins(codeOwners); return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(teamsAndLogins.map(({ login }) => login)); }); -const getCoreTeamsAndLogins = (pull_number, teams) => __awaiter(void 0, void 0, void 0, function* () { - const codeOwners = teams !== null && teams !== void 0 ? teams : (yield getRequiredCodeOwners(pull_number)); +const getRequiredCodeOwnersEntries = (pull_number) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + const codeOwners = (_a = (yield (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.loadOwners)(process.cwd()))) !== null && _a !== void 0 ? _a : []; + const changedFilePaths = yield (0,_get_changed_filepaths__WEBPACK_IMPORTED_MODULE_4__/* .getChangedFilepaths */ .s)(pull_number); + return changedFilePaths.map(filePath => (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.matchFile)(filePath, codeOwners)).filter(Boolean); +}); +const getCoreTeamsAndLogins = (codeOwners) => __awaiter(void 0, void 0, void 0, function* () { if (!(codeOwners === null || codeOwners === void 0 ? void 0 : codeOwners.length)) { _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed('No code owners found. Please provide a "teams" input or set up a CODEOWNERS file in your repo.'); throw new Error(); @@ -324,18 +355,13 @@ const getCoreTeamsAndLogins = (pull_number, teams) => __awaiter(void 0, void 0, })); return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.union)(...teamsAndLogins); }); -const getRequiredCodeOwners = (pull_number) => __awaiter(void 0, void 0, void 0, function* () { - var _a; - const codeOwners = (_a = (yield (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.loadOwners)(process.cwd()))) !== null && _a !== void 0 ? _a : []; - const changedFilePaths = yield (0,_get_changed_filepaths__WEBPACK_IMPORTED_MODULE_4__/* .getChangedFilepaths */ .s)(pull_number); - const matchingCodeOwners = changedFilePaths.map(filePath => (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.matchFile)(filePath, codeOwners)); - return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(matchingCodeOwners - .filter(Boolean) - .map(owner => owner.owners) +const getCodeOwnersFromEntries = (codeOwnersEntries) => { + return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(codeOwnersEntries + .map(entry => entry.owners) .flat() .filter(Boolean) - .map(owner => owner.substring(owner.indexOf('/') + 1))); -}); + .map(codeOwner => (0,_convert_to_team_slug__WEBPACK_IMPORTED_MODULE_7__/* .convertToTeamSlug */ .$)(codeOwner))); +}; /***/ }), diff --git a/dist/153.index.js.map b/dist/153.index.js.map index 155b6adf4..3306a3651 100644 --- a/dist/153.index.js.map +++ b/dist/153.index.js.map @@ -1 +1 @@ -{"version":3,"file":"153.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;AAWA;;;;;;;;;;;;;;;;;;;;;;;;;;AC7CA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AAEA;AAOA;AACA;AAIA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AACA;AAGA;AACA;AACA;AACA;AACA;AAAA;AAGA;AACA;;;;;;;;;;;;;;;;;;AC1EA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAyCA;;;;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;ACjCA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AAEA;AACA;AAEA;;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;AC1DA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AAQA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA","sources":["webpack://github-helpers/./src/constants.ts","webpack://github-helpers/./src/helpers/assign-pr-reviewers.ts","webpack://github-helpers/./src/octokit.ts","webpack://github-helpers/./src/types/generated.ts","webpack://github-helpers/./src/utils/get-changed-filepaths.ts","webpack://github-helpers/./src/utils/get-core-member-logins.ts","webpack://github-helpers/./src/utils/notify-user.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// These extra headers are for experimental API features on Github Enterprise. See https://docs.github.com/en/enterprise-server@3.0/rest/overview/api-previews for details.\nconst PREVIEWS = ['ant-man', 'flash', 'groot', 'inertia', 'starfox'];\nexport const GITHUB_OPTIONS = {\n headers: {\n accept: PREVIEWS.map(preview => `application/vnd.github.${preview}-preview+json`).join()\n }\n};\n\nexport const DEFAULT_EXEMPT_DESCRIPTION = 'Passed in case the check is exempt.';\nexport const DEFAULT_PIPELINE_STATUS = 'Pipeline Status';\nexport const DEFAULT_PIPELINE_DESCRIPTION = 'Pipeline clear.';\nexport const PRODUCTION_ENVIRONMENT = 'production';\nexport const LATE_REVIEW = 'Late Review';\nexport const CORE_APPROVED_PR_LABEL = 'CORE APPROVED';\nexport const PEER_APPROVED_PR_LABEL = 'PEER APPROVED';\nexport const READY_FOR_MERGE_PR_LABEL = 'READY FOR MERGE';\nexport const MERGE_QUEUE_STATUS = 'QUEUE CHECKER';\nexport const QUEUED_FOR_MERGE_PREFIX = 'QUEUED FOR MERGE';\nexport const FIRST_QUEUED_PR_LABEL = `${QUEUED_FOR_MERGE_PREFIX} #1`;\nexport const JUMP_THE_QUEUE_PR_LABEL = 'JUMP THE QUEUE';\nexport const DEFAULT_PR_TITLE_REGEX = '^(build|ci|chore|docs|feat|fix|perf|refactor|style|test|revert|Revert|BREAKING CHANGE)((.*))?: .+$';\nexport const COPYRIGHT_HEADER = `/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/`;\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { HelperInputs } from '../types/generated';\nimport { context } from '@actions/github';\nimport { getCoreMemberLogins } from '../utils/get-core-member-logins';\nimport { map } from 'bluebird';\nimport { notifyUser } from '../utils/notify-user';\nimport { octokit } from '../octokit';\nimport { sampleSize } from 'lodash';\nimport { CORE_APPROVED_PR_LABEL } from '../constants';\n\nexport class AssignPrReviewer extends HelperInputs {\n teams?: string;\n login?: string;\n number_of_assignees?: string;\n slack_webhook_url?: string;\n pull_number?: string;\n}\n\nexport const assignPrReviewers = async ({\n teams,\n login,\n number_of_assignees = '1',\n slack_webhook_url,\n pull_number = String(context.issue.number)\n}: AssignPrReviewer) => {\n const coreMemberLogins = await getCoreMemberLogins(context.issue.number, teams?.split('\\n'));\n const {\n data: { user, labels }\n } = await octokit.pulls.get({ pull_number: context.issue.number, ...context.repo });\n\n if (login && coreMemberLogins.includes(login)) {\n core.info('Already a core member, no need to assign.');\n return;\n }\n\n if (labels?.find(label => label.name === CORE_APPROVED_PR_LABEL)) {\n core.info('Already approved by a core member, no need to assign.');\n return;\n }\n const prAuthorUsername = user?.login;\n const filteredCoreMemberLogins = coreMemberLogins.filter(userName => userName !== prAuthorUsername);\n const assignees = sampleSize(filteredCoreMemberLogins, Number(number_of_assignees));\n\n await octokit.issues.addAssignees({\n assignees,\n issue_number: Number(pull_number),\n ...context.repo\n });\n\n if (slack_webhook_url) {\n await map(\n assignees,\n async assignee =>\n notifyUser({\n login: assignee,\n pull_number: Number(pull_number),\n slack_webhook_url\n }),\n { concurrency: 1 }\n );\n }\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport class HelperInputs {\n helper?: string;\n github_token?: string;\n body?: string;\n project_name?: string;\n project_destination_column_name?: string;\n note?: string;\n project_origin_column_name?: string;\n sha?: string;\n context?: string;\n state?: string;\n description?: string;\n target_url?: string;\n environment?: string;\n environment_url?: string;\n label?: string;\n labels?: string;\n paths?: string;\n ignore_globs?: string;\n extensions?: string;\n override_filter_paths?: string;\n batches?: string;\n pattern?: string;\n teams?: string;\n login?: string;\n paths_no_filter?: string;\n slack_webhook_url?: string;\n number_of_assignees?: string;\n number_of_reviewers?: string;\n globs?: string;\n override_filter_globs?: string;\n title?: string;\n seconds?: string;\n pull_number?: string;\n base?: string;\n head?: string;\n days?: string;\n no_evict_upon_conflict?: string;\n skip_if_already_set?: string;\n delimiter?: string;\n team?: string;\n}\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { ChangedFilesList } from '../types/github';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const getChangedFilepaths = async (pull_number: number) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n return changedFiles.map(file => file.filename);\n};\n\nconst paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => {\n const response = await octokit.pulls.listFiles({\n pull_number,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1));\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { loadOwners, matchFile } from 'codeowners-utils';\nimport { uniq, union } from 'lodash';\nimport { context } from '@actions/github';\nimport { getChangedFilepaths } from './get-changed-filepaths';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => {\n const teamsAndLogins = await getCoreTeamsAndLogins(pull_number, teams);\n return uniq(teamsAndLogins.map(({ login }) => login));\n};\n\nexport const getCoreTeamsAndLogins = async (pull_number: number, teams?: string[]) => {\n const codeOwners = teams ?? (await getRequiredCodeOwners(pull_number));\n\n if (!codeOwners?.length) {\n core.setFailed('No code owners found. Please provide a \"teams\" input or set up a CODEOWNERS file in your repo.');\n throw new Error();\n }\n\n const teamsAndLogins = await map(codeOwners, async team =>\n octokit.teams\n .listMembersInOrg({\n org: context.repo.owner,\n team_slug: team,\n per_page: 100\n })\n .then(listMembersResponse => listMembersResponse.data.map(({ login }) => ({ team, login })))\n );\n return union(...teamsAndLogins);\n};\n\nconst getRequiredCodeOwners = async (pull_number: number) => {\n const codeOwners = (await loadOwners(process.cwd())) ?? [];\n const changedFilePaths = await getChangedFilepaths(pull_number);\n const matchingCodeOwners = changedFilePaths.map(filePath => matchFile(filePath, codeOwners));\n return uniq(\n matchingCodeOwners\n .filter(Boolean)\n .map(owner => owner.owners)\n .flat()\n .filter(Boolean)\n .map(owner => owner.substring(owner.indexOf('/') + 1))\n );\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport axios from 'axios';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\ninterface NotifyUser {\n login: string;\n pull_number: number;\n slack_webhook_url: string;\n}\n\nexport const notifyUser = async ({ login, pull_number, slack_webhook_url }: NotifyUser) => {\n core.info(`Notifying user ${login}...`);\n const {\n data: { email }\n } = await octokit.users.getByUsername({ username: login });\n if (!email) {\n core.info(`No github email found for user ${login}. Ensure you have set your email to be publicly visible on your Github profile.`);\n return;\n }\n const {\n data: { title, html_url }\n } = await octokit.pulls.get({ pull_number, ...context.repo });\n\n try {\n await axios.post(slack_webhook_url, {\n assignee: email,\n title,\n html_url,\n repo: context.repo.repo\n });\n } catch (error) {\n core.warning('User notification failed');\n core.warning(error as Error);\n }\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"153.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;AAWA;;;;;;;;;;;;;;;;;;;;;;;;;;AC7CA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AAEA;AAOA;AACA;AAIA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAMA;AACA;AAGA;AACA;AACA;AACA;AACA;AAAA;AAGA;AACA;;;;;;;;;;;;;;;;;;AC1EA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAyCA;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;AAEA;;;;;;;;;;;;;;ACbA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;ACjCA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;AC5DA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AAQA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA","sources":["webpack://github-helpers/./src/constants.ts","webpack://github-helpers/./src/helpers/assign-pr-reviewers.ts","webpack://github-helpers/./src/octokit.ts","webpack://github-helpers/./src/types/generated.ts","webpack://github-helpers/./src/utils/convert-to-team-slug.ts","webpack://github-helpers/./src/utils/get-changed-filepaths.ts","webpack://github-helpers/./src/utils/get-core-member-logins.ts","webpack://github-helpers/./src/utils/notify-user.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// These extra headers are for experimental API features on Github Enterprise. See https://docs.github.com/en/enterprise-server@3.0/rest/overview/api-previews for details.\nconst PREVIEWS = ['ant-man', 'flash', 'groot', 'inertia', 'starfox'];\nexport const GITHUB_OPTIONS = {\n headers: {\n accept: PREVIEWS.map(preview => `application/vnd.github.${preview}-preview+json`).join()\n }\n};\n\nexport const DEFAULT_EXEMPT_DESCRIPTION = 'Passed in case the check is exempt.';\nexport const DEFAULT_PIPELINE_STATUS = 'Pipeline Status';\nexport const DEFAULT_PIPELINE_DESCRIPTION = 'Pipeline clear.';\nexport const PRODUCTION_ENVIRONMENT = 'production';\nexport const LATE_REVIEW = 'Late Review';\nexport const CORE_APPROVED_PR_LABEL = 'CORE APPROVED';\nexport const PEER_APPROVED_PR_LABEL = 'PEER APPROVED';\nexport const READY_FOR_MERGE_PR_LABEL = 'READY FOR MERGE';\nexport const MERGE_QUEUE_STATUS = 'QUEUE CHECKER';\nexport const QUEUED_FOR_MERGE_PREFIX = 'QUEUED FOR MERGE';\nexport const FIRST_QUEUED_PR_LABEL = `${QUEUED_FOR_MERGE_PREFIX} #1`;\nexport const JUMP_THE_QUEUE_PR_LABEL = 'JUMP THE QUEUE';\nexport const DEFAULT_PR_TITLE_REGEX = '^(build|ci|chore|docs|feat|fix|perf|refactor|style|test|revert|Revert|BREAKING CHANGE)((.*))?: .+$';\nexport const COPYRIGHT_HEADER = `/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/`;\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { HelperInputs } from '../types/generated';\nimport { context } from '@actions/github';\nimport { getCoreMemberLogins } from '../utils/get-core-member-logins';\nimport { map } from 'bluebird';\nimport { notifyUser } from '../utils/notify-user';\nimport { octokit } from '../octokit';\nimport { sampleSize } from 'lodash';\nimport { CORE_APPROVED_PR_LABEL } from '../constants';\n\nexport class AssignPrReviewer extends HelperInputs {\n teams?: string;\n login?: string;\n number_of_assignees?: string;\n slack_webhook_url?: string;\n pull_number?: string;\n}\n\nexport const assignPrReviewers = async ({\n teams,\n login,\n number_of_assignees = '1',\n slack_webhook_url,\n pull_number = String(context.issue.number)\n}: AssignPrReviewer) => {\n const coreMemberLogins = await getCoreMemberLogins(context.issue.number, teams?.split('\\n'));\n const {\n data: { user, labels }\n } = await octokit.pulls.get({ pull_number: context.issue.number, ...context.repo });\n\n if (login && coreMemberLogins.includes(login)) {\n core.info('Already a core member, no need to assign.');\n return;\n }\n\n if (labels?.find(label => label.name === CORE_APPROVED_PR_LABEL)) {\n core.info('Already approved by a core member, no need to assign.');\n return;\n }\n const prAuthorUsername = user?.login;\n const filteredCoreMemberLogins = coreMemberLogins.filter(userName => userName !== prAuthorUsername);\n const assignees = sampleSize(filteredCoreMemberLogins, Number(number_of_assignees));\n\n await octokit.issues.addAssignees({\n assignees,\n issue_number: Number(pull_number),\n ...context.repo\n });\n\n if (slack_webhook_url) {\n await map(\n assignees,\n async assignee =>\n notifyUser({\n login: assignee,\n pull_number: Number(pull_number),\n slack_webhook_url\n }),\n { concurrency: 1 }\n );\n }\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport class HelperInputs {\n helper?: string;\n github_token?: string;\n body?: string;\n project_name?: string;\n project_destination_column_name?: string;\n note?: string;\n project_origin_column_name?: string;\n sha?: string;\n context?: string;\n state?: string;\n description?: string;\n target_url?: string;\n environment?: string;\n environment_url?: string;\n label?: string;\n labels?: string;\n paths?: string;\n ignore_globs?: string;\n extensions?: string;\n override_filter_paths?: string;\n batches?: string;\n pattern?: string;\n teams?: string;\n login?: string;\n paths_no_filter?: string;\n slack_webhook_url?: string;\n number_of_assignees?: string;\n number_of_reviewers?: string;\n globs?: string;\n override_filter_globs?: string;\n title?: string;\n seconds?: string;\n pull_number?: string;\n base?: string;\n head?: string;\n days?: string;\n no_evict_upon_conflict?: string;\n skip_if_already_set?: string;\n delimiter?: string;\n team?: string;\n}\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport const convertToTeamSlug = (codeOwner: string) => codeOwner.substring(codeOwner.indexOf('/') + 1);\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { ChangedFilesList } from '../types/github';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const getChangedFilepaths = async (pull_number: number) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n return changedFiles.map(file => file.filename);\n};\n\nconst paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => {\n const response = await octokit.pulls.listFiles({\n pull_number,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1));\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { CodeOwnersEntry, loadOwners, matchFile } from 'codeowners-utils';\nimport { uniq, union } from 'lodash';\nimport { context } from '@actions/github';\nimport { getChangedFilepaths } from './get-changed-filepaths';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\nimport { convertToTeamSlug } from './convert-to-team-slug';\n\nexport const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => {\n const codeOwners = teams ?? getCodeOwnersFromEntries(await getRequiredCodeOwnersEntries(pull_number));\n const teamsAndLogins = await getCoreTeamsAndLogins(codeOwners);\n return uniq(teamsAndLogins.map(({ login }) => login));\n};\n\nexport const getRequiredCodeOwnersEntries = async (pull_number: number): Promise => {\n const codeOwners = (await loadOwners(process.cwd())) ?? [];\n const changedFilePaths = await getChangedFilepaths(pull_number);\n return changedFilePaths.map(filePath => matchFile(filePath, codeOwners)).filter(Boolean);\n};\n\nconst getCoreTeamsAndLogins = async (codeOwners?: string[]) => {\n if (!codeOwners?.length) {\n core.setFailed('No code owners found. Please provide a \"teams\" input or set up a CODEOWNERS file in your repo.');\n throw new Error();\n }\n\n const teamsAndLogins = await map(codeOwners, async team =>\n octokit.teams\n .listMembersInOrg({\n org: context.repo.owner,\n team_slug: team,\n per_page: 100\n })\n .then(listMembersResponse => listMembersResponse.data.map(({ login }) => ({ team, login })))\n );\n return union(...teamsAndLogins);\n};\n\nconst getCodeOwnersFromEntries = (codeOwnersEntries: CodeOwnersEntry[]) => {\n return uniq(\n codeOwnersEntries\n .map(entry => entry.owners)\n .flat()\n .filter(Boolean)\n .map(codeOwner => convertToTeamSlug(codeOwner))\n );\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport axios from 'axios';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\ninterface NotifyUser {\n login: string;\n pull_number: number;\n slack_webhook_url: string;\n}\n\nexport const notifyUser = async ({ login, pull_number, slack_webhook_url }: NotifyUser) => {\n core.info(`Notifying user ${login}...`);\n const {\n data: { email }\n } = await octokit.users.getByUsername({ username: login });\n if (!email) {\n core.info(`No github email found for user ${login}. Ensure you have set your email to be publicly visible on your Github profile.`);\n return;\n }\n const {\n data: { title, html_url }\n } = await octokit.pulls.get({ pull_number, ...context.repo });\n\n try {\n await axios.post(slack_webhook_url, {\n assignee: email,\n title,\n html_url,\n repo: context.repo.repo\n });\n } catch (error) {\n core.warning('User notification failed');\n core.warning(error as Error);\n }\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/dist/499.index.js b/dist/499.index.js index 899d09764..c9da737c7 100644 --- a/dist/499.index.js +++ b/dist/499.index.js @@ -16,8 +16,9 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _actions_github__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_github__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6161); /* harmony import */ var _utils_get_core_member_logins__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7290); -/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(250); -/* harmony import */ var lodash__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(lodash__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(8710); +/* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(bluebird__WEBPACK_IMPORTED_MODULE_3__); +/* harmony import */ var _utils_convert_to_team_slug__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(489); /* Copyright 2021 Expedia, Inc. Licensed under the Apache License, Version 2.0 (the "License"); @@ -44,23 +45,37 @@ var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _argume + class ApprovalsSatisfied extends _types_generated__WEBPACK_IMPORTED_MODULE_4__/* .HelperInputs */ .s { } const approvalsSatisfied = ({ teams, number_of_reviewers = '1', pull_number } = {}) => __awaiter(void 0, void 0, void 0, function* () { const prNumber = pull_number ? Number(pull_number) : _actions_github__WEBPACK_IMPORTED_MODULE_0__.context.issue.number; const { data: reviews } = yield _octokit__WEBPACK_IMPORTED_MODULE_1__/* .octokit.pulls.listReviews */ .K.pulls.listReviews(Object.assign({ pull_number: prNumber }, _actions_github__WEBPACK_IMPORTED_MODULE_0__.context.repo)); - const teamsAndLogins = yield (0,_utils_get_core_member_logins__WEBPACK_IMPORTED_MODULE_2__/* .getCoreTeamsAndLogins */ .F)(prNumber, teams === null || teams === void 0 ? void 0 : teams.split('\n')); const approverLogins = reviews .filter(({ state }) => state === 'APPROVED') .map(({ user }) => user === null || user === void 0 ? void 0 : user.login) .filter(Boolean); - const codeOwnerTeams = (0,lodash__WEBPACK_IMPORTED_MODULE_3__.uniq)(teamsAndLogins.map(({ team }) => team)); - return codeOwnerTeams.every(team => { - const membersOfCodeOwnerTeam = (0,lodash__WEBPACK_IMPORTED_MODULE_3__.groupBy)(teamsAndLogins, 'team')[team]; - const numberOfApprovalsForTeam = membersOfCodeOwnerTeam.filter(({ login }) => approverLogins.includes(login)).length; - return numberOfApprovalsForTeam >= Number(number_of_reviewers); + const teamsList = teams === null || teams === void 0 ? void 0 : teams.split('\n'); + const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : yield (0,_utils_get_core_member_logins__WEBPACK_IMPORTED_MODULE_2__/* .getRequiredCodeOwnersEntries */ .q)(prNumber); + const codeOwnersEntrySatisfiesApprovals = (entry) => __awaiter(void 0, void 0, void 0, function* () { + const teamsAndLoginsLists = yield (0,bluebird__WEBPACK_IMPORTED_MODULE_3__.map)(entry.owners, (team) => __awaiter(void 0, void 0, void 0, function* () { + const { data } = yield _octokit__WEBPACK_IMPORTED_MODULE_1__/* .octokit.teams.listMembersInOrg */ .K.teams.listMembersInOrg({ + org: _actions_github__WEBPACK_IMPORTED_MODULE_0__.context.repo.owner, + team_slug: (0,_utils_convert_to_team_slug__WEBPACK_IMPORTED_MODULE_5__/* .convertToTeamSlug */ .$)(team), + per_page: 100 + }); + return data.map(({ login }) => ({ team, login })); + })); + const codeOwnerLogins = teamsAndLoginsLists.flat().map(({ login }) => login); + const numberOfCollectiveApprovalsAcrossTeams = approverLogins.filter(login => codeOwnerLogins.includes(login)).length; + const numberOfApprovalsForSingleTeam = codeOwnerLogins.filter(login => approverLogins.includes(login)).length; + const numberOfApprovals = entry.owners.length > 1 ? numberOfCollectiveApprovalsAcrossTeams : numberOfApprovalsForSingleTeam; + return numberOfApprovals >= Number(number_of_reviewers); }); + const booleans = yield Promise.all(requiredCodeOwnersEntries.map(codeOwnersEntrySatisfiesApprovals)); + return booleans.every(Boolean); }); +const createArtificialCodeOwnersEntry = (teams) => teams.map(team => ({ owners: [team] })); /***/ }), @@ -121,6 +136,29 @@ class HelperInputs { } +/***/ }), + +/***/ 489: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "$": () => (/* binding */ convertToTeamSlug) +/* harmony export */ }); +/* +Copyright 2021 Expedia, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const convertToTeamSlug = (codeOwner) => codeOwner.substring(codeOwner.indexOf('/') + 1); + + /***/ }), /***/ 9180: @@ -174,8 +212,8 @@ const paginateAllChangedFilepaths = (pull_number, page = 1) => __awaiter(void 0, /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "F": () => (/* binding */ getCoreTeamsAndLogins), -/* harmony export */ "c": () => (/* binding */ getCoreMemberLogins) +/* harmony export */ "c": () => (/* binding */ getCoreMemberLogins), +/* harmony export */ "q": () => (/* binding */ getRequiredCodeOwnersEntries) /* harmony export */ }); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2186); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_core__WEBPACK_IMPORTED_MODULE_0__); @@ -189,6 +227,7 @@ const paginateAllChangedFilepaths = (pull_number, page = 1) => __awaiter(void 0, /* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8710); /* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(bluebird__WEBPACK_IMPORTED_MODULE_5__); /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(6161); +/* harmony import */ var _convert_to_team_slug__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(489); /* Copyright 2021 Expedia, Inc. Licensed under the Apache License, Version 2.0 (the "License"); @@ -217,12 +256,19 @@ var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _argume + const getCoreMemberLogins = (pull_number, teams) => __awaiter(void 0, void 0, void 0, function* () { - const teamsAndLogins = yield getCoreTeamsAndLogins(pull_number, teams); + const codeOwners = teams !== null && teams !== void 0 ? teams : getCodeOwnersFromEntries(yield getRequiredCodeOwnersEntries(pull_number)); + const teamsAndLogins = yield getCoreTeamsAndLogins(codeOwners); return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(teamsAndLogins.map(({ login }) => login)); }); -const getCoreTeamsAndLogins = (pull_number, teams) => __awaiter(void 0, void 0, void 0, function* () { - const codeOwners = teams !== null && teams !== void 0 ? teams : (yield getRequiredCodeOwners(pull_number)); +const getRequiredCodeOwnersEntries = (pull_number) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + const codeOwners = (_a = (yield (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.loadOwners)(process.cwd()))) !== null && _a !== void 0 ? _a : []; + const changedFilePaths = yield (0,_get_changed_filepaths__WEBPACK_IMPORTED_MODULE_4__/* .getChangedFilepaths */ .s)(pull_number); + return changedFilePaths.map(filePath => (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.matchFile)(filePath, codeOwners)).filter(Boolean); +}); +const getCoreTeamsAndLogins = (codeOwners) => __awaiter(void 0, void 0, void 0, function* () { if (!(codeOwners === null || codeOwners === void 0 ? void 0 : codeOwners.length)) { _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed('No code owners found. Please provide a "teams" input or set up a CODEOWNERS file in your repo.'); throw new Error(); @@ -237,18 +283,13 @@ const getCoreTeamsAndLogins = (pull_number, teams) => __awaiter(void 0, void 0, })); return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.union)(...teamsAndLogins); }); -const getRequiredCodeOwners = (pull_number) => __awaiter(void 0, void 0, void 0, function* () { - var _a; - const codeOwners = (_a = (yield (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.loadOwners)(process.cwd()))) !== null && _a !== void 0 ? _a : []; - const changedFilePaths = yield (0,_get_changed_filepaths__WEBPACK_IMPORTED_MODULE_4__/* .getChangedFilepaths */ .s)(pull_number); - const matchingCodeOwners = changedFilePaths.map(filePath => (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.matchFile)(filePath, codeOwners)); - return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(matchingCodeOwners - .filter(Boolean) - .map(owner => owner.owners) +const getCodeOwnersFromEntries = (codeOwnersEntries) => { + return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(codeOwnersEntries + .map(entry => entry.owners) .flat() .filter(Boolean) - .map(owner => owner.substring(owner.indexOf('/') + 1))); -}); + .map(codeOwner => (0,_convert_to_team_slug__WEBPACK_IMPORTED_MODULE_7__/* .convertToTeamSlug */ .$)(codeOwner))); +}; /***/ }) diff --git a/dist/499.index.js.map b/dist/499.index.js.map index 263a4869a..fdadd9cdd 100644 --- a/dist/499.index.js.map +++ b/dist/499.index.js.map @@ -1 +1 @@ -{"version":3,"file":"499.index.js","mappings":";;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AAEA;AAIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACxCA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAyCA;;;;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;ACjCA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AAEA;AACA;AAEA;;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA","sources":["webpack://github-helpers/./src/helpers/approvals-satisfied.ts","webpack://github-helpers/./src/octokit.ts","webpack://github-helpers/./src/types/generated.ts","webpack://github-helpers/./src/utils/get-changed-filepaths.ts","webpack://github-helpers/./src/utils/get-core-member-logins.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { HelperInputs } from '../types/generated';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\nimport { getCoreTeamsAndLogins } from '../utils/get-core-member-logins';\nimport { groupBy, uniq } from 'lodash';\n\nexport class ApprovalsSatisfied extends HelperInputs {\n teams?: string;\n number_of_reviewers?: string;\n pull_number?: string;\n}\n\nexport const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pull_number }: ApprovalsSatisfied = {}) => {\n const prNumber = pull_number ? Number(pull_number) : context.issue.number;\n const { data: reviews } = await octokit.pulls.listReviews({ pull_number: prNumber, ...context.repo });\n const teamsAndLogins = await getCoreTeamsAndLogins(prNumber, teams?.split('\\n'));\n const approverLogins = reviews\n .filter(({ state }) => state === 'APPROVED')\n .map(({ user }) => user?.login)\n .filter(Boolean);\n const codeOwnerTeams = uniq(teamsAndLogins.map(({ team }) => team));\n\n return codeOwnerTeams.every(team => {\n const membersOfCodeOwnerTeam = groupBy(teamsAndLogins, 'team')[team];\n const numberOfApprovalsForTeam = membersOfCodeOwnerTeam.filter(({ login }) => approverLogins.includes(login)).length;\n return numberOfApprovalsForTeam >= Number(number_of_reviewers);\n });\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport class HelperInputs {\n helper?: string;\n github_token?: string;\n body?: string;\n project_name?: string;\n project_destination_column_name?: string;\n note?: string;\n project_origin_column_name?: string;\n sha?: string;\n context?: string;\n state?: string;\n description?: string;\n target_url?: string;\n environment?: string;\n environment_url?: string;\n label?: string;\n labels?: string;\n paths?: string;\n ignore_globs?: string;\n extensions?: string;\n override_filter_paths?: string;\n batches?: string;\n pattern?: string;\n teams?: string;\n login?: string;\n paths_no_filter?: string;\n slack_webhook_url?: string;\n number_of_assignees?: string;\n number_of_reviewers?: string;\n globs?: string;\n override_filter_globs?: string;\n title?: string;\n seconds?: string;\n pull_number?: string;\n base?: string;\n head?: string;\n days?: string;\n no_evict_upon_conflict?: string;\n skip_if_already_set?: string;\n delimiter?: string;\n team?: string;\n}\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { ChangedFilesList } from '../types/github';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const getChangedFilepaths = async (pull_number: number) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n return changedFiles.map(file => file.filename);\n};\n\nconst paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => {\n const response = await octokit.pulls.listFiles({\n pull_number,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1));\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { loadOwners, matchFile } from 'codeowners-utils';\nimport { uniq, union } from 'lodash';\nimport { context } from '@actions/github';\nimport { getChangedFilepaths } from './get-changed-filepaths';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => {\n const teamsAndLogins = await getCoreTeamsAndLogins(pull_number, teams);\n return uniq(teamsAndLogins.map(({ login }) => login));\n};\n\nexport const getCoreTeamsAndLogins = async (pull_number: number, teams?: string[]) => {\n const codeOwners = teams ?? (await getRequiredCodeOwners(pull_number));\n\n if (!codeOwners?.length) {\n core.setFailed('No code owners found. Please provide a \"teams\" input or set up a CODEOWNERS file in your repo.');\n throw new Error();\n }\n\n const teamsAndLogins = await map(codeOwners, async team =>\n octokit.teams\n .listMembersInOrg({\n org: context.repo.owner,\n team_slug: team,\n per_page: 100\n })\n .then(listMembersResponse => listMembersResponse.data.map(({ login }) => ({ team, login })))\n );\n return union(...teamsAndLogins);\n};\n\nconst getRequiredCodeOwners = async (pull_number: number) => {\n const codeOwners = (await loadOwners(process.cwd())) ?? [];\n const changedFilePaths = await getChangedFilepaths(pull_number);\n const matchingCodeOwners = changedFilePaths.map(filePath => matchFile(filePath, codeOwners));\n return uniq(\n matchingCodeOwners\n .filter(Boolean)\n .map(owner => owner.owners)\n .flat()\n .filter(Boolean)\n .map(owner => owner.substring(owner.indexOf('/') + 1))\n );\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"499.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AAGA;AAIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;AC3DA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAyCA;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;AAEA;;;;;;;;;;;;;;ACbA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;ACjCA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA","sources":["webpack://github-helpers/./src/helpers/approvals-satisfied.ts","webpack://github-helpers/./src/octokit.ts","webpack://github-helpers/./src/types/generated.ts","webpack://github-helpers/./src/utils/convert-to-team-slug.ts","webpack://github-helpers/./src/utils/get-changed-filepaths.ts","webpack://github-helpers/./src/utils/get-core-member-logins.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { HelperInputs } from '../types/generated';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\nimport { getRequiredCodeOwnersEntries } from '../utils/get-core-member-logins';\nimport { map } from 'bluebird';\nimport { convertToTeamSlug } from '../utils/convert-to-team-slug';\nimport { CodeOwnersEntry } from 'codeowners-utils';\n\nexport class ApprovalsSatisfied extends HelperInputs {\n teams?: string;\n number_of_reviewers?: string;\n pull_number?: string;\n}\n\nexport const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pull_number }: ApprovalsSatisfied = {}) => {\n const prNumber = pull_number ? Number(pull_number) : context.issue.number;\n const { data: reviews } = await octokit.pulls.listReviews({ pull_number: prNumber, ...context.repo });\n const approverLogins = reviews\n .filter(({ state }) => state === 'APPROVED')\n .map(({ user }) => user?.login)\n .filter(Boolean);\n const teamsList = teams?.split('\\n');\n const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : await getRequiredCodeOwnersEntries(prNumber);\n\n const codeOwnersEntrySatisfiesApprovals = async (entry: Pick) => {\n const teamsAndLoginsLists = await map(entry.owners, async team => {\n const { data } = await octokit.teams.listMembersInOrg({\n org: context.repo.owner,\n team_slug: convertToTeamSlug(team),\n per_page: 100\n });\n return data.map(({ login }) => ({ team, login }));\n });\n const codeOwnerLogins = teamsAndLoginsLists.flat().map(({ login }) => login);\n\n const numberOfCollectiveApprovalsAcrossTeams = approverLogins.filter(login => codeOwnerLogins.includes(login)).length;\n const numberOfApprovalsForSingleTeam = codeOwnerLogins.filter(login => approverLogins.includes(login)).length;\n const numberOfApprovals = entry.owners.length > 1 ? numberOfCollectiveApprovalsAcrossTeams : numberOfApprovalsForSingleTeam;\n\n return numberOfApprovals >= Number(number_of_reviewers);\n };\n\n const booleans = await Promise.all(requiredCodeOwnersEntries.map(codeOwnersEntrySatisfiesApprovals));\n return booleans.every(Boolean);\n};\n\nconst createArtificialCodeOwnersEntry = (teams: string[]) => teams.map(team => ({ owners: [team] }));\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport class HelperInputs {\n helper?: string;\n github_token?: string;\n body?: string;\n project_name?: string;\n project_destination_column_name?: string;\n note?: string;\n project_origin_column_name?: string;\n sha?: string;\n context?: string;\n state?: string;\n description?: string;\n target_url?: string;\n environment?: string;\n environment_url?: string;\n label?: string;\n labels?: string;\n paths?: string;\n ignore_globs?: string;\n extensions?: string;\n override_filter_paths?: string;\n batches?: string;\n pattern?: string;\n teams?: string;\n login?: string;\n paths_no_filter?: string;\n slack_webhook_url?: string;\n number_of_assignees?: string;\n number_of_reviewers?: string;\n globs?: string;\n override_filter_globs?: string;\n title?: string;\n seconds?: string;\n pull_number?: string;\n base?: string;\n head?: string;\n days?: string;\n no_evict_upon_conflict?: string;\n skip_if_already_set?: string;\n delimiter?: string;\n team?: string;\n}\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport const convertToTeamSlug = (codeOwner: string) => codeOwner.substring(codeOwner.indexOf('/') + 1);\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { ChangedFilesList } from '../types/github';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const getChangedFilepaths = async (pull_number: number) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n return changedFiles.map(file => file.filename);\n};\n\nconst paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => {\n const response = await octokit.pulls.listFiles({\n pull_number,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1));\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { CodeOwnersEntry, loadOwners, matchFile } from 'codeowners-utils';\nimport { uniq, union } from 'lodash';\nimport { context } from '@actions/github';\nimport { getChangedFilepaths } from './get-changed-filepaths';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\nimport { convertToTeamSlug } from './convert-to-team-slug';\n\nexport const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => {\n const codeOwners = teams ?? getCodeOwnersFromEntries(await getRequiredCodeOwnersEntries(pull_number));\n const teamsAndLogins = await getCoreTeamsAndLogins(codeOwners);\n return uniq(teamsAndLogins.map(({ login }) => login));\n};\n\nexport const getRequiredCodeOwnersEntries = async (pull_number: number): Promise => {\n const codeOwners = (await loadOwners(process.cwd())) ?? [];\n const changedFilePaths = await getChangedFilepaths(pull_number);\n return changedFilePaths.map(filePath => matchFile(filePath, codeOwners)).filter(Boolean);\n};\n\nconst getCoreTeamsAndLogins = async (codeOwners?: string[]) => {\n if (!codeOwners?.length) {\n core.setFailed('No code owners found. Please provide a \"teams\" input or set up a CODEOWNERS file in your repo.');\n throw new Error();\n }\n\n const teamsAndLogins = await map(codeOwners, async team =>\n octokit.teams\n .listMembersInOrg({\n org: context.repo.owner,\n team_slug: team,\n per_page: 100\n })\n .then(listMembersResponse => listMembersResponse.data.map(({ login }) => ({ team, login })))\n );\n return union(...teamsAndLogins);\n};\n\nconst getCodeOwnersFromEntries = (codeOwnersEntries: CodeOwnersEntry[]) => {\n return uniq(\n codeOwnersEntries\n .map(entry => entry.owners)\n .flat()\n .filter(Boolean)\n .map(codeOwner => convertToTeamSlug(codeOwner))\n );\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/dist/905.index.js b/dist/905.index.js index d679711f1..83aa99578 100644 --- a/dist/905.index.js +++ b/dist/905.index.js @@ -181,6 +181,29 @@ class HelperInputs { } +/***/ }), + +/***/ 489: +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "$": () => (/* binding */ convertToTeamSlug) +/* harmony export */ }); +/* +Copyright 2021 Expedia, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +const convertToTeamSlug = (codeOwner) => codeOwner.substring(codeOwner.indexOf('/') + 1); + + /***/ }), /***/ 9180: @@ -234,8 +257,8 @@ const paginateAllChangedFilepaths = (pull_number, page = 1) => __awaiter(void 0, /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "F": () => (/* binding */ getCoreTeamsAndLogins), -/* harmony export */ "c": () => (/* binding */ getCoreMemberLogins) +/* harmony export */ "c": () => (/* binding */ getCoreMemberLogins), +/* harmony export */ "q": () => (/* binding */ getRequiredCodeOwnersEntries) /* harmony export */ }); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2186); /* harmony import */ var _actions_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_actions_core__WEBPACK_IMPORTED_MODULE_0__); @@ -249,6 +272,7 @@ const paginateAllChangedFilepaths = (pull_number, page = 1) => __awaiter(void 0, /* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(8710); /* harmony import */ var bluebird__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(bluebird__WEBPACK_IMPORTED_MODULE_5__); /* harmony import */ var _octokit__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(6161); +/* harmony import */ var _convert_to_team_slug__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(489); /* Copyright 2021 Expedia, Inc. Licensed under the Apache License, Version 2.0 (the "License"); @@ -277,12 +301,19 @@ var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _argume + const getCoreMemberLogins = (pull_number, teams) => __awaiter(void 0, void 0, void 0, function* () { - const teamsAndLogins = yield getCoreTeamsAndLogins(pull_number, teams); + const codeOwners = teams !== null && teams !== void 0 ? teams : getCodeOwnersFromEntries(yield getRequiredCodeOwnersEntries(pull_number)); + const teamsAndLogins = yield getCoreTeamsAndLogins(codeOwners); return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(teamsAndLogins.map(({ login }) => login)); }); -const getCoreTeamsAndLogins = (pull_number, teams) => __awaiter(void 0, void 0, void 0, function* () { - const codeOwners = teams !== null && teams !== void 0 ? teams : (yield getRequiredCodeOwners(pull_number)); +const getRequiredCodeOwnersEntries = (pull_number) => __awaiter(void 0, void 0, void 0, function* () { + var _a; + const codeOwners = (_a = (yield (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.loadOwners)(process.cwd()))) !== null && _a !== void 0 ? _a : []; + const changedFilePaths = yield (0,_get_changed_filepaths__WEBPACK_IMPORTED_MODULE_4__/* .getChangedFilepaths */ .s)(pull_number); + return changedFilePaths.map(filePath => (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.matchFile)(filePath, codeOwners)).filter(Boolean); +}); +const getCoreTeamsAndLogins = (codeOwners) => __awaiter(void 0, void 0, void 0, function* () { if (!(codeOwners === null || codeOwners === void 0 ? void 0 : codeOwners.length)) { _actions_core__WEBPACK_IMPORTED_MODULE_0__.setFailed('No code owners found. Please provide a "teams" input or set up a CODEOWNERS file in your repo.'); throw new Error(); @@ -297,18 +328,13 @@ const getCoreTeamsAndLogins = (pull_number, teams) => __awaiter(void 0, void 0, })); return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.union)(...teamsAndLogins); }); -const getRequiredCodeOwners = (pull_number) => __awaiter(void 0, void 0, void 0, function* () { - var _a; - const codeOwners = (_a = (yield (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.loadOwners)(process.cwd()))) !== null && _a !== void 0 ? _a : []; - const changedFilePaths = yield (0,_get_changed_filepaths__WEBPACK_IMPORTED_MODULE_4__/* .getChangedFilepaths */ .s)(pull_number); - const matchingCodeOwners = changedFilePaths.map(filePath => (0,codeowners_utils__WEBPACK_IMPORTED_MODULE_1__.matchFile)(filePath, codeOwners)); - return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(matchingCodeOwners - .filter(Boolean) - .map(owner => owner.owners) +const getCodeOwnersFromEntries = (codeOwnersEntries) => { + return (0,lodash__WEBPACK_IMPORTED_MODULE_2__.uniq)(codeOwnersEntries + .map(entry => entry.owners) .flat() .filter(Boolean) - .map(owner => owner.substring(owner.indexOf('/') + 1))); -}); + .map(codeOwner => (0,_convert_to_team_slug__WEBPACK_IMPORTED_MODULE_7__/* .convertToTeamSlug */ .$)(codeOwner))); +}; /***/ }) diff --git a/dist/905.index.js.map b/dist/905.index.js.map index 8776cbaad..aba8a8c98 100644 --- a/dist/905.index.js.map +++ b/dist/905.index.js.map @@ -1 +1 @@ -{"version":3,"file":"905.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;AAWA;;;;;;;;;;;;;;;;;;;AC7CA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AAEA;AAAA;;AACA;AAEA;AAAA;AAEA;AACA;AACA;AACA;AAKA;;;;;;;;;;;;;;;;;;AChCA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAyCA;;;;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;ACjCA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AAEA;AACA;AAEA;;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA","sources":["webpack://github-helpers/./src/constants.ts","webpack://github-helpers/./src/helpers/add-pr-approval-label.ts","webpack://github-helpers/./src/octokit.ts","webpack://github-helpers/./src/types/generated.ts","webpack://github-helpers/./src/utils/get-changed-filepaths.ts","webpack://github-helpers/./src/utils/get-core-member-logins.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// These extra headers are for experimental API features on Github Enterprise. See https://docs.github.com/en/enterprise-server@3.0/rest/overview/api-previews for details.\nconst PREVIEWS = ['ant-man', 'flash', 'groot', 'inertia', 'starfox'];\nexport const GITHUB_OPTIONS = {\n headers: {\n accept: PREVIEWS.map(preview => `application/vnd.github.${preview}-preview+json`).join()\n }\n};\n\nexport const DEFAULT_EXEMPT_DESCRIPTION = 'Passed in case the check is exempt.';\nexport const DEFAULT_PIPELINE_STATUS = 'Pipeline Status';\nexport const DEFAULT_PIPELINE_DESCRIPTION = 'Pipeline clear.';\nexport const PRODUCTION_ENVIRONMENT = 'production';\nexport const LATE_REVIEW = 'Late Review';\nexport const CORE_APPROVED_PR_LABEL = 'CORE APPROVED';\nexport const PEER_APPROVED_PR_LABEL = 'PEER APPROVED';\nexport const READY_FOR_MERGE_PR_LABEL = 'READY FOR MERGE';\nexport const MERGE_QUEUE_STATUS = 'QUEUE CHECKER';\nexport const QUEUED_FOR_MERGE_PREFIX = 'QUEUED FOR MERGE';\nexport const FIRST_QUEUED_PR_LABEL = `${QUEUED_FOR_MERGE_PREFIX} #1`;\nexport const JUMP_THE_QUEUE_PR_LABEL = 'JUMP THE QUEUE';\nexport const DEFAULT_PR_TITLE_REGEX = '^(build|ci|chore|docs|feat|fix|perf|refactor|style|test|revert|Revert|BREAKING CHANGE)((.*))?: .+$';\nexport const COPYRIGHT_HEADER = `/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/`;\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { CORE_APPROVED_PR_LABEL, PEER_APPROVED_PR_LABEL } from '../constants';\nimport { HelperInputs } from '../types/generated';\nimport { context } from '@actions/github';\nimport { getCoreMemberLogins } from '../utils/get-core-member-logins';\nimport { octokit } from '../octokit';\n\nexport class AddPrApprovalLabel extends HelperInputs {\n login = '';\n teams?: string;\n}\n\nexport const addPrApprovalLabel = async ({ teams, login }: AddPrApprovalLabel) => {\n const coreMemberLogins = await getCoreMemberLogins(context.issue.number, teams?.split('\\n'));\n const approvalLabel = coreMemberLogins.includes(login) ? CORE_APPROVED_PR_LABEL : PEER_APPROVED_PR_LABEL;\n return octokit.issues.addLabels({\n labels: [approvalLabel],\n issue_number: context.issue.number,\n ...context.repo\n });\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport class HelperInputs {\n helper?: string;\n github_token?: string;\n body?: string;\n project_name?: string;\n project_destination_column_name?: string;\n note?: string;\n project_origin_column_name?: string;\n sha?: string;\n context?: string;\n state?: string;\n description?: string;\n target_url?: string;\n environment?: string;\n environment_url?: string;\n label?: string;\n labels?: string;\n paths?: string;\n ignore_globs?: string;\n extensions?: string;\n override_filter_paths?: string;\n batches?: string;\n pattern?: string;\n teams?: string;\n login?: string;\n paths_no_filter?: string;\n slack_webhook_url?: string;\n number_of_assignees?: string;\n number_of_reviewers?: string;\n globs?: string;\n override_filter_globs?: string;\n title?: string;\n seconds?: string;\n pull_number?: string;\n base?: string;\n head?: string;\n days?: string;\n no_evict_upon_conflict?: string;\n skip_if_already_set?: string;\n delimiter?: string;\n team?: string;\n}\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { ChangedFilesList } from '../types/github';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const getChangedFilepaths = async (pull_number: number) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n return changedFiles.map(file => file.filename);\n};\n\nconst paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => {\n const response = await octokit.pulls.listFiles({\n pull_number,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1));\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { loadOwners, matchFile } from 'codeowners-utils';\nimport { uniq, union } from 'lodash';\nimport { context } from '@actions/github';\nimport { getChangedFilepaths } from './get-changed-filepaths';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => {\n const teamsAndLogins = await getCoreTeamsAndLogins(pull_number, teams);\n return uniq(teamsAndLogins.map(({ login }) => login));\n};\n\nexport const getCoreTeamsAndLogins = async (pull_number: number, teams?: string[]) => {\n const codeOwners = teams ?? (await getRequiredCodeOwners(pull_number));\n\n if (!codeOwners?.length) {\n core.setFailed('No code owners found. Please provide a \"teams\" input or set up a CODEOWNERS file in your repo.');\n throw new Error();\n }\n\n const teamsAndLogins = await map(codeOwners, async team =>\n octokit.teams\n .listMembersInOrg({\n org: context.repo.owner,\n team_slug: team,\n per_page: 100\n })\n .then(listMembersResponse => listMembersResponse.data.map(({ login }) => ({ team, login })))\n );\n return union(...teamsAndLogins);\n};\n\nconst getRequiredCodeOwners = async (pull_number: number) => {\n const codeOwners = (await loadOwners(process.cwd())) ?? [];\n const changedFilePaths = await getChangedFilepaths(pull_number);\n const matchingCodeOwners = changedFilePaths.map(filePath => matchFile(filePath, codeOwners));\n return uniq(\n matchingCodeOwners\n .filter(Boolean)\n .map(owner => owner.owners)\n .flat()\n .filter(Boolean)\n .map(owner => owner.substring(owner.indexOf('/') + 1))\n );\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"905.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;AAWA;;;;;;;;;;;;;;;;;;;AC7CA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AAEA;AAAA;;AACA;AAEA;AAAA;AAEA;AACA;AACA;AACA;AAKA;;;;;;;;;;;;;;;;;;AChCA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAyCA;;;;;;;;;;;ACtDA;;;;;;;;;;;AAWA;AAEA;;;;;;;;;;;;;;ACbA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;ACjCA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AAEA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA","sources":["webpack://github-helpers/./src/constants.ts","webpack://github-helpers/./src/helpers/add-pr-approval-label.ts","webpack://github-helpers/./src/octokit.ts","webpack://github-helpers/./src/types/generated.ts","webpack://github-helpers/./src/utils/convert-to-team-slug.ts","webpack://github-helpers/./src/utils/get-changed-filepaths.ts","webpack://github-helpers/./src/utils/get-core-member-logins.ts"],"sourcesContent":["/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\n// These extra headers are for experimental API features on Github Enterprise. See https://docs.github.com/en/enterprise-server@3.0/rest/overview/api-previews for details.\nconst PREVIEWS = ['ant-man', 'flash', 'groot', 'inertia', 'starfox'];\nexport const GITHUB_OPTIONS = {\n headers: {\n accept: PREVIEWS.map(preview => `application/vnd.github.${preview}-preview+json`).join()\n }\n};\n\nexport const DEFAULT_EXEMPT_DESCRIPTION = 'Passed in case the check is exempt.';\nexport const DEFAULT_PIPELINE_STATUS = 'Pipeline Status';\nexport const DEFAULT_PIPELINE_DESCRIPTION = 'Pipeline clear.';\nexport const PRODUCTION_ENVIRONMENT = 'production';\nexport const LATE_REVIEW = 'Late Review';\nexport const CORE_APPROVED_PR_LABEL = 'CORE APPROVED';\nexport const PEER_APPROVED_PR_LABEL = 'PEER APPROVED';\nexport const READY_FOR_MERGE_PR_LABEL = 'READY FOR MERGE';\nexport const MERGE_QUEUE_STATUS = 'QUEUE CHECKER';\nexport const QUEUED_FOR_MERGE_PREFIX = 'QUEUED FOR MERGE';\nexport const FIRST_QUEUED_PR_LABEL = `${QUEUED_FOR_MERGE_PREFIX} #1`;\nexport const JUMP_THE_QUEUE_PR_LABEL = 'JUMP THE QUEUE';\nexport const DEFAULT_PR_TITLE_REGEX = '^(build|ci|chore|docs|feat|fix|perf|refactor|style|test|revert|Revert|BREAKING CHANGE)((.*))?: .+$';\nexport const COPYRIGHT_HEADER = `/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/`;\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { CORE_APPROVED_PR_LABEL, PEER_APPROVED_PR_LABEL } from '../constants';\nimport { HelperInputs } from '../types/generated';\nimport { context } from '@actions/github';\nimport { getCoreMemberLogins } from '../utils/get-core-member-logins';\nimport { octokit } from '../octokit';\n\nexport class AddPrApprovalLabel extends HelperInputs {\n login = '';\n teams?: string;\n}\n\nexport const addPrApprovalLabel = async ({ teams, login }: AddPrApprovalLabel) => {\n const coreMemberLogins = await getCoreMemberLogins(context.issue.number, teams?.split('\\n'));\n const approvalLabel = coreMemberLogins.includes(login) ? CORE_APPROVED_PR_LABEL : PEER_APPROVED_PR_LABEL;\n return octokit.issues.addLabels({\n labels: [approvalLabel],\n issue_number: context.issue.number,\n ...context.repo\n });\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport * as fetch from '@adobe/node-fetch-retry';\nimport { getOctokit } from '@actions/github';\n\nconst githubToken = core.getInput('github_token', { required: true });\nexport const { rest: octokit, graphql: octokitGraphql } = getOctokit(githubToken, { request: { fetch } });\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport class HelperInputs {\n helper?: string;\n github_token?: string;\n body?: string;\n project_name?: string;\n project_destination_column_name?: string;\n note?: string;\n project_origin_column_name?: string;\n sha?: string;\n context?: string;\n state?: string;\n description?: string;\n target_url?: string;\n environment?: string;\n environment_url?: string;\n label?: string;\n labels?: string;\n paths?: string;\n ignore_globs?: string;\n extensions?: string;\n override_filter_paths?: string;\n batches?: string;\n pattern?: string;\n teams?: string;\n login?: string;\n paths_no_filter?: string;\n slack_webhook_url?: string;\n number_of_assignees?: string;\n number_of_reviewers?: string;\n globs?: string;\n override_filter_globs?: string;\n title?: string;\n seconds?: string;\n pull_number?: string;\n base?: string;\n head?: string;\n days?: string;\n no_evict_upon_conflict?: string;\n skip_if_already_set?: string;\n delimiter?: string;\n team?: string;\n}\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nexport const convertToTeamSlug = (codeOwner: string) => codeOwner.substring(codeOwner.indexOf('/') + 1);\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport { ChangedFilesList } from '../types/github';\nimport { context } from '@actions/github';\nimport { octokit } from '../octokit';\n\nexport const getChangedFilepaths = async (pull_number: number) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n return changedFiles.map(file => file.filename);\n};\n\nconst paginateAllChangedFilepaths = async (pull_number: number, page = 1): Promise => {\n const response = await octokit.pulls.listFiles({\n pull_number,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllChangedFilepaths(pull_number, page + 1));\n};\n","/*\nCopyright 2021 Expedia, Inc.\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n https://www.apache.org/licenses/LICENSE-2.0\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n*/\n\nimport * as core from '@actions/core';\nimport { CodeOwnersEntry, loadOwners, matchFile } from 'codeowners-utils';\nimport { uniq, union } from 'lodash';\nimport { context } from '@actions/github';\nimport { getChangedFilepaths } from './get-changed-filepaths';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\nimport { convertToTeamSlug } from './convert-to-team-slug';\n\nexport const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => {\n const codeOwners = teams ?? getCodeOwnersFromEntries(await getRequiredCodeOwnersEntries(pull_number));\n const teamsAndLogins = await getCoreTeamsAndLogins(codeOwners);\n return uniq(teamsAndLogins.map(({ login }) => login));\n};\n\nexport const getRequiredCodeOwnersEntries = async (pull_number: number): Promise => {\n const codeOwners = (await loadOwners(process.cwd())) ?? [];\n const changedFilePaths = await getChangedFilepaths(pull_number);\n return changedFilePaths.map(filePath => matchFile(filePath, codeOwners)).filter(Boolean);\n};\n\nconst getCoreTeamsAndLogins = async (codeOwners?: string[]) => {\n if (!codeOwners?.length) {\n core.setFailed('No code owners found. Please provide a \"teams\" input or set up a CODEOWNERS file in your repo.');\n throw new Error();\n }\n\n const teamsAndLogins = await map(codeOwners, async team =>\n octokit.teams\n .listMembersInOrg({\n org: context.repo.owner,\n team_slug: team,\n per_page: 100\n })\n .then(listMembersResponse => listMembersResponse.data.map(({ login }) => ({ team, login })))\n );\n return union(...teamsAndLogins);\n};\n\nconst getCodeOwnersFromEntries = (codeOwnersEntries: CodeOwnersEntry[]) => {\n return uniq(\n codeOwnersEntries\n .map(entry => entry.owners)\n .flat()\n .filter(Boolean)\n .map(codeOwner => convertToTeamSlug(codeOwner))\n );\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/src/helpers/approvals-satisfied.ts b/src/helpers/approvals-satisfied.ts index 1eccfc093..c6ef6d6af 100644 --- a/src/helpers/approvals-satisfied.ts +++ b/src/helpers/approvals-satisfied.ts @@ -14,8 +14,10 @@ limitations under the License. import { HelperInputs } from '../types/generated'; import { context } from '@actions/github'; import { octokit } from '../octokit'; -import { getCoreTeamsAndLogins } from '../utils/get-core-member-logins'; -import { groupBy, uniq } from 'lodash'; +import { getRequiredCodeOwnersEntries } from '../utils/get-core-member-logins'; +import { map } from 'bluebird'; +import { convertToTeamSlug } from '../utils/convert-to-team-slug'; +import { CodeOwnersEntry } from 'codeowners-utils'; export class ApprovalsSatisfied extends HelperInputs { teams?: string; @@ -26,16 +28,33 @@ export class ApprovalsSatisfied extends HelperInputs { export const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pull_number }: ApprovalsSatisfied = {}) => { const prNumber = pull_number ? Number(pull_number) : context.issue.number; const { data: reviews } = await octokit.pulls.listReviews({ pull_number: prNumber, ...context.repo }); - const teamsAndLogins = await getCoreTeamsAndLogins(prNumber, teams?.split('\n')); const approverLogins = reviews .filter(({ state }) => state === 'APPROVED') .map(({ user }) => user?.login) .filter(Boolean); - const codeOwnerTeams = uniq(teamsAndLogins.map(({ team }) => team)); + const teamsList = teams?.split('\n'); + const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : await getRequiredCodeOwnersEntries(prNumber); - return codeOwnerTeams.every(team => { - const membersOfCodeOwnerTeam = groupBy(teamsAndLogins, 'team')[team]; - const numberOfApprovalsForTeam = membersOfCodeOwnerTeam.filter(({ login }) => approverLogins.includes(login)).length; - return numberOfApprovalsForTeam >= Number(number_of_reviewers); - }); + const codeOwnersEntrySatisfiesApprovals = async (entry: Pick) => { + const teamsAndLoginsLists = await map(entry.owners, async team => { + const { data } = await octokit.teams.listMembersInOrg({ + org: context.repo.owner, + team_slug: convertToTeamSlug(team), + per_page: 100 + }); + return data.map(({ login }) => ({ team, login })); + }); + const codeOwnerLogins = teamsAndLoginsLists.flat().map(({ login }) => login); + + const numberOfCollectiveApprovalsAcrossTeams = approverLogins.filter(login => codeOwnerLogins.includes(login)).length; + const numberOfApprovalsForSingleTeam = codeOwnerLogins.filter(login => approverLogins.includes(login)).length; + const numberOfApprovals = entry.owners.length > 1 ? numberOfCollectiveApprovalsAcrossTeams : numberOfApprovalsForSingleTeam; + + return numberOfApprovals >= Number(number_of_reviewers); + }; + + const booleans = await Promise.all(requiredCodeOwnersEntries.map(codeOwnersEntrySatisfiesApprovals)); + return booleans.every(Boolean); }; + +const createArtificialCodeOwnersEntry = (teams: string[]) => teams.map(team => ({ owners: [team] })); diff --git a/src/utils/convert-to-team-slug.ts b/src/utils/convert-to-team-slug.ts new file mode 100644 index 000000000..a023f7d85 --- /dev/null +++ b/src/utils/convert-to-team-slug.ts @@ -0,0 +1,14 @@ +/* +Copyright 2021 Expedia, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + https://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export const convertToTeamSlug = (codeOwner: string) => codeOwner.substring(codeOwner.indexOf('/') + 1); diff --git a/src/utils/get-core-member-logins.ts b/src/utils/get-core-member-logins.ts index d446b6d76..d82a39ff9 100644 --- a/src/utils/get-core-member-logins.ts +++ b/src/utils/get-core-member-logins.ts @@ -12,21 +12,27 @@ limitations under the License. */ import * as core from '@actions/core'; -import { loadOwners, matchFile } from 'codeowners-utils'; +import { CodeOwnersEntry, loadOwners, matchFile } from 'codeowners-utils'; import { uniq, union } from 'lodash'; import { context } from '@actions/github'; import { getChangedFilepaths } from './get-changed-filepaths'; import { map } from 'bluebird'; import { octokit } from '../octokit'; +import { convertToTeamSlug } from './convert-to-team-slug'; export const getCoreMemberLogins = async (pull_number: number, teams?: string[]) => { - const teamsAndLogins = await getCoreTeamsAndLogins(pull_number, teams); + const codeOwners = teams ?? getCodeOwnersFromEntries(await getRequiredCodeOwnersEntries(pull_number)); + const teamsAndLogins = await getCoreTeamsAndLogins(codeOwners); return uniq(teamsAndLogins.map(({ login }) => login)); }; -export const getCoreTeamsAndLogins = async (pull_number: number, teams?: string[]) => { - const codeOwners = teams ?? (await getRequiredCodeOwners(pull_number)); +export const getRequiredCodeOwnersEntries = async (pull_number: number): Promise => { + const codeOwners = (await loadOwners(process.cwd())) ?? []; + const changedFilePaths = await getChangedFilepaths(pull_number); + return changedFilePaths.map(filePath => matchFile(filePath, codeOwners)).filter(Boolean); +}; +const getCoreTeamsAndLogins = async (codeOwners?: string[]) => { if (!codeOwners?.length) { core.setFailed('No code owners found. Please provide a "teams" input or set up a CODEOWNERS file in your repo.'); throw new Error(); @@ -44,16 +50,12 @@ export const getCoreTeamsAndLogins = async (pull_number: number, teams?: string[ return union(...teamsAndLogins); }; -const getRequiredCodeOwners = async (pull_number: number) => { - const codeOwners = (await loadOwners(process.cwd())) ?? []; - const changedFilePaths = await getChangedFilepaths(pull_number); - const matchingCodeOwners = changedFilePaths.map(filePath => matchFile(filePath, codeOwners)); +const getCodeOwnersFromEntries = (codeOwnersEntries: CodeOwnersEntry[]) => { return uniq( - matchingCodeOwners - .filter(Boolean) - .map(owner => owner.owners) + codeOwnersEntries + .map(entry => entry.owners) .flat() .filter(Boolean) - .map(owner => owner.substring(owner.indexOf('/') + 1)) + .map(codeOwner => convertToTeamSlug(codeOwner)) ); }; diff --git a/test/helpers/approvals-satisfied.test.ts b/test/helpers/approvals-satisfied.test.ts index 1f9d19fca..32a0724ea 100644 --- a/test/helpers/approvals-satisfied.test.ts +++ b/test/helpers/approvals-satisfied.test.ts @@ -14,8 +14,16 @@ limitations under the License. import { Mocktokit } from '../types'; import { approvalsSatisfied } from '../../src/helpers/approvals-satisfied'; import { octokit } from '../../src/octokit'; -import { getCoreTeamsAndLogins } from '../../src/utils/get-core-member-logins'; +import { getRequiredCodeOwnersEntries } from '../../src/utils/get-core-member-logins'; +const ownerMap: { [key: string]: Object } = { + team1: { data: [{ login: 'user1' }] }, + team2: { data: [{ login: 'user2' }, { login: 'user3' }] }, + team3: { data: [{ login: 'user1' }] }, + team4: { data: [{ login: 'user4' }, { login: 'user5' }] }, + team5: { data: [{ login: 'user4' }, { login: 'user6' }, { login: 'user7' }] }, + team6: { data: [{ login: 'user8' }, { login: 'user9' }] } +}; jest.mock('@actions/core'); jest.mock('@actions/github', () => ({ context: { repo: { repo: 'repo', owner: 'owner' }, issue: { number: 123 } }, @@ -23,6 +31,9 @@ jest.mock('@actions/github', () => ({ rest: { pulls: { listReviews: jest.fn() + }, + teams: { + listMembersInOrg: jest.fn(async input => ownerMap[input.team_slug]) } } })) @@ -30,13 +41,7 @@ jest.mock('@actions/github', () => ({ jest.mock('../../src/utils/get-core-member-logins'); describe('approvalsSatisfied', () => { - it('should return false when a core member has not approved', async () => { - (getCoreTeamsAndLogins as jest.Mock).mockResolvedValue([ - { - team: 'team1', - login: 'user1' - } - ]); + it('should return false when passing teams override and required approvals are not met', async () => { (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ data: [ { @@ -47,17 +52,43 @@ describe('approvalsSatisfied', () => { })); const result = await approvalsSatisfied({ teams: 'team1', pull_number: '12345' }); expect(octokit.pulls.listReviews).toHaveBeenCalledWith({ pull_number: 12345, repo: 'repo', owner: 'owner' }); - expect(getCoreTeamsAndLogins).toHaveBeenCalledWith(12345, ['team1']); + expect(getRequiredCodeOwnersEntries).not.toHaveBeenCalled(); + expect(result).toBe(false); + }); + + it('should return true when passing teams override and required approvals are met', async () => { + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ + { + state: 'APPROVED', + user: { login: 'user1' } + } + ] + })); + const result = await approvalsSatisfied({ teams: 'team1', pull_number: '12345' }); + expect(octokit.pulls.listReviews).toHaveBeenCalledWith({ pull_number: 12345, repo: 'repo', owner: 'owner' }); + expect(getRequiredCodeOwnersEntries).not.toHaveBeenCalled(); + expect(result).toBe(true); + }); + + it('should return false when a core member has not approved', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([{ owners: ['@ExpediaGroup/team1'] }]); + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ + { + state: 'APPROVED', + user: { login: 'user3' } + } + ] + })); + const result = await approvalsSatisfied({ pull_number: '12345' }); + expect(octokit.pulls.listReviews).toHaveBeenCalledWith({ pull_number: 12345, repo: 'repo', owner: 'owner' }); + expect(getRequiredCodeOwnersEntries).toHaveBeenCalledWith(12345); expect(result).toBe(false); }); it('should return true when a core member has approved', async () => { - (getCoreTeamsAndLogins as jest.Mock).mockResolvedValue([ - { - team: 'team1', - login: 'user1' - } - ]); + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([{ owners: ['@ExpediaGroup/team1'] }]); (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ data: [ { @@ -75,23 +106,10 @@ describe('approvalsSatisfied', () => { }); it('should return false when not all core teams have approved', async () => { - (getCoreTeamsAndLogins as jest.Mock).mockResolvedValue([ - { - team: 'team1', - login: 'user1' - }, - { - team: 'team2', - login: 'user2' - }, - { - team: 'team2', - login: 'user3' - }, - { - team: 'team3', - login: 'user1' - } + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: ['@ExpediaGroup/team1'] }, + { owners: ['@ExpediaGroup/team2'] }, + { owners: ['@ExpediaGroup/team3'] } ]); (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ data: [ @@ -110,23 +128,10 @@ describe('approvalsSatisfied', () => { }); it('should return true when a member from each core team has approved', async () => { - (getCoreTeamsAndLogins as jest.Mock).mockResolvedValue([ - { - team: 'team1', - login: 'user1' - }, - { - team: 'team2', - login: 'user2' - }, - { - team: 'team2', - login: 'user3' - }, - { - team: 'team3', - login: 'user1' - } + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: ['@ExpediaGroup/team1'] }, + { owners: ['@ExpediaGroup/team2'] }, + { owners: ['@ExpediaGroup/team3'] } ]); (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ data: [ @@ -149,33 +154,15 @@ describe('approvalsSatisfied', () => { }); it('should return false when not enough members from core teams have approved', async () => { - (getCoreTeamsAndLogins as jest.Mock).mockResolvedValue([ - { - team: 'team1', - login: 'user1' - }, - { - team: 'team1', - login: 'user2' - }, - { - team: 'team2', - login: 'user3' - }, - { - team: 'team2', - login: 'user4' - }, - { - team: 'team3', - login: 'user5' - } + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: ['@ExpediaGroup/team2'] }, + { owners: ['@ExpediaGroup/team4'] } ]); (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ data: [ { state: 'APPROVED', - user: { login: 'user1' } + user: { login: 'user2' } }, { state: 'APPROVED', @@ -192,45 +179,119 @@ describe('approvalsSatisfied', () => { }); it('should return true when enough members from core teams have approved', async () => { - (getCoreTeamsAndLogins as jest.Mock).mockResolvedValue([ - { - team: 'team1', - login: 'user1' - }, - { - team: 'team1', - login: 'user2' - }, - { - team: 'team2', - login: 'user3' - }, - { - team: 'team2', - login: 'user4' - }, - { - team: 'team2', - login: 'user5' - } + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: ['@ExpediaGroup/team2'] }, + { owners: ['@ExpediaGroup/team4'] } ]); (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ data: [ { state: 'APPROVED', - user: { login: 'user1' } + user: { login: 'user2' } }, { state: 'APPROVED', - user: { login: 'user2' } + user: { login: 'user3' } }, { state: 'APPROVED', - user: { login: 'user3' } + user: { login: 'user4' } + }, + { + state: 'APPROVED', + user: { login: 'user5' } + } + ] + })); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(true); + }); + + it('should return false when not enough collective approvals from shared owners are met', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([{ owners: ['@ExpediaGroup/team4', '@ExpediaGroup/team5'] }]); + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ + { + state: 'APPROVED', + user: { login: 'user4' } + } + ] + })); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(false); + }); + + it('should return false when not enough collective approvals from shared owners are met even if user is in both groups', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([{ owners: ['@ExpediaGroup/team4', '@ExpediaGroup/team5'] }]); + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ + { + state: 'APPROVED', + user: { login: 'user5' } + } + ] + })); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(false); + }); + + it('should return true when enough collective approvals from shared owners are met', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([{ owners: ['@ExpediaGroup/team4', '@ExpediaGroup/team5'] }]); + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ + { + state: 'APPROVED', + user: { login: 'user4' } }, + { + state: 'APPROVED', + user: { login: 'user6' } + } + ] + })); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(true); + }); + + it('should return false when collective approvals are met but not standalone approvals', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: ['@ExpediaGroup/team4'] }, + { owners: ['@ExpediaGroup/team5', '@ExpediaGroup/team6'] } + ]); + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ + { + state: 'APPROVED', + user: { login: 'user4' } + }, + { + state: 'APPROVED', + user: { login: 'user8' } + } + ] + })); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(false); + }); + + it('should return true when both collective and standalone approvals are met', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: ['@ExpediaGroup/team4'] }, + { owners: ['@ExpediaGroup/team5', '@ExpediaGroup/team6'] } + ]); + (octokit.pulls.listReviews as unknown as Mocktokit).mockImplementation(async () => ({ + data: [ { state: 'APPROVED', user: { login: 'user4' } + }, + { + state: 'APPROVED', + user: { login: 'user5' } + }, + { + state: 'APPROVED', + user: { login: 'user8' } } ] })); diff --git a/test/utils/get-core-member-logins.test.ts b/test/utils/get-core-member-logins.test.ts index 9fcae9df0..56b58815f 100644 --- a/test/utils/get-core-member-logins.test.ts +++ b/test/utils/get-core-member-logins.test.ts @@ -12,13 +12,15 @@ limitations under the License. */ import { Mocktokit } from '../types'; -import { getCoreMemberLogins } from '../../src/utils/get-core-member-logins'; +import { getCoreMemberLogins, getRequiredCodeOwnersEntries } from '../../src/utils/get-core-member-logins'; import { octokit } from '../../src/octokit'; jest.mock('@actions/core'); const ownerMap: { [key: string]: Object } = { 'test-owners-1': { data: [{ login: 'user1' }, { login: 'user2' }] }, 'test-owners-2': { data: [{ login: 'user2' }, { login: 'user3' }] }, + 'test-shared-owners-1': { data: [{ login: 'user4' }, { login: 'user5' }] }, + 'test-shared-owners-2': { data: [{ login: 'user5' }, { login: 'user6' }] }, 'github-helpers-committers': { data: [{ login: 'user4' }] } }; jest.mock('@actions/github', () => ({ @@ -34,7 +36,8 @@ jest.mock('@actions/github', () => ({ })); const file1 = 'file/path/1/file1.txt'; const file2 = 'file/path/2/file2.ts'; -const file3 = 'something/totally/different/file1.txt'; +const sharedFile = 'file/path/shared/file.ts'; +const someTotallyDifferentFile = 'something/totally/different/file1.txt'; const pkg = 'package.json'; const pull_number = 123; @@ -78,7 +81,7 @@ describe('getCoreMemberLogins', () => { filename: file2 }, { - filename: file3 + filename: someTotallyDifferentFile }, { filename: pkg @@ -94,6 +97,39 @@ describe('getCoreMemberLogins', () => { expect(result).toEqual(['user1', 'user2', 'user3', 'user4']); }); }); + + describe('getRequiredCodeOwnersEntries', () => { + beforeEach(() => { + (octokit.pulls.listFiles as unknown as Mocktokit).mockImplementation(async ({ page }) => ({ + data: + page === 1 + ? [ + { + filename: file1 + }, + { + filename: sharedFile + } + ] + : [] + })); + }); + + it('should return expected result', async () => { + const result = await getRequiredCodeOwnersEntries(pull_number); + + expect(result).toEqual([ + { + pattern: '/file/path/1', + owners: ['@ExpediaGroup/test-owners-1'] + }, + { + pattern: '/file/path/shared', + owners: ['@ExpediaGroup/test-shared-owners-1', '@ExpediaGroup/test-shared-owners-2'] + } + ]); + }); + }); }); describe('specified teams case', () => { @@ -111,7 +147,7 @@ describe('getCoreMemberLogins', () => { filename: file2 }, { - filename: file3 + filename: someTotallyDifferentFile }, { filename: pkg