UNPKG

renovate

Version:

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

814 lines • 34.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.id = void 0; exports.initPlatform = initPlatform; exports.getRepos = getRepos; exports.getRawFile = getRawFile; exports.getJsonFile = getJsonFile; exports.initRepo = initRepo; exports.getBranchForceRebase = getBranchForceRebase; exports.getPr = getPr; exports.getPrList = getPrList; exports.findPr = findPr; exports.getBranchPr = getBranchPr; exports.refreshPr = refreshPr; exports.getBranchStatus = getBranchStatus; exports.getBranchStatusCheck = getBranchStatusCheck; exports.setBranchStatus = setBranchStatus; exports.findIssue = findIssue; exports.ensureIssue = ensureIssue; exports.getIssueList = getIssueList; exports.ensureIssueClosing = ensureIssueClosing; exports.addAssignees = addAssignees; exports.addReviewers = addReviewers; exports.deleteLabel = deleteLabel; exports.ensureComment = ensureComment; exports.ensureCommentRemoval = ensureCommentRemoval; exports.createPr = createPr; exports.updatePr = updatePr; exports.mergePr = mergePr; exports.massageMarkdown = massageMarkdown; exports.maxBodyLength = maxBodyLength; const tslib_1 = require("tslib"); const promises_1 = require("timers/promises"); const semver_1 = tslib_1.__importDefault(require("semver")); const error_messages_1 = require("../../../constants/error-messages"); const logger_1 = require("../../../logger"); 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 bitbucket_server_1 = require("../../../util/http/bitbucket-server"); const memory_http_cache_provider_1 = require("../../../util/http/cache/memory-http-cache-provider"); const regex_1 = require("../../../util/regex"); const sanitize_1 = require("../../../util/sanitize"); const url_1 = require("../../../util/url"); const util_1 = require("../util"); const pr_body_1 = require("../utils/pr-body"); const pr_cache_1 = require("./pr-cache"); const schema_1 = require("./schema"); const utils = tslib_1.__importStar(require("./utils")); const utils_1 = require("./utils"); /* * Version: 5.3 (EOL Date: 15 Aug 2019) * See following docs for api information: * https://docs.atlassian.com/bitbucket-server/rest/5.3.0/bitbucket-rest.html * https://docs.atlassian.com/bitbucket-server/rest/5.3.0/bitbucket-build-rest.html * * See following page for uptodate supported versions * https://confluence.atlassian.com/support/atlassian-support-end-of-life-policy-201851003.html#AtlassianSupportEndofLifePolicy-BitbucketServer */ exports.id = 'bitbucket-server'; let config = {}; const bitbucketServerHttp = new bitbucket_server_1.BitbucketServerHttp(); const defaults = { hostType: 'bitbucket-server', version: '0.0.0', }; /* v8 ignore start */ function updatePrVersion(pr, version) { const res = Math.max(config.prVersions.get(pr) ?? 0, version); config.prVersions.set(pr, res); return res; } /* v8 ignore stop */ async function initPlatform({ endpoint, token, username, password, gitAuthor, }) { if (!endpoint) { throw new Error('Init: You must configure a Bitbucket Server endpoint'); } if (!(username && password) && !token) { throw new Error('Init: You must either configure a Bitbucket Server username/password or a HTTP access token'); } else if (password && token) { throw new Error('Init: You must configure either a Bitbucket Server password or a HTTP access token, not both'); } // TODO: Add a connection check that endpoint/username/password combination are valid (#9595) defaults.endpoint = (0, url_1.ensureTrailingSlash)(endpoint); (0, bitbucket_server_1.setBaseUrl)(defaults.endpoint); const platformConfig = { endpoint: defaults.endpoint, }; try { let bitbucketServerVersion; const env = (0, env_1.getEnv)(); /* v8 ignore start: experimental feature */ if (env.RENOVATE_X_PLATFORM_VERSION) { bitbucketServerVersion = env.RENOVATE_X_PLATFORM_VERSION; } /* v8 ignore stop */ else { const { version } = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/application-properties`)).body; bitbucketServerVersion = version; logger_1.logger.debug('Bitbucket Server version is: ' + bitbucketServerVersion); } if (semver_1.default.valid(bitbucketServerVersion)) { defaults.version = bitbucketServerVersion; } } catch (err) { logger_1.logger.debug({ err }, 'Error authenticating with Bitbucket. Check that your token includes "api" permissions'); } if (!gitAuthor && username) { logger_1.logger.debug(`Attempting to confirm gitAuthor from username`); const options = { memCache: false, }; if (token) { options.token = token; } else { options.username = username; options.password = password; } try { const { displayName, emailAddress } = (await bitbucketServerHttp.getJson(`./rest/api/1.0/users/${username}`, options, schema_1.UserSchema)).body; if (!emailAddress.length) { throw new Error(`No email address configured for username ${username}`); } platformConfig.gitAuthor = `${displayName} <${emailAddress}>`; logger_1.logger.debug(`Detected gitAuthor: ${platformConfig.gitAuthor}`); } catch (err) { logger_1.logger.debug({ err }, 'Failed to get user info, fallback gitAuthor will be used'); } } return platformConfig; } // Get all repositories that the user has access to async function getRepos() { logger_1.logger.debug('Autodiscovering Bitbucket Server repositories'); try { const repos = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/repos?permission=REPO_WRITE&state=AVAILABLE`, { paginate: true })).body; const result = repos.map((repo) => `${repo.project.key}/${repo.slug}`); logger_1.logger.debug({ result }, 'result of getRepos()'); return result; } catch (err) /* v8 ignore start */ { logger_1.logger.error({ err }, `bitbucket getRepos error`); throw err; } /* v8 ignore stop */ } async function getRawFile(fileName, repoName, branchOrTag) { const repo = repoName ?? config.repository; const [project, slug] = repo.split('/'); const fileUrl = `./rest/api/1.0/projects/${project}/repos/${slug}/browse/${fileName}?limit=20000` + (branchOrTag ? '&at=' + branchOrTag : ''); const res = await bitbucketServerHttp.getJsonUnchecked(fileUrl); const { isLastPage, lines, size } = res.body; if (isLastPage) { return lines.map(({ text }) => text).join('\n'); } logger_1.logger.warn({ size }, 'The file is too big'); throw new Error(`The file is too big (${size}B)`); } async function getJsonFile(fileName, repoName, branchOrTag) { // TODO #22198 const raw = await getRawFile(fileName, repoName, branchOrTag); return (0, common_1.parseJson)(raw, fileName); } // Initialize Bitbucket Server by getting base branch async function initRepo({ repository, cloneSubmodules, cloneSubmodulesFilter, ignorePrAuthor, gitUrl, }) { logger_1.logger.debug(`initRepo("${JSON.stringify({ repository }, null, 2)}")`); const opts = hostRules.find({ hostType: defaults.hostType, url: defaults.endpoint, }); const [projectKey, repositorySlug] = repository.split('/'); config = { projectKey, repositorySlug, repository, prVersions: new Map(), username: opts.username, ignorePrAuthor, }; try { const info = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}`)).body; config.owner = info.project.key; logger_1.logger.debug(`${repository} owner = ${config.owner}`); const branchRes = await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/branches/default`); // 204 means empty, 404 means repo not found or missing default branch. repo must exist here. if ([204, 404].includes(branchRes.statusCode)) { throw new Error(error_messages_1.REPOSITORY_EMPTY); } const url = utils.getRepoGitUrl(config.repositorySlug, // TODO #22198 defaults.endpoint, gitUrl, info, opts); await git.initRepo({ ...config, url, extraCloneOpts: (0, utils_1.getExtraCloneOpts)(opts), cloneSubmodules, cloneSubmodulesFilter, fullClone: semver_1.default.lte(defaults.version, '8.0.0'), }); config.mergeMethod = 'merge'; const repoConfig = { defaultBranch: branchRes.body.displayId, isFork: !!info.origin, repoFingerprint: (0, util_1.repoFingerprint)(info.id, defaults.endpoint), }; return repoConfig; } catch (err) /* v8 ignore start */ { if (err.statusCode === 404) { throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } if (err.message === error_messages_1.REPOSITORY_EMPTY) { throw err; } logger_1.logger.debug({ err }, 'Unknown Bitbucket initRepo error'); throw err; } /* v8 ignore stop */ } async function getBranchForceRebase(_branchName) { // https://docs.atlassian.com/bitbucket-server/rest/7.0.1/bitbucket-rest.html#idp342 const res = await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/settings/pull-requests`); // If the default merge strategy contains `ff-only` the PR can only be merged // if it is up to date with the base branch. // The current options for id are: // no-ff, ff, ff-only, rebase-no-ff, rebase-ff-only, squash, squash-ff-only return Boolean(res.body?.mergeConfig?.defaultStrategy?.id.includes('ff-only')); } // Gets details for a PR async function getPr(prNo, refreshCache) { logger_1.logger.debug(`getPr(${prNo})`); if (!prNo) { return null; } // Disables memCache (which is enabled by default) to be replaced by // memCacheProvider. const opts = { memCache: false }; // TODO: should refresh the cache rather than just ignore it if (!refreshCache) { opts.cacheProvider = memory_http_cache_provider_1.memCacheProvider; } const res = await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}`, opts); const pr = { ...utils.prInfo(res.body), reviewers: res.body.reviewers.map((r) => r.user.name), }; // TODO #22198 pr.version = updatePrVersion(pr.number, pr.version); return pr; } // TODO: coverage (#9624) /* v8 ignore start */ function matchesState(state, desiredState) { if (desiredState === 'all') { return true; } if (desiredState.startsWith('!')) { return state !== desiredState.substring(1); } return state === desiredState; } /* v8 ignore stop */ // TODO: coverage (#9624) /* v8 ignore start */ function isRelevantPr(branchName, prTitle, state) { return (p) => p.sourceBranch === branchName && (!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) && matchesState(p.state, state); } /* v8 ignore stop */ // TODO: coverage (#9624) async function getPrList() { logger_1.logger.debug(`getPrList()`); return await pr_cache_1.BbsPrCache.getPrs(bitbucketServerHttp, config.projectKey, config.repositorySlug, config.ignorePrAuthor, config.username); } // TODO: coverage (#9624) /* v8 ignore start */ async function findPr({ branchName, prTitle, state = 'all', includeOtherAuthors, }) { logger_1.logger.debug(`findPr(${branchName}, "${prTitle}", "${state}")`); if (includeOtherAuthors) { // PR might have been created by anyone, so don't use the cached Renovate PR list const searchParams = { state: 'OPEN', }; searchParams.direction = 'outgoing'; searchParams.at = `refs/heads/${branchName}`; const query = (0, url_1.getQueryString)(searchParams); const prs = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests?${query}`, { paginate: true, limit: 1, // only fetch the latest pr })).body; if (!prs.length) { logger_1.logger.debug(`No PR found for branch ${branchName}`); return null; } return utils.prInfo(prs[0]); } const prList = await getPrList(); const pr = prList.find(isRelevantPr(branchName, prTitle, state)); if (pr) { logger_1.logger.debug(`Found PR #${pr.number}`); } else { logger_1.logger.debug(`Renovate did not find a PR for branch #${branchName}`); } return pr ?? null; } /* v8 ignore stop */ // Returns the Pull Request for a branch. Null if not exists. async function getBranchPr(branchName) { logger_1.logger.debug(`getBranchPr(${branchName})`); const existingPr = await findPr({ branchName, state: 'open', }); return existingPr ? getPr(existingPr.number) : null; } /* v8 ignore start */ async function refreshPr(number) { // wait for pr change propagation await (0, promises_1.setTimeout)(1000); // refresh cache await getPr(number, true); } /* v8 ignore stop */ async function getStatus(branchName, memCache = true) { const branchCommit = git.getBranchCommit(branchName); /* v8 ignore start: temporary code */ const opts = memCache ? { cacheProvider: memory_http_cache_provider_1.memCacheProvider } : { memCache: false }; /* v8 ignore stop */ return (await bitbucketServerHttp.getJsonUnchecked( // TODO: types (#22198) `./rest/build-status/1.0/commits/stats/${branchCommit}`, opts)).body; } // Returns the combined status for a branch. // umbrella for status checks // https://docs.atlassian.com/bitbucket-server/rest/6.0.0/bitbucket-build-rest.html#idp2 async function getBranchStatus(branchName) { logger_1.logger.debug(`getBranchStatus(${branchName})`); if (!git.branchExists(branchName)) { logger_1.logger.debug('Branch does not exist - cannot fetch status'); throw new Error(error_messages_1.REPOSITORY_CHANGED); } try { const commitStatus = await getStatus(branchName); logger_1.logger.debug({ commitStatus }, 'branch status check result'); if (commitStatus.failed > 0) { return 'red'; } if (commitStatus.inProgress > 0) { return 'yellow'; } return commitStatus.successful > 0 ? 'green' : 'yellow'; } catch (err) { logger_1.logger.warn({ err }, `Failed to get branch status`); return 'red'; } } async function getStatusCheck(branchName, memCache = true) { const branchCommit = git.getBranchCommit(branchName); const opts = { paginate: true }; /* v8 ignore start: temporary code */ if (memCache) { opts.cacheProvider = memory_http_cache_provider_1.memCacheProvider; } else { opts.memCache = false; } /* v8 ignore stop */ return (await bitbucketServerHttp.getJsonUnchecked(`./rest/build-status/1.0/commits/${branchCommit}`, opts)).body; } // https://docs.atlassian.com/bitbucket-server/rest/6.0.0/bitbucket-build-rest.html#idp2 async function getBranchStatusCheck(branchName, context) { logger_1.logger.debug(`getBranchStatusCheck(${branchName}, context=${context})`); try { const states = await getStatusCheck(branchName); for (const state of states) { if (state.key === context) { switch (state.state) { case 'SUCCESSFUL': return 'green'; case 'INPROGRESS': return 'yellow'; case 'FAILED': default: return 'red'; } } } } catch (err) { logger_1.logger.warn({ err }, `Failed to check branch status`); } return null; } async function setBranchStatus({ branchName, context, description, state, url: targetUrl, }) { logger_1.logger.debug(`setBranchStatus(${branchName})`); const existingStatus = await getBranchStatusCheck(branchName, context); if (existingStatus === state) { return; } logger_1.logger.debug({ branch: branchName, context, state }, 'Setting branch status'); const branchCommit = git.getBranchCommit(branchName); try { const body = { key: context, description, url: targetUrl ?? 'https://renovatebot.com', }; switch (state) { case 'green': body.state = 'SUCCESSFUL'; break; case 'yellow': body.state = 'INPROGRESS'; break; case 'red': default: body.state = 'FAILED'; break; } await bitbucketServerHttp.postJson( // TODO: types (#22198) `./rest/build-status/1.0/commits/${branchCommit}`, { body }); // update status cache await getStatus(branchName, false); await getStatusCheck(branchName, false); } catch (err) { logger_1.logger.warn({ err }, `Failed to set branch status`); } } // Issue /* v8 ignore start */ function findIssue(title) { logger_1.logger.debug(`findIssue(${title})`); // This is used by Renovate when creating its own issues, // e.g. for deprecated package warnings, // config error notifications, or "dependencyDashboard" // // Bitbucket Server does not have issues return Promise.resolve(null); } /* v8 ignore stop */ /* v8 ignore start */ function ensureIssue({ title, }) { logger_1.logger.warn({ title }, 'Cannot ensure issue'); // This is used by Renovate when creating its own issues, // e.g. for deprecated package warnings, // config error notifications, or "dependencyDashboard" // // Bitbucket Server does not have issues return Promise.resolve(null); } /* v8 ignore stop */ /* v8 ignore start */ function getIssueList() { logger_1.logger.debug(`getIssueList()`); // This is used by Renovate when creating its own issues, // e.g. for deprecated package warnings, // config error notifications, or "dependencyDashboard" // // Bitbucket Server does not have issues return Promise.resolve([]); } /* v8 ignore stop */ /* v8 ignore start */ function ensureIssueClosing(title) { logger_1.logger.debug(`ensureIssueClosing(${title})`); // This is used by Renovate when creating its own issues, // e.g. for deprecated package warnings, // config error notifications, or "dependencyDashboard" // // Bitbucket Server does not have issues return Promise.resolve(); } /* v8 ignore stop */ function addAssignees(iid, assignees) { logger_1.logger.debug(`addAssignees(${iid}, [${assignees.join(', ')}])`); // This is used by Renovate when creating its own issues, // e.g. for deprecated package warnings, // config error notifications, or "dependencyDashboard" // // Bitbucket Server does not have issues return Promise.resolve(); } async function addReviewers(prNo, reviewers) { logger_1.logger.debug(`Adding reviewers '${reviewers.join(', ')}' to #${prNo}`); await retry(updatePRAndAddReviewers, [prNo, reviewers], 3, [ error_messages_1.REPOSITORY_CHANGED, ]); } async function updatePRAndAddReviewers(prNo, reviewers) { try { const pr = await getPr(prNo); if (!pr) { throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } // TODO: can `reviewers` be undefined? (#22198) const reviewersSet = new Set([...pr.reviewers, ...reviewers]); await bitbucketServerHttp.putJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}`, { body: { title: pr.title, version: pr.version, reviewers: Array.from(reviewersSet).map((name) => ({ user: { name }, })), }, }); await getPr(prNo, true); } catch (err) { logger_1.logger.warn({ err, reviewers, prNo }, `Failed to add reviewers`); if (err.statusCode === 404) { throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } else if (err.statusCode === 409 && !utils.isInvalidReviewersResponse(err)) { logger_1.logger.debug('409 response to adding reviewers - has repository changed?'); throw new Error(error_messages_1.REPOSITORY_CHANGED); } else { throw err; } } } async function retry(fn, args, maxTries, retryErrorMessages) { const maxAttempts = Math.max(maxTries, 1); let lastError; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { return await fn(...args); } catch (e) { lastError = e; if (retryErrorMessages.length !== 0 && !retryErrorMessages.includes(e.message)) { logger_1.logger.debug(`Error not marked for retry`); throw e; } } } logger_1.logger.debug(`All ${maxAttempts} retry attempts exhausted`); // Can't be `undefined` here. // eslint-disable-next-line @typescript-eslint/only-throw-error throw lastError; } function deleteLabel(issueNo, label) { logger_1.logger.debug(`deleteLabel(${issueNo}, ${label})`); // Only used for the "request Renovate to rebase a PR using a label" feature // // Bitbucket Server does not have issues return Promise.resolve(); } async function getComments(prNo) { // GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/activities const activities = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}/activities`, { paginate: true })).body; const comments = activities .filter((a) => a.action === 'COMMENTED' && 'comment' in a && 'commentAction' in a) .filter((a) => a.commentAction === 'ADDED') .map((a) => a.comment); logger_1.logger.debug(`Found ${comments.length} comments`); return comments; } async function addComment(prNo, text) { // POST /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments await bitbucketServerHttp.postJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}/comments`, { body: { text }, }); } async function getCommentVersion(prNo, commentId) { // GET /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId} const { version } = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}/comments/${commentId}`)).body; return version; } async function editComment(prNo, commentId, text) { const version = await getCommentVersion(prNo, commentId); // PUT /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId} await bitbucketServerHttp.putJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}/comments/${commentId}`, { body: { text, version }, }); } async function deleteComment(prNo, commentId) { const version = await getCommentVersion(prNo, commentId); // DELETE /rest/api/1.0/projects/{projectKey}/repos/{repositorySlug}/pull-requests/{pullRequestId}/comments/{commentId} await bitbucketServerHttp.deleteJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}/comments/${commentId}?version=${version}`); } async function ensureComment({ number, topic, content, }) { const sanitizedContent = (0, sanitize_1.sanitize)(content); try { const comments = await getComments(number); let body; let commentId; let commentNeedsUpdating; if (topic) { logger_1.logger.debug(`Ensuring comment "${topic}" in #${number}`); body = `### ${topic}\n\n${sanitizedContent}`; comments.forEach((comment) => { if (comment.text.startsWith(`### ${topic}\n\n`)) { commentId = comment.id; commentNeedsUpdating = comment.text !== body; } }); } else { logger_1.logger.debug(`Ensuring content-only comment in #${number}`); body = `${sanitizedContent}`; comments.forEach((comment) => { if (comment.text === body) { commentId = comment.id; commentNeedsUpdating = false; } }); } if (!commentId) { await addComment(number, body); logger_1.logger.info({ repository: config.repository, prNo: number, topic }, 'Comment added'); } else if (commentNeedsUpdating) { await editComment(number, commentId, body); logger_1.logger.debug({ repository: config.repository, prNo: number }, 'Comment updated'); } else { logger_1.logger.debug('Comment is already update-to-date'); } return true; } catch (err) /* v8 ignore start */ { logger_1.logger.warn({ err }, 'Error ensuring comment'); return false; } /* v8 ignore stop */ } async function ensureCommentRemoval(deleteConfig) { try { const { number: prNo } = deleteConfig; const key = deleteConfig.type === 'by-topic' ? deleteConfig.topic : deleteConfig.content; logger_1.logger.debug(`Ensuring comment "${key}" in #${prNo} is removed`); const comments = await getComments(prNo); let commentId = null; if (deleteConfig.type === 'by-topic') { const byTopic = (comment) => comment.text.startsWith(`### ${deleteConfig.topic}\n\n`); commentId = comments.find(byTopic)?.id; } else if (deleteConfig.type === 'by-content') { const byContent = (comment) => comment.text.trim() === deleteConfig.content; commentId = comments.find(byContent)?.id; } if (commentId) { await deleteComment(prNo, commentId); } } catch (err) /* v8 ignore start */ { logger_1.logger.warn({ err }, 'Error ensuring comment removal'); } /* v8 ignore stop */ } // Pull Request const escapeHash = (input) => input?.replace((0, regex_1.regEx)(/#/g), '%23'); async function createPr({ sourceBranch, targetBranch, prTitle: title, prBody: rawDescription, platformPrOptions, }) { const description = (0, sanitize_1.sanitize)(rawDescription); logger_1.logger.debug(`createPr(${sourceBranch}, title=${title})`); const base = targetBranch; let reviewers = []; if (platformPrOptions?.bbUseDefaultReviewers) { logger_1.logger.debug(`fetching default reviewers`); const { id } = (await bitbucketServerHttp.getJsonUnchecked(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}`)).body; const defReviewers = (await bitbucketServerHttp.getJsonUnchecked(`./rest/default-reviewers/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/reviewers?sourceRefId=refs/heads/${escapeHash(sourceBranch)}&targetRefId=refs/heads/${base}&sourceRepoId=${id}&targetRepoId=${id}`)).body; reviewers = defReviewers.map((u) => ({ user: { name: u.name }, })); } const body = { title, description, fromRef: { id: `refs/heads/${sourceBranch}`, }, toRef: { id: `refs/heads/${base}`, }, reviewers, }; let prInfoRes; try { prInfoRes = await bitbucketServerHttp.postJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests`, { body }); } catch (err) /* v8 ignore start */ { if (err.body?.errors?.[0]?.exceptionName === 'com.atlassian.bitbucket.pull.EmptyPullRequestException') { logger_1.logger.debug('Empty pull request - deleting branch so it can be recreated next run'); await (0, git_1.deleteBranch)(sourceBranch); throw new Error(error_messages_1.REPOSITORY_CHANGED); } throw err; } /* v8 ignore stop */ const pr = { ...utils.prInfo(prInfoRes.body), }; // TODO #22198 updatePrVersion(pr.number, pr.version); await pr_cache_1.BbsPrCache.setPr(bitbucketServerHttp, config.projectKey, config.repositorySlug, config.ignorePrAuthor, config.username, pr); return pr; } async function updatePr({ number: prNo, prTitle: title, prBody: rawDescription, state, bitbucketInvalidReviewers, targetBranch, }) { const description = (0, sanitize_1.sanitize)(rawDescription); logger_1.logger.debug(`updatePr(${prNo}, title=${title})`); try { const pr = await getPr(prNo); if (!pr) { throw Object.assign(new Error(error_messages_1.REPOSITORY_NOT_FOUND), { statusCode: 404 }); } const body = { title, description, version: pr.version, reviewers: pr.reviewers ?.filter((name) => !bitbucketInvalidReviewers?.includes(name)) .map((name) => ({ user: { name } })), }; if (targetBranch) { body.toRef = { id: (0, util_1.getNewBranchName)(targetBranch), }; } const { body: updatedPr } = await bitbucketServerHttp.putJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}`, { body }); updatePrVersion(prNo, updatedPr.version); const currentState = updatedPr.state; // TODO #22198 const newState = { ['open']: 'OPEN', ['closed']: 'DECLINED', }[state]; let finalState = currentState === 'OPEN' ? 'open' : 'closed'; if (newState && ['OPEN', 'DECLINED'].includes(currentState) && currentState !== newState) { const command = state === 'open' ? 'reopen' : 'decline'; const { body: updatedStatePr } = await bitbucketServerHttp.postJson(`./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${pr.number}/${command}?version=${updatedPr.version}`); finalState = state; updatePrVersion(pr.number, updatedStatePr.version); } const bbsPr = utils.prInfo(updatedPr); await pr_cache_1.BbsPrCache.setPr(bitbucketServerHttp, config.projectKey, config.repositorySlug, config.ignorePrAuthor, config.username, { ...bbsPr, state: finalState }); } catch (err) { logger_1.logger.debug({ err, prNo }, `Failed to update PR`); if (err.statusCode === 404) { throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } else if (err.statusCode === 409) { if (utils.isInvalidReviewersResponse(err) && !bitbucketInvalidReviewers) { // Retry again with invalid reviewers being removed const invalidReviewers = utils.getInvalidReviewers(err); await updatePr({ number: prNo, prTitle: title, prBody: rawDescription, state, bitbucketInvalidReviewers: invalidReviewers, }); } else { throw new Error(error_messages_1.REPOSITORY_CHANGED); } } else { throw err; } } } // https://docs.atlassian.com/bitbucket-server/rest/6.0.0/bitbucket-rest.html#idp261 async function mergePr({ branchName, id: prNo, }) { logger_1.logger.debug(`mergePr(${prNo}, ${branchName})`); // Used for "automerge" feature try { const pr = await getPr(prNo); if (!pr) { throw Object.assign(new Error(error_messages_1.REPOSITORY_NOT_FOUND), { statusCode: 404 }); } const { body } = await bitbucketServerHttp.postJson( // TODO: types (#22198) `./rest/api/1.0/projects/${config.projectKey}/repos/${config.repositorySlug}/pull-requests/${prNo}/merge?version=${pr.version}`); updatePrVersion(prNo, body.version); } catch (err) { if (err.statusCode === 404) { throw new Error(error_messages_1.REPOSITORY_NOT_FOUND); } else if (err.statusCode === 409) { logger_1.logger.warn({ err }, `Failed to merge PR`); return false; } else { logger_1.logger.warn({ err }, `Failed to merge PR`); return false; } } logger_1.logger.debug(`PR merged, PrNo:${prNo}`); return true; } function massageMarkdown(input) { logger_1.logger.debug(`massageMarkdown(${input.split(regex_1.newlineRegex)[0]})`); // Remove any HTML we use return (0, pr_body_1.smartTruncate)(input, maxBodyLength()) .replace('you tick the rebase/retry checkbox', 'PR is renamed to start with "rebase!"') .replace('checking the rebase/retry box above', 'renaming the PR to start with "rebase!"') .replace((0, regex_1.regEx)(/<\/?summary>/g), '**') .replace((0, regex_1.regEx)(/<\/?details>/g), '') .replace((0, regex_1.regEx)(`\n---\n\n.*?<!-- rebase-check -->.*?(\n|$)`), '') .replace((0, regex_1.regEx)(/<!--.*?-->/gs), ''); } function maxBodyLength() { return 30000; } //# sourceMappingURL=index.js.map