diff --git a/dist/431.index.js b/dist/431.index.js index 595fcf6c..45d3d921 100644 --- a/dist/431.index.js +++ b/dist/431.index.js @@ -133,7 +133,7 @@ const getDiff = async (compareBase, compareHead, basehead) => { catch (err) { _actions_core__WEBPACK_IMPORTED_MODULE_7__.info(`Failed to fetch diff: ${err.message} Status: ${err.status}`); // diff too large error - if (err?.status === 406) { + if (err?.status === 406 || err?.message.includes('diff is taking too long to generate')) { _actions_core__WEBPACK_IMPORTED_MODULE_7__.info(`Attempting to generate diff using local git command`); if (compareBase.repo?.html_url) { changedFileNames = await getDiffUsingGitCommand(compareBase.repo?.html_url, compareBase.sha, compareHead.sha); diff --git a/dist/431.index.js.map b/dist/431.index.js.map index 99d53dcc..b1c06224 100644 --- a/dist/431.index.js.map +++ b/dist/431.index.js.map @@ -1 +1 @@ -{"version":3,"file":"431.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAMA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAMA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AAEA;AAIA;AAaA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;;AAEA;AACA;AAEA;AAGA;;;;;;;;;;;;;;;;;;;;;AC3OA;;;;;;;;;;;AAWA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AAAA;;AACA;AACA;AACA;AAIA;AAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACrDA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAqDA;;;;;;;;;;;;;;AClEA;;;;;;;;;;;AAWA;AAGA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":[".././src/helpers/check-merge-safety.ts",".././src/helpers/set-commit-status.ts",".././src/octokit.ts",".././src/types/generated.ts",".././src/utils/paginate-open-pull-requests.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 as githubContext } from '@actions/github';\nimport { simpleGit } from 'simple-git';\nimport { octokit } from '../octokit';\nimport micromatch from 'micromatch';\nimport { GithubError, PullRequest } from '../types/github';\nimport { paginateAllOpenPullRequests } from '../utils/paginate-open-pull-requests';\nimport { map } from 'bluebird';\nimport { setCommitStatus } from './set-commit-status';\nimport * as core from '@actions/core';\n\nconst git = simpleGit();\n\nconst maxBranchNameLength = 50;\nexport class CheckMergeSafety extends HelperInputs {\n context?: string;\n paths?: string;\n ignore_globs?: string;\n override_filter_paths?: string;\n override_filter_globs?: string;\n}\n\nexport const checkMergeSafety = async (inputs: CheckMergeSafety) => {\n const isPrWorkflow = Boolean(githubContext.issue.number);\n if (!isPrWorkflow) {\n return handlePushWorkflow(inputs);\n }\n const { data: pullRequest } = await octokit.pulls.get({ pull_number: githubContext.issue.number, ...githubContext.repo });\n\n const { state, message } = await setMergeSafetyStatus(pullRequest, inputs);\n if (state === 'failure') {\n core.setFailed(message);\n }\n};\n\nconst setMergeSafetyStatus = async (pullRequest: PullRequest, { context = 'Merge Safety', ...inputs }: CheckMergeSafety) => {\n const { state, message } = await getMergeSafetyStateAndMessage(pullRequest, inputs);\n const hasExistingFailureStatus = await checkForExistingFailureStatus(pullRequest, context);\n if (hasExistingFailureStatus && state === 'failure') {\n const {\n head: {\n ref,\n user: { login: username }\n }\n } = pullRequest;\n const truncatedRef = ref.length > maxBranchNameLength ? `${ref.substring(0, maxBranchNameLength)}...` : ref;\n const truncatedBranchName = `${username}:${truncatedRef}`;\n core.info(`Found existing failure status for ${truncatedBranchName}, skipping setting new status`);\n } else {\n await setCommitStatus({\n sha: pullRequest.head.sha,\n state,\n context,\n description: message,\n ...githubContext.repo\n });\n }\n\n return { state, message };\n};\n\nconst handlePushWorkflow = async (inputs: CheckMergeSafety) => {\n const pullRequests = await paginateAllOpenPullRequests();\n const filteredPullRequests = pullRequests.filter(({ base, draft }) => !draft && base.ref === base.repo.default_branch);\n await map(filteredPullRequests, pullRequest => setMergeSafetyStatus(pullRequest as PullRequest, inputs));\n};\n\nconst checkForExistingFailureStatus = async (pullRequest: PullRequest, context: string) => {\n const { data } = await octokit.repos.getCombinedStatusForRef({\n ...githubContext.repo,\n ref: pullRequest.head.sha\n });\n if (data.state === 'failure') {\n const existingContext = data.statuses.find(status => status.context === context);\n return Boolean(existingContext);\n }\n return false;\n};\n\nconst fetchSha = async (repoUrl: string, sha: string) => {\n try {\n await git.fetch(repoUrl, sha, { '--depth': 1 });\n core.info(`Fetched ${sha} from ${repoUrl}`);\n } catch (err) {\n core.info(`Failed to fetch ${sha} from ${repoUrl}: ${(err as GithubError).message}`);\n throw new Error(`Failed to fetch ${sha} from ${repoUrl}: ${(err as GithubError).message}`);\n }\n};\n\nconst getDiffUsingGitCommand = async (repoUrl: string, baseSha: string, headSha: string): Promise => {\n // update local repo copy\n await fetchSha(repoUrl, baseSha);\n await fetchSha(repoUrl, headSha);\n\n try {\n const diff = await git.diff(['--name-only', baseSha, headSha]);\n return (diff ?? '').split('\\n').filter(Boolean);\n } catch (err) {\n core.error(`Failed to run local git diff for ${repoUrl}: ${(err as GithubError).message}`);\n throw new Error(`Failed to run local git diff for ${repoUrl}: ${(err as GithubError).message}`);\n }\n};\n\ntype DiffRefs = PullRequest['base' | 'head'];\nconst getDiff = async (compareBase: DiffRefs, compareHead: DiffRefs, basehead: string) => {\n let changedFileNames: string[] = [];\n try {\n const { data: { files: changedFiles } = {}, status } = await octokit.repos.compareCommitsWithBasehead({\n ...githubContext.repo,\n basehead\n });\n if (status > 400) {\n throw { status };\n }\n changedFileNames = changedFiles?.map(file => file.filename) ?? [];\n } catch (err) {\n core.info(`Failed to fetch diff: ${(err as GithubError).message} Status: ${(err as GithubError).status}`);\n\n // diff too large error\n if ((err as GithubError)?.status === 406) {\n core.info(`Attempting to generate diff using local git command`);\n if (compareBase.repo?.html_url) {\n changedFileNames = await getDiffUsingGitCommand(compareBase.repo?.html_url, compareBase.sha, compareHead.sha);\n } else {\n core.error(`Could not fetch repo url to run local git diff`);\n throw err;\n }\n } else {\n throw err;\n }\n }\n return changedFileNames;\n};\n\nconst getMergeSafetyStateAndMessage = async (\n pullRequest: PullRequest,\n { paths, ignore_globs, override_filter_paths, override_filter_globs }: CheckMergeSafety\n) => {\n const {\n base: {\n repo: {\n default_branch,\n owner: { login: baseOwner }\n }\n },\n head: {\n ref,\n user: { login: username }\n }\n } = pullRequest;\n\n const branchName = `${username}:${ref}`;\n const diffAgainstUserBranch = `${branchName}...${baseOwner}:${default_branch}`;\n let fileNamesWhichBranchIsBehindOn;\n try {\n fileNamesWhichBranchIsBehindOn = await getDiff(pullRequest.head, pullRequest.base, diffAgainstUserBranch);\n } catch (err) {\n const message = diffErrorMessage(diffAgainstUserBranch, (err as GithubError).message);\n core.error(message);\n return { state: 'failure', message } as const;\n }\n\n const truncatedRef = ref.length > maxBranchNameLength ? `${ref.substring(0, maxBranchNameLength)}...` : ref;\n const truncatedBranchName = `${username}:${truncatedRef}`;\n const globalFilesOutdatedOnBranch = override_filter_globs\n ? micromatch(fileNamesWhichBranchIsBehindOn, override_filter_globs.split(/[\\n,]/))\n : override_filter_paths\n ? fileNamesWhichBranchIsBehindOn.filter(changedFile => override_filter_paths.split(/[\\n,]/).includes(changedFile))\n : [];\n\n if (globalFilesOutdatedOnBranch.length) {\n core.error(buildErrorMessage(globalFilesOutdatedOnBranch, 'global files', truncatedBranchName));\n return {\n state: 'failure',\n message: `This branch has one or more outdated global files. Please update with ${default_branch}.`\n } as const;\n }\n\n const diffAgainstDefaultBranch = `${baseOwner}:${default_branch}...${branchName}`;\n let changedFileNames;\n try {\n changedFileNames = await getDiff(pullRequest.base, pullRequest.head, diffAgainstDefaultBranch);\n } catch (err) {\n const message = diffErrorMessage(diffAgainstDefaultBranch, (err as GithubError).message);\n core.error(message);\n return { state: 'failure', message } as const;\n }\n\n const changedFilesToIgnore = changedFileNames && ignore_globs ? micromatch(changedFileNames, ignore_globs.split(/[\\n,]/)) : [];\n const filteredFileNames = changedFileNames?.filter(file => !changedFilesToIgnore.includes(file));\n const allProjectDirectories = paths?.split(/[\\n,]/);\n\n const changedProjectsOutdatedOnBranch = allProjectDirectories?.filter(\n dir => fileNamesWhichBranchIsBehindOn.some(file => file.includes(dir)) && filteredFileNames?.some(file => file.includes(dir))\n );\n\n if (changedProjectsOutdatedOnBranch?.length) {\n core.error(buildErrorMessage(changedProjectsOutdatedOnBranch, 'projects', truncatedBranchName));\n return {\n state: 'failure',\n message: `This branch has one or more outdated projects. Please update with ${default_branch}.`\n } as const;\n }\n\n const safeMessage = buildSuccessMessage(truncatedBranchName);\n core.info(safeMessage);\n return {\n state: 'success',\n message: safeMessage\n } as const;\n};\n\nconst buildErrorMessage = (paths: string[], pathType: 'projects' | 'global files', branchName: string) =>\n `\nThe following ${pathType} are outdated on branch ${branchName}\n\n${paths.map(path => `* ${path}`).join('\\n')}\n`;\n\nconst diffErrorMessage = (basehead: string, message = '') =>\n `Failed to generate diff for ${basehead}. Please verify SHAs are valid and try again.${message ? `\\nError: ${message}` : ''}`;\n\nconst buildSuccessMessage = (branchName: string) => `Branch ${branchName} is safe to merge!`;\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 { PipelineState } from '../types/github';\nimport { HelperInputs } from '../types/generated';\nimport { context as githubContext } from '@actions/github';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport class SetCommitStatus extends HelperInputs {\n sha = '';\n context = '';\n state = '';\n description?: string;\n target_url?: string;\n skip_if_already_set?: string;\n}\n\nexport const setCommitStatus = async ({ sha, context, state, description, target_url, skip_if_already_set }: SetCommitStatus) => {\n await map(context.split('\\n').filter(Boolean), async context => {\n if (skip_if_already_set === 'true') {\n const check_runs = await octokit.checks.listForRef({\n ...githubContext.repo,\n ref: sha\n });\n const run = check_runs.data.check_runs.find(({ name }) => name === context);\n const runCompletedAndIsValid = run?.status === 'completed' && (run?.conclusion === 'failure' || run?.conclusion === 'success');\n if (runCompletedAndIsValid) {\n core.info(`${context} already completed with a ${run.conclusion} conclusion.`);\n return;\n }\n }\n\n octokit.repos.createCommitStatus({\n sha,\n context,\n state: state as PipelineState,\n description,\n target_url,\n ...githubContext.repo\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 users?: 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 skip_auto_merge?: string;\n repo_name?: string;\n repo_owner_name?: string;\n load_balancing_sizes?: string;\n required_review_overrides?: string;\n max_queue_size?: string;\n allow_only_for_maintainers?: string;\n use_basic_matrix_configuration?: string;\n merge_queue_enabled?: string;\n}\n","/*\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 { PullRequestList } from '../types/github';\nimport { octokit } from '../octokit';\nimport { context } from '@actions/github';\n\nexport const paginateAllOpenPullRequests = async (page = 1): Promise => {\n const response = await octokit.pulls.list({\n state: 'open',\n sort: 'updated',\n direction: 'desc',\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllOpenPullRequests(page + 1));\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file +{"version":3,"file":"431.index.js","mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAAA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AAEA;AACA;AAMA;AAEA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAMA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AAEA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AACA;AAEA;AAIA;AAaA;AACA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AAAA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AAEA;AAIA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AAEA;;AAEA;AACA;AAEA;AAGA;;;;;;;;;;;;;;;;;;;;;AC3OA;;;;;;;;;;;AAWA;AAEA;AAEA;AACA;AACA;AACA;AAEA;AAAA;;AACA;AACA;AACA;AAIA;AAAA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;ACrDA;;;;;;;;;;;AAWA;AAEA;AACA;AACA;AAEA;AACA;;;;;;;;;;;AClBA;;;;;;;;;;;AAWA;AAEA;AAqDA;;;;;;;;;;;;;;AClEA;;;;;;;;;;;AAWA;AAGA;AACA;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","sources":[".././src/helpers/check-merge-safety.ts",".././src/helpers/set-commit-status.ts",".././src/octokit.ts",".././src/types/generated.ts",".././src/utils/paginate-open-pull-requests.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 as githubContext } from '@actions/github';\nimport { simpleGit } from 'simple-git';\nimport { octokit } from '../octokit';\nimport micromatch from 'micromatch';\nimport { GithubError, PullRequest } from '../types/github';\nimport { paginateAllOpenPullRequests } from '../utils/paginate-open-pull-requests';\nimport { map } from 'bluebird';\nimport { setCommitStatus } from './set-commit-status';\nimport * as core from '@actions/core';\n\nconst git = simpleGit();\n\nconst maxBranchNameLength = 50;\nexport class CheckMergeSafety extends HelperInputs {\n context?: string;\n paths?: string;\n ignore_globs?: string;\n override_filter_paths?: string;\n override_filter_globs?: string;\n}\n\nexport const checkMergeSafety = async (inputs: CheckMergeSafety) => {\n const isPrWorkflow = Boolean(githubContext.issue.number);\n if (!isPrWorkflow) {\n return handlePushWorkflow(inputs);\n }\n const { data: pullRequest } = await octokit.pulls.get({ pull_number: githubContext.issue.number, ...githubContext.repo });\n\n const { state, message } = await setMergeSafetyStatus(pullRequest, inputs);\n if (state === 'failure') {\n core.setFailed(message);\n }\n};\n\nconst setMergeSafetyStatus = async (pullRequest: PullRequest, { context = 'Merge Safety', ...inputs }: CheckMergeSafety) => {\n const { state, message } = await getMergeSafetyStateAndMessage(pullRequest, inputs);\n const hasExistingFailureStatus = await checkForExistingFailureStatus(pullRequest, context);\n if (hasExistingFailureStatus && state === 'failure') {\n const {\n head: {\n ref,\n user: { login: username }\n }\n } = pullRequest;\n const truncatedRef = ref.length > maxBranchNameLength ? `${ref.substring(0, maxBranchNameLength)}...` : ref;\n const truncatedBranchName = `${username}:${truncatedRef}`;\n core.info(`Found existing failure status for ${truncatedBranchName}, skipping setting new status`);\n } else {\n await setCommitStatus({\n sha: pullRequest.head.sha,\n state,\n context,\n description: message,\n ...githubContext.repo\n });\n }\n\n return { state, message };\n};\n\nconst handlePushWorkflow = async (inputs: CheckMergeSafety) => {\n const pullRequests = await paginateAllOpenPullRequests();\n const filteredPullRequests = pullRequests.filter(({ base, draft }) => !draft && base.ref === base.repo.default_branch);\n await map(filteredPullRequests, pullRequest => setMergeSafetyStatus(pullRequest as PullRequest, inputs));\n};\n\nconst checkForExistingFailureStatus = async (pullRequest: PullRequest, context: string) => {\n const { data } = await octokit.repos.getCombinedStatusForRef({\n ...githubContext.repo,\n ref: pullRequest.head.sha\n });\n if (data.state === 'failure') {\n const existingContext = data.statuses.find(status => status.context === context);\n return Boolean(existingContext);\n }\n return false;\n};\n\nconst fetchSha = async (repoUrl: string, sha: string) => {\n try {\n await git.fetch(repoUrl, sha, { '--depth': 1 });\n core.info(`Fetched ${sha} from ${repoUrl}`);\n } catch (err) {\n core.info(`Failed to fetch ${sha} from ${repoUrl}: ${(err as GithubError).message}`);\n throw new Error(`Failed to fetch ${sha} from ${repoUrl}: ${(err as GithubError).message}`);\n }\n};\n\nconst getDiffUsingGitCommand = async (repoUrl: string, baseSha: string, headSha: string): Promise => {\n // update local repo copy\n await fetchSha(repoUrl, baseSha);\n await fetchSha(repoUrl, headSha);\n\n try {\n const diff = await git.diff(['--name-only', baseSha, headSha]);\n return (diff ?? '').split('\\n').filter(Boolean);\n } catch (err) {\n core.error(`Failed to run local git diff for ${repoUrl}: ${(err as GithubError).message}`);\n throw new Error(`Failed to run local git diff for ${repoUrl}: ${(err as GithubError).message}`);\n }\n};\n\ntype DiffRefs = PullRequest['base' | 'head'];\nconst getDiff = async (compareBase: DiffRefs, compareHead: DiffRefs, basehead: string) => {\n let changedFileNames: string[] = [];\n try {\n const { data: { files: changedFiles } = {}, status } = await octokit.repos.compareCommitsWithBasehead({\n ...githubContext.repo,\n basehead\n });\n if (status > 400) {\n throw { status };\n }\n changedFileNames = changedFiles?.map(file => file.filename) ?? [];\n } catch (err) {\n core.info(`Failed to fetch diff: ${(err as GithubError).message} Status: ${(err as GithubError).status}`);\n\n // diff too large error\n if ((err as GithubError)?.status === 406 || (err as GithubError)?.message.includes('diff is taking too long to generate')) {\n core.info(`Attempting to generate diff using local git command`);\n if (compareBase.repo?.html_url) {\n changedFileNames = await getDiffUsingGitCommand(compareBase.repo?.html_url, compareBase.sha, compareHead.sha);\n } else {\n core.error(`Could not fetch repo url to run local git diff`);\n throw err;\n }\n } else {\n throw err;\n }\n }\n return changedFileNames;\n};\n\nconst getMergeSafetyStateAndMessage = async (\n pullRequest: PullRequest,\n { paths, ignore_globs, override_filter_paths, override_filter_globs }: CheckMergeSafety\n) => {\n const {\n base: {\n repo: {\n default_branch,\n owner: { login: baseOwner }\n }\n },\n head: {\n ref,\n user: { login: username }\n }\n } = pullRequest;\n\n const branchName = `${username}:${ref}`;\n const diffAgainstUserBranch = `${branchName}...${baseOwner}:${default_branch}`;\n let fileNamesWhichBranchIsBehindOn;\n try {\n fileNamesWhichBranchIsBehindOn = await getDiff(pullRequest.head, pullRequest.base, diffAgainstUserBranch);\n } catch (err) {\n const message = diffErrorMessage(diffAgainstUserBranch, (err as GithubError).message);\n core.error(message);\n return { state: 'failure', message } as const;\n }\n\n const truncatedRef = ref.length > maxBranchNameLength ? `${ref.substring(0, maxBranchNameLength)}...` : ref;\n const truncatedBranchName = `${username}:${truncatedRef}`;\n const globalFilesOutdatedOnBranch = override_filter_globs\n ? micromatch(fileNamesWhichBranchIsBehindOn, override_filter_globs.split(/[\\n,]/))\n : override_filter_paths\n ? fileNamesWhichBranchIsBehindOn.filter(changedFile => override_filter_paths.split(/[\\n,]/).includes(changedFile))\n : [];\n\n if (globalFilesOutdatedOnBranch.length) {\n core.error(buildErrorMessage(globalFilesOutdatedOnBranch, 'global files', truncatedBranchName));\n return {\n state: 'failure',\n message: `This branch has one or more outdated global files. Please update with ${default_branch}.`\n } as const;\n }\n\n const diffAgainstDefaultBranch = `${baseOwner}:${default_branch}...${branchName}`;\n let changedFileNames;\n try {\n changedFileNames = await getDiff(pullRequest.base, pullRequest.head, diffAgainstDefaultBranch);\n } catch (err) {\n const message = diffErrorMessage(diffAgainstDefaultBranch, (err as GithubError).message);\n core.error(message);\n return { state: 'failure', message } as const;\n }\n\n const changedFilesToIgnore = changedFileNames && ignore_globs ? micromatch(changedFileNames, ignore_globs.split(/[\\n,]/)) : [];\n const filteredFileNames = changedFileNames?.filter(file => !changedFilesToIgnore.includes(file));\n const allProjectDirectories = paths?.split(/[\\n,]/);\n\n const changedProjectsOutdatedOnBranch = allProjectDirectories?.filter(\n dir => fileNamesWhichBranchIsBehindOn.some(file => file.includes(dir)) && filteredFileNames?.some(file => file.includes(dir))\n );\n\n if (changedProjectsOutdatedOnBranch?.length) {\n core.error(buildErrorMessage(changedProjectsOutdatedOnBranch, 'projects', truncatedBranchName));\n return {\n state: 'failure',\n message: `This branch has one or more outdated projects. Please update with ${default_branch}.`\n } as const;\n }\n\n const safeMessage = buildSuccessMessage(truncatedBranchName);\n core.info(safeMessage);\n return {\n state: 'success',\n message: safeMessage\n } as const;\n};\n\nconst buildErrorMessage = (paths: string[], pathType: 'projects' | 'global files', branchName: string) =>\n `\nThe following ${pathType} are outdated on branch ${branchName}\n\n${paths.map(path => `* ${path}`).join('\\n')}\n`;\n\nconst diffErrorMessage = (basehead: string, message = '') =>\n `Failed to generate diff for ${basehead}. Please verify SHAs are valid and try again.${message ? `\\nError: ${message}` : ''}`;\n\nconst buildSuccessMessage = (branchName: string) => `Branch ${branchName} is safe to merge!`;\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 { PipelineState } from '../types/github';\nimport { HelperInputs } from '../types/generated';\nimport { context as githubContext } from '@actions/github';\nimport { map } from 'bluebird';\nimport { octokit } from '../octokit';\n\nexport class SetCommitStatus extends HelperInputs {\n sha = '';\n context = '';\n state = '';\n description?: string;\n target_url?: string;\n skip_if_already_set?: string;\n}\n\nexport const setCommitStatus = async ({ sha, context, state, description, target_url, skip_if_already_set }: SetCommitStatus) => {\n await map(context.split('\\n').filter(Boolean), async context => {\n if (skip_if_already_set === 'true') {\n const check_runs = await octokit.checks.listForRef({\n ...githubContext.repo,\n ref: sha\n });\n const run = check_runs.data.check_runs.find(({ name }) => name === context);\n const runCompletedAndIsValid = run?.status === 'completed' && (run?.conclusion === 'failure' || run?.conclusion === 'success');\n if (runCompletedAndIsValid) {\n core.info(`${context} already completed with a ${run.conclusion} conclusion.`);\n return;\n }\n }\n\n octokit.repos.createCommitStatus({\n sha,\n context,\n state: state as PipelineState,\n description,\n target_url,\n ...githubContext.repo\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 users?: 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 skip_auto_merge?: string;\n repo_name?: string;\n repo_owner_name?: string;\n load_balancing_sizes?: string;\n required_review_overrides?: string;\n max_queue_size?: string;\n allow_only_for_maintainers?: string;\n use_basic_matrix_configuration?: string;\n merge_queue_enabled?: string;\n}\n","/*\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 { PullRequestList } from '../types/github';\nimport { octokit } from '../octokit';\nimport { context } from '@actions/github';\n\nexport const paginateAllOpenPullRequests = async (page = 1): Promise => {\n const response = await octokit.pulls.list({\n state: 'open',\n sort: 'updated',\n direction: 'desc',\n per_page: 100,\n page,\n ...context.repo\n });\n if (!response.data.length) {\n return [];\n }\n return response.data.concat(await paginateAllOpenPullRequests(page + 1));\n};\n"],"names":[],"sourceRoot":""} \ No newline at end of file diff --git a/src/helpers/check-merge-safety.ts b/src/helpers/check-merge-safety.ts index c1d1193b..68847d0e 100644 --- a/src/helpers/check-merge-safety.ts +++ b/src/helpers/check-merge-safety.ts @@ -130,7 +130,7 @@ const getDiff = async (compareBase: DiffRefs, compareHead: DiffRefs, basehead: s core.info(`Failed to fetch diff: ${(err as GithubError).message} Status: ${(err as GithubError).status}`); // diff too large error - if ((err as GithubError)?.status === 406) { + if ((err as GithubError)?.status === 406 || (err as GithubError)?.message.includes('diff is taking too long to generate')) { core.info(`Attempting to generate diff using local git command`); if (compareBase.repo?.html_url) { changedFileNames = await getDiffUsingGitCommand(compareBase.repo?.html_url, compareBase.sha, compareHead.sha); diff --git a/test/helpers/check-merge-safety.test.ts b/test/helpers/check-merge-safety.test.ts index ff19af4b..a5bad301 100644 --- a/test/helpers/check-merge-safety.test.ts +++ b/test/helpers/check-merge-safety.test.ts @@ -62,7 +62,7 @@ type MockGithubRequests = ( filesOutOfDate: string[], changedFilesOnPr: string[], branch?: string, - error?: { status: number; message?: string } | null + error?: { status: number | string; message?: string } | null ) => void; const mockGithubRequests: MockGithubRequests = (filesOutOfDate, changedFilesOnPr, branch = branchName, error = null) => { (octokit.repos.compareCommitsWithBasehead as unknown as Mocktokit).mockImplementation(async ({ basehead }) => { @@ -136,7 +136,7 @@ describe('checkMergeSafety', () => { expect(core.setFailed).toHaveBeenCalledWith('This branch has one or more outdated projects. Please update with main.'); }); - it('should allow merge when branch is only out of date for an unchanged project - diff too large error', async () => { + it('should allow merge when branch is only out of date for an unchanged project - diff too large error status', async () => { const filesOutOfDate = ['packages/package-2/src/another-file.ts']; const changedFilesOnPr = ['packages/package-1/src/some-file.ts']; mockGithubRequests(filesOutOfDate, changedFilesOnPr, branchName, { status: 406 }); @@ -157,6 +157,30 @@ describe('checkMergeSafety', () => { expect(core.setFailed).not.toHaveBeenCalled(); }); + it('should allow merge when branch is only out of date for an unchanged project - diff too large error message', async () => { + const filesOutOfDate = ['packages/package-2/src/another-file.ts']; + const changedFilesOnPr = ['packages/package-1/src/some-file.ts']; + mockGithubRequests(filesOutOfDate, changedFilesOnPr, branchName, { + status: 'not_available', + message: 'yadda yadda diff is taking too long to generate yadda yadda' + }); + mockGitInteractions(filesOutOfDate, changedFilesOnPr); + + await checkMergeSafety({ + paths: allProjectPaths, + ...context.repo + }); + expect(setCommitStatus).toHaveBeenCalledWith({ + sha, + state: 'success', + context: 'Merge Safety', + description: 'Branch username:some-branch-name is safe to merge!', + repo: 'repo', + owner: 'owner' + }); + expect(core.setFailed).not.toHaveBeenCalled(); + }); + it('should prevent merge when branch is out of date for a changed project - diff too large error', async () => { const filesOutOfDate = ['packages/package-1/src/another-file.ts']; const changedFilesOnPr = ['packages/package-1/src/some-file.ts'];