UNPKG

renovate

Version:

Automated dependency updates. Flexible so you don't need to be.

1,179 lines • 67.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.id = void 0; exports.resetConfigs = resetConfigs; exports.isGHApp = isGHApp; exports.detectGhe = detectGhe; exports.initPlatform = initPlatform; exports.getRepos = getRepos; exports.getRawFile = getRawFile; exports.getJsonFile = getJsonFile; exports.listForks = listForks; exports.findFork = findFork; exports.createFork = createFork; exports.initRepo = initRepo; exports.getBranchForceRebase = getBranchForceRebase; exports.getPr = getPr; exports.getPrList = getPrList; exports.findPr = findPr; exports.getBranchPr = getBranchPr; exports.tryReuseAutoclosedPr = tryReuseAutoclosedPr; exports.getBranchStatus = getBranchStatus; exports.getBranchStatusCheck = getBranchStatusCheck; exports.setBranchStatus = setBranchStatus; exports.getIssueList = getIssueList; exports.getIssue = getIssue; exports.findIssue = findIssue; exports.ensureIssue = ensureIssue; exports.ensureIssueClosing = ensureIssueClosing; exports.addAssignees = addAssignees; exports.addReviewers = addReviewers; exports.addLabels = addLabels; exports.deleteLabel = deleteLabel; exports.ensureComment = ensureComment; exports.ensureCommentRemoval = ensureCommentRemoval; exports.createPr = createPr; exports.updatePr = updatePr; exports.reattemptPlatformAutomerge = reattemptPlatformAutomerge; exports.mergePr = mergePr; exports.massageMarkdown = massageMarkdown; exports.maxBodyLength = maxBodyLength; exports.getVulnerabilityAlerts = getVulnerabilityAlerts; exports.commitFiles = commitFiles; const tslib_1 = require("tslib"); const node_url_1 = tslib_1.__importDefault(require("node:url")); const promises_1 = require("timers/promises"); const is_1 = tslib_1.__importDefault(require("@sindresorhus/is")); const semver_1 = tslib_1.__importDefault(require("semver")); const error_messages_1 = require("../../../constants/error-messages"); const logger_1 = require("../../../logger"); const external_host_error_1 = require("../../../types/errors/external-host-error"); const check_token_1 = require("../../../util/check-token"); const coerce_1 = require("../../../util/coerce"); const common_1 = require("../../../util/common"); const env_1 = require("../../../util/env"); const git = tslib_1.__importStar(require("../../../util/git")); const git_1 = require("../../../util/git"); const hostRules = tslib_1.__importStar(require("../../../util/host-rules")); const memory_http_cache_provider_1 = require("../../../util/http/cache/memory-http-cache-provider"); const repository_http_cache_provider_1 = require("../../../util/http/cache/repository-http-cache-provider"); const githubHttp = tslib_1.__importStar(require("../../../util/http/github")); const object_1 = require("../../../util/object"); const regex_1 = require("../../../util/regex"); const sanitize_1 = require("../../../util/sanitize"); const string_1 = require("../../../util/string"); const url_1 = require("../../../util/url"); const limits_1 = require("../../../workers/global/limits"); const util_1 = require("../util"); const github_alerts_1 = require("../utils/github-alerts"); const pr_body_1 = require("../utils/pr-body"); const branch_1 = require("./branch"); const common_2 = require("./common"); const graphql_1 = require("./graphql"); const issue_1 = require("./issue"); const massage_markdown_links_1 = require("./massage-markdown-links"); const pr_1 = require("./pr"); const schema_1 = require("./schema"); const user_1 = require("./user"); exports.id = 'github'; let config; let platformConfig; const GitHubMaxPrBodyLen = 60000; function resetConfigs() { config = {}; platformConfig = { hostType: 'github', endpoint: 'https://api.github.com/', }; } resetConfigs(); function escapeHash(input) { return input?.replace((0, regex_1.regEx)(/#/g), '%23'); } function isGHApp() { return !!platformConfig.isGHApp; } async function detectGhe(token) { platformConfig.isGhe = node_url_1.default.parse(platformConfig.endpoint).host !== 'api.github.com'; if (platformConfig.isGhe) { const gheHeaderKey = 'x-github-enterprise-version'; const gheQueryRes = await common_2.githubApi.headJson('/', { token }); const gheHeaders = (0, object_1.coerceObject)(gheQueryRes?.headers); const [, gheVersion] = Object.entries(gheHeaders).find(([k]) => k.toLowerCase() === gheHeaderKey) ?? []; platformConfig.gheVersion = semver_1.default.valid(gheVersion) ?? null; logger_1.logger.debug(`Detected GitHub Enterprise Server, version: ${platformConfig.gheVersion}`); } } async function initPlatform({ endpoint, token: originalToken, username, gitAuthor, }) { let token = originalToken; if (!token) { throw new Error('Init: You must configure a GitHub token'); } token = token.replace(/^ghs_/, 'x-access-token:ghs_'); platformConfig.isGHApp = token.startsWith('x-access-token:'); if (endpoint) { platformConfig.endpoint = (0, url_1.ensureTrailingSlash)(endpoint); githubHttp.setBaseUrl(platformConfig.endpoint); } else { logger_1.logger.debug('Using default github endpoint: ' + platformConfig.endpoint); } await detectGhe(token); /** * GHE requires version >=3.10 to support fine-grained access tokens * https://docs.github.com/en/enterprise-server@3.10/admin/release-notes#authentication */ if ((0, check_token_1.isGithubFineGrainedPersonalAccessToken)(token) && platformConfig.isGhe && (!platformConfig.gheVersion || semver_1.default.lt(platformConfig.gheVersion, '3.10.0'))) { throw new Error('Init: Fine-grained Personal Access Tokens do not support GitHub Enterprise Server API version <3.10 and cannot be used with Renovate.'); } let renovateUsername; if (username) { renovateUsername = username; } else if (platformConfig.isGHApp) { platformConfig.userDetails ??= await (0, user_1.getAppDetails)(token); renovateUsername = platformConfig.userDetails.username; } else { platformConfig.userDetails ??= await (0, user_1.getUserDetails)(platformConfig.endpoint, token); renovateUsername = platformConfig.userDetails.username; } let discoveredGitAuthor; if (!gitAuthor) { if (platformConfig.isGHApp) { platformConfig.userDetails ??= await (0, user_1.getAppDetails)(token); const ghHostname = platformConfig.isGhe ? node_url_1.default.parse(platformConfig.endpoint).hostname : 'github.com'; discoveredGitAuthor = `${platformConfig.userDetails.name} <${platformConfig.userDetails.id}+${platformConfig.userDetails.username}@users.noreply.${ghHostname}>`; } else { platformConfig.userDetails ??= await (0, user_1.getUserDetails)(platformConfig.endpoint, token); platformConfig.userEmail ??= await (0, user_1.getUserEmail)(platformConfig.endpoint, token); if (platformConfig.userEmail) { discoveredGitAuthor = `${platformConfig.userDetails.name} <${platformConfig.userEmail}>`; } } } logger_1.logger.debug({ platformConfig, renovateUsername }, 'Platform config'); const platformResult = { endpoint: platformConfig.endpoint, gitAuthor: gitAuthor ?? discoveredGitAuthor, renovateUsername, token, }; if ((0, env_1.getEnv)().RENOVATE_X_GITHUB_HOST_RULES && platformResult.endpoint === 'https://api.github.com/') { logger_1.logger.debug('Adding GitHub token as GHCR password'); platformResult.hostRules = [ { matchHost: 'ghcr.io', hostType: 'docker', username: 'USERNAME', password: token.replace(/^x-access-token:/, ''), }, ]; logger_1.logger.debug('Adding GitHub token as npm.pkg.github.com Basic token'); platformResult.hostRules.push({ matchHost: 'npm.pkg.github.com', hostType: 'npm', token: token.replace(/^x-access-token:/, ''), }); const usernamePasswordHostTypes = ['rubygems', 'maven', 'nuget']; for (const hostType of usernamePasswordHostTypes) { logger_1.logger.debug(`Adding GitHub token as ${hostType}.pkg.github.com password`); platformResult.hostRules.push({ hostType, matchHost: `${hostType}.pkg.github.com`, username: renovateUsername, password: token.replace(/^x-access-token:/, ''), }); } } return platformResult; } async function fetchRepositories() { try { if (isGHApp()) { const res = await common_2.githubApi.getJsonUnchecked(`installation/repositories?per_page=100`, { paginationField: 'repositories', paginate: 'all', }); return res.body.repositories; } else { const res = await common_2.githubApi.getJsonUnchecked(`user/repos?per_page=100`, { paginate: 'all' }); return res.body; } } catch (err) /* v8 ignore start */ { logger_1.logger.error({ err }, `GitHub getRepos error`); throw err; } /* v8 ignore stop */ } // Get all repositories that the user has access to async function getRepos(config) { logger_1.logger.debug('Autodiscovering GitHub repositories'); const nonEmptyRepositories = (await fetchRepositories()).filter(is_1.default.nonEmptyObject); const nonArchivedRepositories = nonEmptyRepositories.filter((repo) => !repo.archived); if (nonArchivedRepositories.length < nonEmptyRepositories.length) { logger_1.logger.debug(`Filtered out ${nonEmptyRepositories.length - nonArchivedRepositories.length} archived repositories`); } if (!config?.topics) { return nonArchivedRepositories.map((repo) => repo.full_name); } logger_1.logger.debug({ topics: config.topics }, 'Filtering by topics'); const topicRepositories = nonArchivedRepositories.filter((repo) => repo.topics?.some((topic) => config?.topics?.includes(topic))); if (topicRepositories.length < nonArchivedRepositories.length) { logger_1.logger.debug(`Filtered out ${nonArchivedRepositories.length - topicRepositories.length} repositories not matching topic filters`); } return topicRepositories.map((repo) => repo.full_name); } async function getBranchProtection(branchName) { /* v8 ignore start */ if (config.parentRepo) { return {}; } /* v8 ignore stop */ const res = await common_2.githubApi.getJsonUnchecked(`repos/${config.repository}/branches/${escapeHash(branchName)}/protection`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider }); return res.body; } async function getRawFile(fileName, repoName, branchOrTag) { const repo = repoName ?? config.repository; // only use cache for the same org const httpOptions = {}; const isSameOrg = repo?.split('/')?.[0] === config.repositoryOwner; if (isSameOrg) { httpOptions.cacheProvider = repository_http_cache_provider_1.repoCacheProvider; } let url = `repos/${repo}/contents/${fileName}`; if (branchOrTag) { url += `?ref=` + branchOrTag; } const res = await common_2.githubApi.getJsonUnchecked(url, httpOptions); const buf = res.body.content; const str = (0, string_1.fromBase64)(buf); return str; } async function getJsonFile(fileName, repoName, branchOrTag) { const raw = await getRawFile(fileName, repoName, branchOrTag); return (0, common_1.parseJson)(raw, fileName); } async function listForks(token, repository) { try { // Get list of existing repos const url = `repos/${repository}/forks?per_page=100`; const repos = (await common_2.githubApi.getJsonUnchecked(url, { token, paginate: true, pageLimit: 100, })).body; logger_1.logger.debug(`Found ${repos.length} forked repo(s)`); return repos; } catch (err) { if (err.statusCode === 404) { logger_1.logger.debug('Cannot list repo forks - it is likely private'); } else { logger_1.logger.debug({ err }, 'Unknown error listing repository forks'); } throw new Error(error_messages_1.REPOSITORY_CANNOT_FORK); } } async function findFork(token, repository, forkOrg) { const forks = await listForks(token, repository); if (forkOrg) { logger_1.logger.debug(`Searching for forked repo in forkOrg (${forkOrg})`); const forkedRepo = forks.find((repo) => repo.owner.login === forkOrg); if (forkedRepo) { logger_1.logger.debug(`Found repo in forkOrg: ${forkedRepo.full_name}`); return forkedRepo; } logger_1.logger.debug(`No repo found in forkOrg`); } logger_1.logger.debug(`Searching for forked repo in user account`); try { const { username } = await (0, user_1.getUserDetails)(platformConfig.endpoint, token); const forkedRepo = forks.find((repo) => repo.owner.login === username); if (forkedRepo) { logger_1.logger.debug(`Found repo in user account: ${forkedRepo.full_name}`); return forkedRepo; } } catch { throw new Error(error_messages_1.REPOSITORY_CANNOT_FORK); } logger_1.logger.debug(`No repo found in user account`); return null; } async function createFork(token, repository, forkOrg) { let forkedRepo; try { forkedRepo = (await common_2.githubApi.postJson(`repos/${repository}/forks`, { token, body: { organization: forkOrg ?? undefined, name: config.parentRepo.replace('/', '-_-'), default_branch_only: true, // no baseBranches support yet }, })).body; } catch (err) { logger_1.logger.debug({ err }, 'Error creating fork'); } if (!forkedRepo) { throw new Error(error_messages_1.REPOSITORY_CANNOT_FORK); } logger_1.logger.info({ forkedRepo: forkedRepo.full_name }, 'Created forked repo'); logger_1.logger.debug(`Sleeping 30s after creating fork`); await (0, promises_1.setTimeout)(30000); return forkedRepo; } // Initialize GitHub by getting base branch and SHA async function initRepo({ endpoint, repository, forkCreation, forkOrg, forkToken, renovateUsername, cloneSubmodules, cloneSubmodulesFilter, ignorePrAuthor, }) { logger_1.logger.debug(`initRepo("${repository}")`); // config is used by the platform api itself, not necessary for the app layer to know config = { repository, cloneSubmodules, cloneSubmodulesFilter, ignorePrAuthor, }; /* v8 ignore start */ if (endpoint) { // Necessary for Renovate Pro - do not remove logger_1.logger.debug(`Overriding default GitHub endpoint with ${endpoint}`); platformConfig.endpoint = endpoint; githubHttp.setBaseUrl(endpoint); } /* v8 ignore stop */ const opts = hostRules.find({ hostType: 'github', url: platformConfig.endpoint, readOnly: true, }); config.renovateUsername = renovateUsername; [config.repositoryOwner, config.repositoryName] = repository.split('/'); let repo; try { let infoQuery = graphql_1.repoInfoQuery; // GitHub Enterprise Server <3.3.0 doesn't support autoMergeAllowed and hasIssuesEnabled objects // TODO #22198 if (platformConfig.isGhe && // semver not null safe, accepts null and undefined semver_1.default.satisfies(platformConfig.gheVersion, '<3.3.0')) { infoQuery = infoQuery.replace(/\n\s*autoMergeAllowed\s*\n/, '\n'); infoQuery = infoQuery.replace(/\n\s*hasIssuesEnabled\s*\n/, '\n'); } // GitHub Enterprise Server <3.9.0 doesn't support hasVulnerabilityAlertsEnabled objects if (platformConfig.isGhe && // semver not null safe, accepts null and undefined semver_1.default.satisfies(platformConfig.gheVersion, '<3.9.0')) { infoQuery = infoQuery.replace(/\n\s*hasVulnerabilityAlertsEnabled\s*\n/, '\n'); } const res = await common_2.githubApi.requestGraphql(infoQuery, { variables: { owner: config.repositoryOwner, name: config.repositoryName, ...(!ignorePrAuthor && { user: renovateUsername }), }, readOnly: true, }); if (res?.errors) { if (res.errors.find((err) => err.type === 'RATE_LIMITED')) { logger_1.logger.debug({ res }, 'Graph QL rate limit exceeded.'); throw new Error(error_messages_1.PLATFORM_RATE_LIMIT_EXCEEDED); } logger_1.logger.debug({ res }, 'Unexpected Graph QL errors'); throw new Error(error_messages_1.PLATFORM_UNKNOWN_ERROR); } repo = res?.data?.repository; /* v8 ignore start */ if (!repo) { logger_1.logger.debug({ res }, 'No repository returned'); throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } /* v8 ignore stop */ /* v8 ignore start */ if (!repo.defaultBranchRef?.name) { logger_1.logger.debug({ res }, 'No default branch returned - treating repo as empty'); throw new Error(error_messages_1.REPOSITORY_EMPTY); } /* v8 ignore stop */ if (repo.nameWithOwner && repo.nameWithOwner.toUpperCase() !== repository.toUpperCase()) { logger_1.logger.debug({ desiredRepo: repository, foundRepo: repo.nameWithOwner }, 'Repository has been renamed'); throw new Error(error_messages_1.REPOSITORY_RENAMED); } if (repo.isArchived) { logger_1.logger.debug('Repository is archived - throwing error to abort renovation'); throw new Error(error_messages_1.REPOSITORY_ARCHIVED); } // Use default branch as PR target unless later overridden. config.defaultBranch = repo.defaultBranchRef.name; // Base branch may be configured but defaultBranch is always fixed logger_1.logger.debug(`${repository} default branch = ${config.defaultBranch}`); // GitHub allows administrators to block certain types of merge, so we need to check it if (repo.squashMergeAllowed) { config.mergeMethod = 'squash'; } else if (repo.mergeCommitAllowed) { config.mergeMethod = 'merge'; } else if (repo.rebaseMergeAllowed) { config.mergeMethod = 'rebase'; } else { // This happens if we don't have Administrator read access, it is not a critical error logger_1.logger.debug('Could not find allowed merge methods for repo'); } config.autoMergeAllowed = repo.autoMergeAllowed; config.hasIssuesEnabled = repo.hasIssuesEnabled; config.hasVulnerabilityAlertsEnabled = repo.hasVulnerabilityAlertsEnabled; const recentIssues = issue_1.GithubIssue.array() .catch([]) .parse(res?.data?.repository?.issues?.nodes); issue_1.GithubIssueCache.addIssuesToReconcile(recentIssues); } catch (err) /* v8 ignore start */ { logger_1.logger.debug({ err }, 'Caught initRepo error'); if (err.message === error_messages_1.REPOSITORY_ARCHIVED || err.message === error_messages_1.REPOSITORY_RENAMED || err.message === error_messages_1.REPOSITORY_NOT_FOUND) { throw err; } if (err.statusCode === 403) { throw new Error(error_messages_1.REPOSITORY_ACCESS_FORBIDDEN); } if (err.statusCode === 404) { throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } if (err.message.startsWith('Repository access blocked')) { throw new Error(error_messages_1.REPOSITORY_BLOCKED); } if (err.message === error_messages_1.REPOSITORY_FORK_MODE_FORKED) { throw err; } if (err.message === error_messages_1.REPOSITORY_FORKED) { throw err; } if (err.message === error_messages_1.REPOSITORY_DISABLED) { throw err; } if (err.message === 'Response code 451 (Unavailable for Legal Reasons)') { throw new Error(error_messages_1.REPOSITORY_ACCESS_FORBIDDEN); } logger_1.logger.debug({ err }, 'Unknown GitHub initRepo error'); throw err; } /* v8 ignore stop */ // This shouldn't be necessary, but occasional strange errors happened until it was added config.prList = null; if (forkToken) { logger_1.logger.debug('Bot is in fork mode'); if (repo.isFork) { logger_1.logger.debug(`Forked repos cannot be processed when running with a forkToken, so this repo will be skipped`); logger_1.logger.debug(`Parent repo for this forked repo is ${repo.parent?.nameWithOwner}`); throw new Error(error_messages_1.REPOSITORY_FORKED); } config.forkOrg = forkOrg; config.forkToken = forkToken; // save parent name then delete config.parentRepo = config.repository; config.repository = null; let forkedRepo = await findFork(forkToken, repository, forkOrg); if (forkedRepo) { config.repository = forkedRepo.full_name; const forkDefaultBranch = forkedRepo.default_branch; if (forkDefaultBranch !== config.defaultBranch) { const body = { ref: `refs/heads/${config.defaultBranch}`, sha: repo.defaultBranchRef.target.oid, }; logger_1.logger.debug({ defaultBranch: config.defaultBranch, forkDefaultBranch, body, }, 'Fork has different default branch to parent, attempting to create branch'); try { await common_2.githubApi.postJson(`repos/${config.repository}/git/refs`, { body, token: forkToken, }); logger_1.logger.debug('Created new default branch in fork'); } catch (err) /* v8 ignore start */ { if (err.response?.body?.message === 'Reference already exists') { logger_1.logger.debug(`Branch ${config.defaultBranch} already exists in the fork`); } else { logger_1.logger.warn({ err, body: err.response?.body }, 'Could not create parent defaultBranch in fork'); } } /* v8 ignore stop */ logger_1.logger.debug(`Setting ${config.defaultBranch} as default branch for ${config.repository}`); try { await common_2.githubApi.patchJson(`repos/${config.repository}`, { body: { name: config.repository.split('/')[1], default_branch: config.defaultBranch, }, token: forkToken, }); logger_1.logger.debug('Successfully changed default branch for fork'); } catch (err) /* v8 ignore start */ { logger_1.logger.warn({ err }, 'Could not set default branch'); } /* v8 ignore stop */ } } else if (forkCreation) { logger_1.logger.debug('Forked repo is not found - attempting to create it'); forkedRepo = await createFork(forkToken, repository, forkOrg); config.repository = forkedRepo.full_name; } else { logger_1.logger.debug('Forked repo is not found and forkCreation is disabled'); throw new Error(error_messages_1.REPOSITORY_FORK_MISSING); } } const parsedEndpoint = node_url_1.default.parse(platformConfig.endpoint); if (forkToken) { logger_1.logger.debug('Using forkToken for git init'); parsedEndpoint.auth = (0, coerce_1.coerceToNull)(config.forkToken); } /* v8 ignore start */ else { const tokenType = opts.token?.startsWith('x-access-token:') ? 'app' : 'personal access'; logger_1.logger.debug(`Using ${tokenType} token for git init`); parsedEndpoint.auth = opts.token ?? null; } /* v8 ignore stop */ // TODO: null checks (#22198) parsedEndpoint.host = parsedEndpoint.host.replace('api.github.com', 'github.com'); parsedEndpoint.pathname = `${config.repository}.git`; const url = node_url_1.default.format(parsedEndpoint); let upstreamUrl = undefined; if (forkCreation && config.parentRepo) { parsedEndpoint.pathname = config.parentRepo + '.git'; upstreamUrl = node_url_1.default.format(parsedEndpoint); } await git.initRepo({ ...config, url, upstreamUrl, }); const repoConfig = { defaultBranch: config.defaultBranch, isFork: repo.isFork === true, repoFingerprint: (0, util_1.repoFingerprint)(repo.id, platformConfig.endpoint), }; return repoConfig; } async function getBranchForceRebase(branchName) { config.branchForceRebase ??= {}; if (config.branchForceRebase[branchName] === undefined) { try { config.branchForceRebase[branchName] = false; const branchProtection = await getBranchProtection(branchName); logger_1.logger.debug(`Found branch protection for branch ${branchName}`); if (branchProtection?.required_status_checks?.strict) { logger_1.logger.debug(`Branch protection: PRs must be up-to-date before merging for ${branchName}`); config.branchForceRebase[branchName] = true; } } catch (err) { if (err.statusCode === 404) { logger_1.logger.debug(`No branch protection found for ${branchName}`); } else if (err.message === error_messages_1.PLATFORM_INTEGRATION_UNAUTHORIZED || err.statusCode === 403) { logger_1.logger.once.debug('Branch protection: Do not have permissions to detect branch protection'); } else { throw err; } } } return !!config.branchForceRebase[branchName]; } function cachePr(pr) { config.prList ??= []; if (pr) { (0, pr_1.updatePrCache)(pr); for (let idx = 0; idx < config.prList.length; idx += 1) { const cachedPr = config.prList[idx]; if (cachedPr.number === pr.number) { config.prList[idx] = pr; return; } } config.prList.push(pr); } } // Fetch fresh Pull Request and cache it when possible async function fetchPr(prNo) { try { const { body: ghRestPr } = await common_2.githubApi.getJsonUnchecked(`repos/${config.parentRepo ?? config.repository}/pulls/${prNo}`); const result = (0, common_2.coerceRestPr)(ghRestPr); cachePr(result); return result; } catch (err) { logger_1.logger.warn({ err, prNo }, `GitHub fetchPr error`); return null; } } // Gets details for a PR async function getPr(prNo) { if (!prNo) { return null; } const prList = await getPrList(); let pr = prList.find(({ number }) => number === prNo) ?? null; if (pr) { logger_1.logger.debug('Returning PR from cache'); } pr ??= await fetchPr(prNo); return pr; } function matchesState(state, desiredState) { if (desiredState === 'all') { return true; } if (desiredState.startsWith('!')) { return state !== desiredState.substring(1); } return state === desiredState; } async function getPrList() { if (!config.prList) { const repo = config.parentRepo ?? config.repository; const username = !config.forkToken && !config.ignorePrAuthor && config.renovateUsername ? config.renovateUsername : null; // TODO: check null `repo` (#22198) const prCache = await (0, pr_1.getPrCache)(common_2.githubApi, repo, username); config.prList = Object.values(prCache).sort(({ number: a }, { number: b }) => b - a); } return config.prList; } async function findPr({ branchName, prTitle, state = 'all', includeOtherAuthors, }) { logger_1.logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`); if (includeOtherAuthors) { const repo = config.parentRepo ?? config.repository; const org = repo?.split('/')[0]; // PR might have been created by anyone, so don't use the cached Renovate PR list const { body: prList } = await common_2.githubApi.getJsonUnchecked(`repos/${repo}/pulls?head=${org}:${branchName}&state=open`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider }); if (!prList.length) { logger_1.logger.debug(`No PR found for branch ${branchName}`); return null; } return (0, common_2.coerceRestPr)(prList[0]); } const prList = await getPrList(); const pr = prList.find((p) => { if (p.sourceBranch !== branchName) { return false; } if (prTitle && prTitle.toUpperCase() !== p.title.toUpperCase()) { return false; } if (!matchesState(p.state, state)) { return false; } if (!config.forkToken && !(0, string_1.looseEquals)(config.repository, p.sourceRepo)) { return false; } return true; }); if (pr) { logger_1.logger.debug(`Found PR #${pr.number}`); } return pr ?? null; } async function ensureBranchSha(branchName, sha) { const repository = config.repository; try { const commitUrl = `/repos/${repository}/git/commits/${sha}`; await common_2.githubApi.head(commitUrl, { memCache: false }); } catch (err) { logger_1.logger.error({ err, sha, branchName }, 'Commit not found'); throw err; } const refUrl = `/repos/${config.repository}/git/refs/heads/${branchName}`; const branchExists = await (0, branch_1.remoteBranchExists)(repository, branchName); if (branchExists) { try { await common_2.githubApi.patchJson(refUrl, { body: { sha, force: true } }); return; } catch (err) { if (err.err?.response?.statusCode === 422) { logger_1.logger.debug({ err }, 'Branch update failed due to reference not existing - will try to create'); } else { logger_1.logger.warn({ refUrl, err }, 'Error updating branch'); throw err; } } } await common_2.githubApi.postJson(`/repos/${repository}/git/refs`, { body: { sha, ref: `refs/heads/${branchName}` }, }); } // Returns the Pull Request for a branch. Null if not exists. async function getBranchPr(branchName) { logger_1.logger.debug(`getBranchPr(${branchName})`); const openPr = await findPr({ branchName, state: 'open', }); if (openPr) { return openPr; } return null; } async function tryReuseAutoclosedPr(autoclosedPr) { const { sha, number, sourceBranch: branchName } = autoclosedPr; try { await ensureBranchSha(branchName, sha); logger_1.logger.debug(`Recreated autoclosed branch ${branchName} with sha ${sha}`); } catch (err) { logger_1.logger.debug({ err, branchName, sha, autoclosedPr }, 'Could not recreate autoclosed branch - skipping reopen'); return null; } try { const title = autoclosedPr.title.replace((0, regex_1.regEx)(/ - autoclosed$/), ''); const { body: ghPr } = await common_2.githubApi.patchJson(`repos/${config.repository}/pulls/${number}`, { body: { state: 'open', title, }, }); logger_1.logger.info({ branchName, title, number }, 'Successfully reopened autoclosed PR'); const result = (0, common_2.coerceRestPr)(ghPr); cachePr(result); return result; } catch { logger_1.logger.debug('Could not reopen autoclosed PR'); return null; } } async function getStatus(branchName, useCache = true) { const branch = escapeHash(branchName); const url = `repos/${config.repository}/commits/${branch}/status`; const { body: status } = await common_2.githubApi.getJsonUnchecked(url, { memCache: useCache, cacheProvider: repository_http_cache_provider_1.repoCacheProvider, }); return status; } // Returns the combined status for a branch. async function getBranchStatus(branchName, internalChecksAsSuccess) { logger_1.logger.debug(`getBranchStatus(${branchName})`); let commitStatus; try { commitStatus = await getStatus(branchName); } catch (err) /* v8 ignore start */ { if (err.statusCode === 404) { logger_1.logger.debug('Received 404 when checking branch status, assuming that branch has been deleted'); throw new Error(error_messages_1.REPOSITORY_CHANGED); } logger_1.logger.debug('Unknown error when checking branch status'); throw err; } /* v8 ignore stop */ logger_1.logger.debug({ state: commitStatus.state, statuses: commitStatus.statuses }, 'branch status check result'); if (commitStatus.statuses && !internalChecksAsSuccess) { commitStatus.statuses = commitStatus.statuses.filter((status) => status.state !== 'success' || !status.context?.startsWith('renovate/')); if (!commitStatus.statuses.length) { logger_1.logger.debug('Successful checks are all internal renovate/ checks, so returning "pending" branch status'); commitStatus.state = 'pending'; } } let checkRuns = []; // API is supported in oldest available GHE version 2.19 try { const checkRunsUrl = `repos/${config.repository}/commits/${escapeHash(branchName)}/check-runs?per_page=100`; const opts = { headers: { accept: 'application/vnd.github.antiope-preview+json', }, paginate: true, paginationField: 'check_runs', cacheProvider: memory_http_cache_provider_1.memCacheProvider, }; const checkRunsRaw = (await common_2.githubApi.getJsonUnchecked(checkRunsUrl, opts)).body; if (checkRunsRaw.check_runs?.length) { checkRuns = checkRunsRaw.check_runs.map((run) => ({ name: run.name, status: run.status, conclusion: run.conclusion, })); logger_1.logger.debug({ checkRuns }, 'check runs result'); } /* v8 ignore start */ else { logger_1.logger.debug({ result: checkRunsRaw }, 'No check runs found'); } /* v8 ignore stop */ } catch (err) /* v8 ignore start */ { if (err instanceof external_host_error_1.ExternalHostError) { throw err; } if (err.statusCode === 403 || err.message === error_messages_1.PLATFORM_INTEGRATION_UNAUTHORIZED) { logger_1.logger.debug('No permission to view check runs'); } else { logger_1.logger.warn({ err }, 'Error retrieving check runs'); } } /* v8 ignore stop */ if (checkRuns.length === 0) { if (commitStatus.state === 'success') { return 'green'; } if (commitStatus.state === 'failure') { return 'red'; } return 'yellow'; } if (commitStatus.state === 'failure' || checkRuns.some((run) => run.conclusion === 'failure')) { return 'red'; } if ((commitStatus.state === 'success' || commitStatus.statuses.length === 0) && checkRuns.every((run) => ['skipped', 'neutral', 'success'].includes(run.conclusion))) { return 'green'; } return 'yellow'; } async function getStatusCheck(branchName, useCache = true) { const branchCommit = git.getBranchCommit(branchName); const url = `repos/${config.repository}/commits/${branchCommit}/statuses`; const opts = useCache ? { cacheProvider: memory_http_cache_provider_1.memCacheProvider } : { memCache: false }; return (await common_2.githubApi.getJsonUnchecked(url, opts)).body; } const githubToRenovateStatusMapping = { success: 'green', error: 'red', failure: 'red', pending: 'yellow', }; async function getBranchStatusCheck(branchName, context) { try { const res = await getStatusCheck(branchName); for (const check of res) { if (check.context === context) { return githubToRenovateStatusMapping[check.state] || 'yellow'; } } return null; } catch (err) /* v8 ignore start */ { if (err.statusCode === 404) { logger_1.logger.debug('Commit not found when checking statuses'); throw new Error(error_messages_1.REPOSITORY_CHANGED); } throw err; } /* v8 ignore stop */ } async function setBranchStatus({ branchName, context, description, state, url: targetUrl, }) { /* v8 ignore start */ if (config.parentRepo) { logger_1.logger.debug('Cannot set branch status when in forking mode'); return; } /* v8 ignore stop */ const existingStatus = await getBranchStatusCheck(branchName, context); if (existingStatus === state) { return; } logger_1.logger.debug({ branch: branchName, context, state }, 'Setting branch status'); let url; try { const branchCommit = git.getBranchCommit(branchName); url = `repos/${config.repository}/statuses/${branchCommit}`; const renovateToGitHubStateMapping = { green: 'success', yellow: 'pending', red: 'failure', }; const options = { state: renovateToGitHubStateMapping[state], description, context, }; if (targetUrl) { options.target_url = targetUrl; } await common_2.githubApi.postJson(url, { body: options }); // update status cache await getStatus(branchName, false); await getStatusCheck(branchName, false); } catch (err) /* v8 ignore start */ { logger_1.logger.debug({ err, url }, 'Caught error setting branch status - aborting'); throw new Error(error_messages_1.REPOSITORY_CHANGED); } /* v8 ignore stop */ } // Issue async function getIssues() { const result = await common_2.githubApi.queryRepoField(graphql_1.getIssuesQuery, 'issues', { variables: { owner: config.repositoryOwner, name: config.repositoryName, ...(!config.ignorePrAuthor && { user: config.renovateUsername }), }, readOnly: true, }); logger_1.logger.debug(`Retrieved ${result.length} issues`); return issue_1.GithubIssue.array().parse(result); } async function getIssueList() { /* v8 ignore start */ if (config.hasIssuesEnabled === false) { return []; } /* v8 ignore stop */ let issueList = issue_1.GithubIssueCache.getIssues(); if (!issueList) { logger_1.logger.debug('Retrieving issueList'); issueList = await getIssues(); issue_1.GithubIssueCache.setIssues(issueList); } return issueList; } async function getIssue(number) { if (config.hasIssuesEnabled === false) { return null; } try { const repo = config.parentRepo ?? config.repository; const { body: issue } = await common_2.githubApi.getJson(`repos/${repo}/issues/${number}`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider, }, issue_1.GithubIssue); issue_1.GithubIssueCache.updateIssue(issue); return issue; } catch (err) { logger_1.logger.debug({ err, number }, 'Error getting issue'); if (err.response?.statusCode === 410) { logger_1.logger.debug(`Issue #${number} has been deleted`); issue_1.GithubIssueCache.deleteIssue(number); } return null; } } async function findIssue(title) { logger_1.logger.debug(`findIssue(${title})`); const [issue] = (await getIssueList()).filter((i) => i.state === 'open' && i.title === title); if (!issue) { return null; } logger_1.logger.debug(`Found issue ${issue.number}`); return getIssue(issue.number); } async function closeIssue(issueNumber) { logger_1.logger.debug(`closeIssue(${issueNumber})`); const repo = config.parentRepo ?? config.repository; const { body: closedIssue } = await common_2.githubApi.patchJson(`repos/${repo}/issues/${issueNumber}`, { body: { state: 'closed' } }, issue_1.GithubIssue); issue_1.GithubIssueCache.updateIssue(closedIssue); } async function ensureIssue({ title, reuseTitle, body: rawBody, labels, once = false, shouldReOpen = true, }) { logger_1.logger.debug(`ensureIssue(${title})`); /* v8 ignore start */ if (config.hasIssuesEnabled === false) { logger_1.logger.info('Cannot ensure issue because issues are disabled in this repository'); return null; } /* v8 ignore stop */ const body = (0, sanitize_1.sanitize)(rawBody); try { const issueList = await getIssueList(); let issues = issueList.filter((i) => i.title === title); if (!issues.length) { issues = issueList.filter((i) => i.title === reuseTitle); if (issues.length) { logger_1.logger.debug(`Reusing issue title: "${reuseTitle}"`); } } if (issues.length) { let issue = issues.find((i) => i.state === 'open'); if (!issue) { if (once) { logger_1.logger.debug('Issue already closed - skipping recreation'); return null; } if (shouldReOpen) { logger_1.logger.debug('Reopening previously closed issue'); } issue = issues[issues.length - 1]; } for (const i of issues) { if (i.state === 'open' && i.number !== issue.number) { logger_1.logger.warn({ issueNo: i.number }, 'Closing duplicate issue'); await closeIssue(i.number); } } const repo = config.parentRepo ?? config.repository; const { body: serverIssue } = await common_2.githubApi.getJson(`repos/${repo}/issues/${issue.number}`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider }, issue_1.GithubIssue); issue_1.GithubIssueCache.updateIssue(serverIssue); if (issue.title === title && serverIssue.body === body && issue.state === 'open') { logger_1.logger.debug('Issue is open and up to date - nothing to do'); return null; } if (shouldReOpen) { logger_1.logger.debug('Patching issue'); const data = { body, state: 'open', title }; if (labels) { data.labels = labels; } const repo = config.parentRepo ?? config.repository; const { body: updatedIssue } = await common_2.githubApi.patchJson(`repos/${repo}/issues/${issue.number}`, { body: data }, issue_1.GithubIssue); issue_1.GithubIssueCache.updateIssue(updatedIssue); logger_1.logger.debug('Issue updated'); return 'updated'; } } const { body: createdIssue } = await common_2.githubApi.postJson(`repos/${config.parentRepo ?? config.repository}/issues`, { body: { title, body, labels: labels ?? [], }, }, issue_1.GithubIssue); logger_1.logger.info('Issue created'); // reset issueList so that it will be fetched again as-needed issue_1.GithubIssueCache.updateIssue(createdIssue); return 'created'; } catch (err) /* v8 ignore start */ { if (err.body?.message?.startsWith('Issues are disabled for this repo')) { logger_1.logger.debug(`Issues are disabled, so could not create issue: ${title}`); } else { logger_1.logger.warn({ err }, 'Could not ensure issue'); } } /* v8 ignore stop */ return null; } async function ensureIssueClosing(title) { logger_1.logger.trace(`ensureIssueClosing(${title})`); /* v8 ignore start */ if (config.hasIssuesEnabled === false) { return; } /* v8 ignore stop */ const issueList = await getIssueList(); for (const issue of issueList) { if (issue.state === 'open' && issue.title === title) { await closeIssue(issue.number); logger_1.logger.debug(`Issue closed, issueNo: ${issue.number}`); } } } async function tryAddMilestone(issueNo, milestoneNo) { if (!milestoneNo) { return; } logger_1.logger.debug({ milestone: milestoneNo, pr: issueNo, }, 'Adding milestone to PR'); try { const repo = config.parentRepo ?? config.repository; const { body: updatedIssue } = await common_2.githubApi.patchJson(`repos/${repo}/issues/${issueNo}`, { body: { milestone: milestoneNo } }, issue_1.GithubIssue); issue_1.GithubIssueCache.updateIssue(updatedIssue); } catch (err) { /* v8 ignore next */ const actualError = err.response?.body ?? err; logger_1.logger.warn({ milestone: milestoneNo, pr: issueNo, err: actualError, }, 'Unable to add milestone to PR'); } } async function addAssignees(issueNo, assignees) { logger_1.logger.debug(`Adding assignees '${assignees.join(', ')}' to #${issueNo}`); const repository = config.parentRepo ?? config.repository; const { body: updatedIssue } = await common_2.githubApi.postJson(`repos/${repository}/issues/${issueNo}/assignees`, { body: { assignees } }, issue_1.GithubIssue); issue_1.GithubIssueCache.updateIssue(updatedIssue); } async function addReviewers(prNo, reviewers) { logger_1.logger.debug(`Adding reviewers '${reviewers.join(', ')}' to #${prNo}`); const userReviewers = reviewers.filter((e) => !e.startsWith('team:')); const teamReviewers = reviewers .filter((e) => e.startsWith('team:')) .map((e) => e.replace((0, regex_1.regEx)(/^team:/), '')); try { await common_2.githubApi.postJson(`repos/${config.parentRepo ?? config.repository}/pulls/${prNo}/requested_reviewers`, { body: { reviewers: userReviewers, team_reviewers: teamReviewers, }, }); } catch (err) /* v8 ignore start */ { logger_1.logger.warn({ err }, 'Failed to assign reviewer'); } /* v8 ignore stop */ } async function addLabels(issueNo, labels) { logger_1.logger.debug(`Adding labels '${labels?.join(', ')}' to #${issueNo}`); try { const repository = config.parentRepo ?? config.repository; if (is_1.default.array(labels) && labels.length) { await common_2.githubApi.postJson(`repos/${repository}/issues/${issueNo}/labels`, { body: labels, }); } } catch (err) /* v8 ignore start */ { logger_1.logger.warn({ err, issueNo, labels }, 'Error while adding labels. Skipping'); } /* v8 ignore stop */ } async function deleteLabel(issueNo, label) { logger_1.logger.debug(`Deleting label ${label} from #${issueNo}`); const repository = config.parentRepo ?? config.repository; try { await common_2.githubApi.deleteJson(`repos/${repository}/issues/${issueNo}/labels/${label}`); } catch (err) /* v8 ignore start */ { logger_1.logger.warn({ err, issueNo, label }, 'Failed to delete label'); } /* v8 ignore stop */ } async function addComment(issueNo, body) { // POST /repos/:owner/:repo/issues/:number/comments await common_2.githubApi.postJson(`repos/${config.parentRepo ?? config.repository}/issues/${issueNo}/comments`, { body: { body }, }); } async function editComment(commentId, body) { // PATCH /repos/:owner/:repo/issues/comments/:id await common_2.githubApi.patchJson(`repos/${config.parentRepo ?? config.repository}/issues/comments/${commentId}`, { body: { body }, }); } async function deleteComment(commentId) { // DELETE /repos/:owner/:repo/issues/comments/:id await common_2.githubApi.deleteJson(`repos/${config.parentRepo ?? config.repository}/issues/comments/${commentId}`); } async function getComments(issueNo) { // GET /repos/:owner/:repo/issues/:number/comments logger_1.logger.debug(`Getting comments for #${issueNo}`); const repo = config.parentRepo ?? config.repository; const url = `repos/${repo}/issues/${issueNo}/comments?per_page=100`; try { const { body: comments } = await common_2.githubApi.getJsonUnchecked(url, { paginate: true, cacheProvider: repository_http_cache_provider_1.repoCacheProvider, }); logger_1.logger.debug(`Found ${comments.length} comments`); return comments; } catch (err) /* v8 ignore start */ { if (err.statusCode === 404) {