From 997dfbdd90eebf99da3c2df9b121a5f6b502f1a5 Mon Sep 17 00:00:00 2001 From: Dan Adajian Date: Thu, 18 Jan 2024 10:08:59 -0600 Subject: [PATCH] fix(approvals-satisfied): handle ownerless codeowner entries --- dist/431.index.js | 5 +-- dist/431.index.js.map | 2 +- src/helpers/approvals-satisfied.ts | 5 +-- test/helpers/approvals-satisfied.test.ts | 42 ++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 5 deletions(-) diff --git a/dist/431.index.js b/dist/431.index.js index 401589d35..9ba3de0cc 100644 --- a/dist/431.index.js +++ b/dist/431.index.js @@ -103,6 +103,7 @@ const approvalsSatisfied = ({ teams, number_of_reviewers = '1', pull_number } = core.debug(`PR already approved by: ${approverLogins.toString()}`); const teamsList = teams === null || teams === void 0 ? void 0 : teams.split('\n'); const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : yield (0,get_core_member_logins/* getRequiredCodeOwnersEntries */.q)(prNumber); + const requiredCodeOwnersEntriesWithOwners = requiredCodeOwnersEntries.filter(({ owners }) => owners.length); const codeOwnersEntrySatisfiesApprovals = (entry) => approvals_satisfied_awaiter(void 0, void 0, void 0, function* () { const teamsAndLoginsLists = yield (0,bluebird.map)(entry.owners, (team) => approvals_satisfied_awaiter(void 0, void 0, void 0, function* () { const { data } = yield octokit/* octokit.teams.listMembersInOrg */.K.teams.listMembersInOrg({ @@ -119,8 +120,8 @@ const approvalsSatisfied = ({ teams, number_of_reviewers = '1', pull_number } = core.debug(`Current number of approvals satisfied for ${entry.owners}: ${numberOfApprovals}`); return numberOfApprovals >= Number(number_of_reviewers); }); - core.debug(`Required code owners: ${requiredCodeOwnersEntries.map(({ owners }) => owners).toString()}`); - const booleans = yield Promise.all(requiredCodeOwnersEntries.map(codeOwnersEntrySatisfiesApprovals)); + core.debug(`Required code owners: ${requiredCodeOwnersEntriesWithOwners.map(({ owners }) => owners).toString()}`); + const booleans = yield Promise.all(requiredCodeOwnersEntriesWithOwners.map(codeOwnersEntrySatisfiesApprovals)); return booleans.every(Boolean); }); const createArtificialCodeOwnersEntry = (teams) => teams.map(team => ({ owners: [team] })); diff --git a/dist/431.index.js.map b/dist/431.index.js.map index 38fc60d55..be99a7b00 100644 --- a/dist/431.index.js.map +++ b/dist/431.index.js.map @@ -1 +1 @@ -{"version":3,"file":"431.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;AC5BA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AAIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;AClEA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AA2CA;;;;;;;;;;;ACxDA;;;;;;;;;;;AAWA;AAEA;;;;;;;;;;;;;;ACbA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;AClCA;;;;;;;;;;;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":[".././src/utils/paginate-all-reviews.ts",".././src/helpers/approvals-satisfied.ts",".././src/octokit.ts",".././src/types/generated.ts",".././src/utils/convert-to-team-slug.ts",".././src/utils/get-changed-filepaths.ts",".././src/utils/get-core-member-logins.ts"],"sourcesContent":["/*\nCopyright 2022 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 { PullRequestReviewList } from '../types/github';\nimport { octokit } from '../octokit';\nimport { context } from '@actions/github';\n\nexport const paginateAllReviews = async (prNumber: number, page = 1): Promise => {\n const response = await octokit.pulls.listReviews({\n pull_number: prNumber,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllReviews(prNumber, 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 { 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';\nimport * as core from '@actions/core';\nimport { paginateAllReviews } from '../utils/paginate-all-reviews';\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 reviews = await paginateAllReviews(prNumber);\n const approverLogins = reviews\n .filter(({ state }) => state === 'APPROVED')\n .map(({ user }) => user?.login)\n .filter(Boolean);\n core.debug(`PR already approved by: ${approverLogins.toString()}`);\n\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 core.debug(`Current number of approvals satisfied for ${entry.owners}: ${numberOfApprovals}`);\n\n return numberOfApprovals >= Number(number_of_reviewers);\n };\n\n core.debug(`Required code owners: ${requiredCodeOwnersEntries.map(({ owners }) => owners).toString()}`);\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 ignore_deleted?: string;\n return_full_payload?: 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, ignore_deleted?: boolean) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n const filesToMap = ignore_deleted ? changedFiles.filter(file => file.status !== 'removed') : changedFiles;\n return filesToMap.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 +{"version":3,"file":"431.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;AC5BA;;;;;;;;;;;AAWA;;;;;;;;;;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AAIA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAEA;AACA;AAEA;AACA;AACA;AACA;AAEA;;;;;;;;;;;;;;;;;;ACnEA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AA2CA;;;;;;;;;;;ACxDA;;;;;;;;;;;AAWA;AAEA;;;;;;;;;;;;;;ACbA;;;;;;;;;;;AAWA;;;;;;;;;;AAGA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AAMA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;;;AClCA;;;;;;;;;;;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":[".././src/utils/paginate-all-reviews.ts",".././src/helpers/approvals-satisfied.ts",".././src/octokit.ts",".././src/types/generated.ts",".././src/utils/convert-to-team-slug.ts",".././src/utils/get-changed-filepaths.ts",".././src/utils/get-core-member-logins.ts"],"sourcesContent":["/*\nCopyright 2022 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 { PullRequestReviewList } from '../types/github';\nimport { octokit } from '../octokit';\nimport { context } from '@actions/github';\n\nexport const paginateAllReviews = async (prNumber: number, page = 1): Promise => {\n const response = await octokit.pulls.listReviews({\n pull_number: prNumber,\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllReviews(prNumber, 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 { 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';\nimport * as core from '@actions/core';\nimport { paginateAllReviews } from '../utils/paginate-all-reviews';\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 reviews = await paginateAllReviews(prNumber);\n const approverLogins = reviews\n .filter(({ state }) => state === 'APPROVED')\n .map(({ user }) => user?.login)\n .filter(Boolean);\n core.debug(`PR already approved by: ${approverLogins.toString()}`);\n\n const teamsList = teams?.split('\\n');\n const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : await getRequiredCodeOwnersEntries(prNumber);\n const requiredCodeOwnersEntriesWithOwners = requiredCodeOwnersEntries.filter(({ owners }) => owners.length);\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 core.debug(`Current number of approvals satisfied for ${entry.owners}: ${numberOfApprovals}`);\n\n return numberOfApprovals >= Number(number_of_reviewers);\n };\n\n core.debug(`Required code owners: ${requiredCodeOwnersEntriesWithOwners.map(({ owners }) => owners).toString()}`);\n const booleans = await Promise.all(requiredCodeOwnersEntriesWithOwners.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 ignore_deleted?: string;\n return_full_payload?: 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, ignore_deleted?: boolean) => {\n const changedFiles = await paginateAllChangedFilepaths(pull_number);\n const filesToMap = ignore_deleted ? changedFiles.filter(file => file.status !== 'removed') : changedFiles;\n return filesToMap.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 d9661c539..c8cc8f0e0 100644 --- a/src/helpers/approvals-satisfied.ts +++ b/src/helpers/approvals-satisfied.ts @@ -38,6 +38,7 @@ export const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pul const teamsList = teams?.split('\n'); const requiredCodeOwnersEntries = teamsList ? createArtificialCodeOwnersEntry(teamsList) : await getRequiredCodeOwnersEntries(prNumber); + const requiredCodeOwnersEntriesWithOwners = requiredCodeOwnersEntries.filter(({ owners }) => owners.length); const codeOwnersEntrySatisfiesApprovals = async (entry: Pick) => { const teamsAndLoginsLists = await map(entry.owners, async team => { @@ -59,8 +60,8 @@ export const approvalsSatisfied = async ({ teams, number_of_reviewers = '1', pul return numberOfApprovals >= Number(number_of_reviewers); }; - core.debug(`Required code owners: ${requiredCodeOwnersEntries.map(({ owners }) => owners).toString()}`); - const booleans = await Promise.all(requiredCodeOwnersEntries.map(codeOwnersEntrySatisfiesApprovals)); + core.debug(`Required code owners: ${requiredCodeOwnersEntriesWithOwners.map(({ owners }) => owners).toString()}`); + const booleans = await Promise.all(requiredCodeOwnersEntriesWithOwners.map(codeOwnersEntrySatisfiesApprovals)); return booleans.every(Boolean); }; diff --git a/test/helpers/approvals-satisfied.test.ts b/test/helpers/approvals-satisfied.test.ts index 43ebb28a9..2866b2b7f 100644 --- a/test/helpers/approvals-satisfied.test.ts +++ b/test/helpers/approvals-satisfied.test.ts @@ -304,4 +304,46 @@ describe('approvalsSatisfied', () => { const result = await approvalsSatisfied({ number_of_reviewers: '2' }); expect(result).toBe(true); }); + + it('should return true when there are no code owners for one file and the other file is satisfied', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: [] }, + { owners: ['@ExpediaGroup/team5', '@ExpediaGroup/team6'] } + ]); + mockPagination({ + data: [ + { + state: 'APPROVED', + user: { login: 'user4' } + }, + { + state: 'APPROVED', + user: { login: 'user5' } + }, + { + state: 'APPROVED', + user: { login: 'user8' } + } + ] + }); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(true); + }); + + it('should return false when there are no code owners for one file and the other file is not satisfied', async () => { + (getRequiredCodeOwnersEntries as jest.Mock).mockResolvedValue([ + { owners: [] }, + { owners: ['@ExpediaGroup/team5', '@ExpediaGroup/team6'] } + ]); + mockPagination({ + data: [ + { + state: 'APPROVED', + user: { login: 'user4' } + } + ] + }); + const result = await approvalsSatisfied({ number_of_reviewers: '2' }); + expect(result).toBe(false); + }); });