UNPKG

renovate

Version:

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

655 lines (654 loc) • 27.8 kB
import { __exportAll } from "../../../_virtual/_rolldown/runtime.js"; import { REPOSITORY_NOT_FOUND } from "../../../constants/error-messages.js"; import { regEx } from "../../../util/regex.js"; import { GlobalConfig } from "../../../config/global.js"; import { UUIDRegex, matchRegexOrGlobList } from "../../../util/string-match.js"; import { sanitize } from "../../../util/sanitize.js"; import { logger } from "../../../logger/index.js"; import { parseUrl } from "../../../util/url.js"; import { find } from "../../../util/host-rules.js"; import { getInheritedOrGlobal, parseJson } from "../../../util/common.js"; import { BitbucketHttp, setBaseUrl } from "../../../util/http/bitbucket.js"; import { RepoInfo, Repositories, UnresolvedPrTasks, WorkspaceAccesses } from "./schema.js"; import { map } from "../../../util/promises.js"; import { memCacheProvider } from "../../../util/http/cache/memory-http-cache-provider.js"; import { getUrl, initRepo as initRepo$1 } from "../../../util/git/index.js"; import { aggressiveRepoCacheProvider, repoCacheProvider } from "../../../util/http/cache/repository-http-cache-provider.js"; import { repoFingerprint } from "../util.js"; import { smartTruncate } from "../utils/pr-body.js"; import readOnlyIssueBody from "../utils/read-only-issue-body.js"; import { REOPEN_PR_COMMENT_KEYWORD, ensureComment as ensureComment$1, ensureCommentRemoval as ensureCommentRemoval$1, reopenComments } from "./comments.js"; import { buildStates, mergeBodyTransformer, prInfo } from "./utils.js"; import { BitbucketPrCache } from "./pr-cache.js"; import { isNonEmptyArray, isNonEmptyString } from "@sindresorhus/is"; //#region lib/modules/platform/bitbucket/index.ts var bitbucket_exports = /* @__PURE__ */ __exportAll({ addAssignees: () => addAssignees, addReviewers: () => addReviewers, createPr: () => createPr, deleteLabel: () => deleteLabel, ensureComment: () => ensureComment, ensureCommentRemoval: () => ensureCommentRemoval, ensureIssue: () => ensureIssue, ensureIssueClosing: () => ensureIssueClosing, findIssue: () => findIssue, findPr: () => findPr, getBranchPr: () => getBranchPr, getBranchStatus: () => getBranchStatus, getBranchStatusCheck: () => getBranchStatusCheck, getIssueList: () => getIssueList, getJsonFile: () => getJsonFile, getPr: () => getPr, getPrList: () => getPrList, getRawFile: () => getRawFile, getRepos: () => getRepos, id: () => id, initPlatform: () => initPlatform, initRepo: () => initRepo, massageMarkdown: () => massageMarkdown, maxBodyLength: () => maxBodyLength, mergePr: () => mergePr, resetPlatform: () => resetPlatform, setBranchStatus: () => setBranchStatus, updatePr: () => updatePr }); const id = "bitbucket"; const bitbucketHttp = new BitbucketHttp(); const BITBUCKET_PROD_ENDPOINT = "https://api.bitbucket.org/"; let config = {}; function resetPlatform() { config = {}; renovateUserUuid = null; } const defaults = { endpoint: BITBUCKET_PROD_ENDPOINT }; const pathSeparator = "/"; let renovateUserUuid = null; async function initPlatform({ endpoint, username, password, token }) { if (!(username && password) && !token) throw new Error("Init: You must configure either a Bitbucket token or username and password"); if (endpoint && endpoint !== BITBUCKET_PROD_ENDPOINT) { logger.warn(`Init: Bitbucket Cloud endpoint should generally be ${BITBUCKET_PROD_ENDPOINT} but is being configured to a different value. Did you mean to use Bitbucket Server?`); defaults.endpoint = endpoint; } setBaseUrl(defaults.endpoint); renovateUserUuid = null; const options = { memCache: false }; if (token) options.token = token; else { options.username = username; options.password = password; } try { const { uuid } = (await bitbucketHttp.getJsonUnchecked("/2.0/user", options)).body; renovateUserUuid = uuid; } catch (err) { if (err.statusCode === 403 && err.body?.error?.detail?.required?.includes("account")) logger.warn(`Bitbucket: missing 'account' scope for password`); else logger.debug({ err }, "Unknown error fetching Bitbucket user identity"); } const platformConfig = { endpoint: endpoint ?? BITBUCKET_PROD_ENDPOINT }; return Promise.resolve(platformConfig); } async function getRepos(config) { logger.debug("Autodiscovering Bitbucket Cloud repositories"); try { let workspaceSlugs; const autodiscoverNamespaces = config.namespaces; if (isNonEmptyArray(autodiscoverNamespaces)) { logger.debug({ autodiscoverNamespaces }, "Using configured namespaces as Bitbucket workspaces"); workspaceSlugs = autodiscoverNamespaces; } else { logger.debug("Fetching Bitbucket workspaces for the current user"); const { body: slugs } = await bitbucketHttp.getJson("/2.0/user/workspaces", { paginate: true }, WorkspaceAccesses); workspaceSlugs = slugs; logger.debug({ workspaceSlugs }, `Found ${workspaceSlugs.length} Bitbucket workspace(s)`); } let repos = (await map(workspaceSlugs, (workspace) => bitbucketHttp.getJson(`/2.0/repositories/${workspace}`, { paginate: true }, Repositories).then(({ body }) => body))).flat(); const autodiscoverProjects = config.projects; if (isNonEmptyArray(autodiscoverProjects)) { logger.debug({ autodiscoverProjects: config.projects }, "Applying autodiscoverProjects filter"); repos = repos.filter((repo) => repo.projectName && matchRegexOrGlobList(repo.projectName, autodiscoverProjects)); } return repos.map(({ owner, name }) => `${owner}/${name}`); } catch (err) /* v8 ignore next */ { logger.error({ err }, `bitbucket getRepos error`); throw err; } } async function getRawFile(fileName, repoName, branchOrTag) { const repo = repoName ?? config.repository; const path = fileName; let finalBranchOrTag = branchOrTag; if (branchOrTag?.includes(pathSeparator)) finalBranchOrTag = await getBranchCommit(branchOrTag); const url = `/2.0/repositories/${repo}/src/${finalBranchOrTag ?? `HEAD`}/${path}`; return (await bitbucketHttp.getText(url, { cacheProvider: repoCacheProvider })).body; } async function getJsonFile(fileName, repoName, branchOrTag) { return parseJson(await getRawFile(fileName, repoName, branchOrTag), fileName); } async function initRepo({ repository, cloneSubmodules, cloneSubmodulesFilter }) { logger.debug(`initRepo("${repository}")`); const opts = find({ hostType: "bitbucket", url: defaults.endpoint }); config = { repository, ignorePrAuthor: GlobalConfig.get("ignorePrAuthor") }; let info; let mainBranch; try { const { body: repoInfo } = await bitbucketHttp.getJson(`/2.0/repositories/${repository}`, RepoInfo); info = repoInfo; mainBranch = info.mainbranch; if (getInheritedOrGlobal("bbUseDevelopmentBranch")) { const developmentBranch = (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${repository}/branching-model`)).body.development?.branch?.name; if (developmentBranch) mainBranch = developmentBranch; } config.defaultBranch = mainBranch; config = { ...config, owner: info.owner, mergeMethod: info.mergeMethod, has_issues: info.has_issues, is_private: info.is_private }; logger.debug(`${repository} owner = ${config.owner}`); } catch (err) /* v8 ignore next */ { if (err.statusCode === 404) throw new Error(REPOSITORY_NOT_FOUND); logger.debug({ err }, "Unknown Bitbucket initRepo error"); throw err; } const parsedEndpoint = parseUrl(defaults.endpoint); // v8 ignore if: endpoint is a constant if (!parsedEndpoint) throw new Error(`Invalid Bitbucket endpoint: ${defaults.endpoint}`); const { hostname } = parsedEndpoint; const hostnameWithoutApiPrefix = regEx(/api[.|-](.+)/).exec(hostname)?.[1]; let auth = ""; if (opts.token) auth = `x-token-auth:${opts.token}`; else if (opts.password?.startsWith("ATAT")) auth = `x-bitbucket-api-token-auth:${opts.password}`; else auth = `${opts.username}:${opts.password}`; const url = getUrl({ protocol: "https", auth, hostname: hostnameWithoutApiPrefix, repository }); await initRepo$1({ ...config, url, cloneSubmodules, cloneSubmodulesFilter }); return { defaultBranch: mainBranch, isFork: info.isFork, repoFingerprint: repoFingerprint(info.uuid, defaults.endpoint) }; } /* v8 ignore next */ function matchesState(state, desiredState) { if (desiredState === "all") return true; if (desiredState.startsWith("!")) return state !== desiredState.substring(1); return state === desiredState; } async function getPrList() { logger.trace("getPrList()"); return await BitbucketPrCache.getPrs(bitbucketHttp, config.repository, renovateUserUuid); } async function findPr({ branchName, prTitle, state = "all", includeOtherAuthors }) { logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`); if (includeOtherAuthors) { const prs = (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/pullrequests?q=source.branch.name="${branchName}"&state=open`, { cacheProvider: memCacheProvider })).body.values; if (prs.length === 0) { logger.debug(`No PR found for branch ${branchName}`); return null; } return prInfo(prs[0]); } const pr = (await getPrList()).find((p) => p.sourceBranch === branchName && (!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) && matchesState(p.state, state)); if (!pr) return null; logger.debug(`Found PR #${pr.number}`); /** * Bitbucket doesn't support renaming or reopening declined PRs. * Instead, we have to use comment-driven signals. */ if (pr.state === "closed") { const reopenComments$1 = await reopenComments(config, pr.number); if (isNonEmptyArray(reopenComments$1)) { if (config.is_private) { logger.debug(`Found '${REOPEN_PR_COMMENT_KEYWORD}' comment from workspace member. Renovate will reopen PR ${pr.number} as a new PR`); return null; } for (const comment of reopenComments$1) if (await isAccountMemberOfWorkspace(comment.user, config.repository)) { logger.debug(`Found '${REOPEN_PR_COMMENT_KEYWORD}' comment from workspace member. Renovate will reopen PR ${pr.number} as a new PR`); return null; } } } return pr; } async function getPr(prNo) { const pr = (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/pullrequests/${prNo}`, { cacheProvider: aggressiveRepoCacheProvider })).body; /* v8 ignore next */ if (!pr) return null; const res = { ...prInfo(pr) }; if (isNonEmptyArray(pr.reviewers)) res.reviewers = pr.reviewers.map(({ uuid }) => uuid).filter(isNonEmptyString); return res; } const escapeHash = (input) => input?.replace(regEx(/#/g), "%23"); async function getBranchCommit(branchName) { try { return (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/refs/branches/${escapeHash(branchName)}`, { cacheProvider: aggressiveRepoCacheProvider })).body.target.hash; } catch (err) /* v8 ignore next */ { logger.debug({ err }, `getBranchCommit('${branchName}') failed'`); return; } } async function getBranchPr(branchName) { logger.debug(`getBranchPr(${branchName})`); const existingPr = await findPr({ branchName, state: "open" }); return existingPr ? getPr(existingPr.number) : null; } async function getStatus(branchName, memCache = true) { const sha = await getBranchCommit(branchName); const opts = { paginate: true }; /* v8 ignore next: temporary code */ if (memCache) opts.cacheProvider = aggressiveRepoCacheProvider; else opts.memCache = false; return (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/commit/${sha}/statuses`, opts)).body.values; } async function getBranchStatus(branchName, internalChecksAsSuccess) { logger.debug(`getBranchStatus(${branchName})`); const statuses = await getStatus(branchName); logger.debug({ branch: branchName, statuses }, "branch status check result"); if (!statuses.length) { logger.debug("empty branch status check result = returning \"pending\""); return "yellow"; } if (statuses.filter((status) => status.state === "FAILED" || status.state === "STOPPED").length) return "red"; if (statuses.filter((status) => status.state === "INPROGRESS").length) return "yellow"; if (!internalChecksAsSuccess && statuses.every((status) => status.state === "SUCCESSFUL" && status.key?.startsWith("renovate/"))) { logger.debug("Successful checks are all internal renovate/ checks, so returning \"pending\" branch status"); return "yellow"; } return "green"; } const bbToRenovateStatusMapping = { SUCCESSFUL: "green", INPROGRESS: "yellow", FAILED: "red" }; async function getBranchStatusCheck(branchName, context) { return bbToRenovateStatusMapping[(await getStatus(branchName)).find((status) => status.key === context)?.state] || null; } async function setBranchStatus({ branchName, context, description, state, url: targetUrl }) { const sha = await getBranchCommit(branchName); /* v8 ignore next */ const url = targetUrl ?? "https://bitbucket.org"; const body = { name: context, state: buildStates[state], key: context, description, url }; await bitbucketHttp.postJson(`/2.0/repositories/${config.repository}/commit/${sha}/statuses/build`, { body }); const branchStatusesUrl = bitbucketHttp.resolveUrl(`/2.0/repositories/${config.repository}/commit/${sha}/statuses`).toString(); aggressiveRepoCacheProvider.markSynced("get", branchStatusesUrl, false); } async function findOpenIssues(title) { try { const filters = [`title=${JSON.stringify(title)}`, "(state = \"new\" OR state = \"open\")"]; if (renovateUserUuid) filters.push(`reporter.uuid="${renovateUserUuid}"`); const filter = encodeURIComponent(filters.join(" AND ")); // v8 ignore next -- TODO: add test #40625 return (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/issues?q=${filter}`, { cacheProvider: aggressiveRepoCacheProvider })).body.values || []; } catch (err) /* v8 ignore next */ { logger.warn({ err }, "Error finding issues"); return []; } } async function findIssue(title) { logger.debug(`findIssue(${title})`); /* v8 ignore next */ if (!config.has_issues) { logger.debug("Issues are disabled - cannot findIssue"); return null; } const issues = await findOpenIssues(title); if (!issues.length) return null; const [issue] = issues; return { number: issue.id, body: issue.content?.raw }; } async function closeIssue(issueNumber) { await bitbucketHttp.putJson(`/2.0/repositories/${config.repository}/issues/${issueNumber}`, { body: { state: "closed" } }); } /** * Remove or transform markdown into Bitbucket supported syntax. * * See https://bitbucket.org/tutorials/markdowndemo/src for supported markdown syntax */ /** * Remove or transform markdown into Bitbucket supported syntax. * * See https://bitbucket.org/tutorials/markdowndemo/src for supported markdown syntax */ function massageMarkdown(input) { let massaged = smartTruncate(input, maxBodyLength()).replace("you tick the rebase/retry checkbox", "by renaming this PR to start with \"rebase!\"").replace("checking the rebase/retry box above", "renaming the PR to start with \"rebase!\"").replace(regEx(/<details>\n(<summary>View abandoned dependencies.*<\/summary>\n\n)([\s\S]*?)<\/details>/), "$2").replace(regEx(`\n---\n\n.*?<!-- rebase-check -->.*?\n`), "").replace(regEx(/\]\(\.\.\/issues\//g), "](../../issues/").replace(regEx(/\]\(\.\.\/pull\//g), "](../../pull-requests/").replace(regEx(/<!--renovate-(?:debug|config-hash):.*?-->/g), ""); massaged = massageDetailSummaryHtmlToNestedLists(massaged); return massageCodeblockMarkdown(massaged); } /** * Massage codeblocks indentation to ensure correct rendering in Bitbucket. */ function massageCodeblockMarkdown(body) { const codeBlockRegex = regEx(/^(?<indent>[ \t]*)```(?<lang>\w*)[^\n]*\n(?<code>[\s\S]*?)\n[ \t]*```/gm); let codeMatch; let result = body; while ((codeMatch = codeBlockRegex.exec(body)) !== null) { const { indent, lang, code } = codeMatch.groups; const indentLength = indent.length; const replacement = `\`\`\`${lang}\n${code.split("\n").map((line) => line.slice(indentLength)).join("\n")}\n\`\`\``; result = result.replace(codeMatch[0], replacement); } return result; } /** * Massage collapsible html sections into nested unordered lists. * * Bitbucket doesn't currently support collapsible syntax; https://jira.atlassian.com/browse/BCLOUD-20231 */ function massageDetailSummaryHtmlToNestedLists(body) { let depth = 0; return body.split("<details>").map((raw) => { const partDepth = depth; depth += 1; const countClosingDetailsTags = raw.split("</details>").length - 1; depth = Math.max(0, depth - countClosingDetailsTags); return { raw, partDepth }; }).map(({ raw, partDepth }) => { let t = raw; if (partDepth === 0) return t; const partIndentation = " ".repeat(partDepth - 1); const nestedListItemIndentation = " ".repeat(partDepth); const rawContainsBlockquote = raw.includes("<blockquote>"); t = t.replace(regEx(/<\/?summary>/g), partDepth === 1 ? "**" : "`"); if (partDepth > 1) t = t.replace(regEx(/^([ \t]*- [`[])/gm), `${nestedListItemIndentation}$1`); let result = partIndentation; if (rawContainsBlockquote || partDepth > 1) result += " - "; result += t; return result; }).join("").replace(/<\/?(summary|details|blockquote)>/g, ""); } function maxBodyLength() { return 25e4; } async function ensureIssue({ title, reuseTitle, body }) { logger.debug(`ensureIssue()`); /* v8 ignore next */ if (!config.has_issues) { logger.debug("Issues are disabled - cannot ensureIssue"); logger.debug(`Failed to ensure Issue with title:${title}`); return null; } try { let issues = await findOpenIssues(title); const description = massageMarkdown(sanitize(body)); const issueKind = "task"; if (!issues.length && reuseTitle) issues = await findOpenIssues(reuseTitle); if (issues.length) { for (const issue of issues.slice(1)) await closeIssue(issue.id); const [issue] = issues; if (issue.title !== title || String(issue.content?.raw).trim() !== description.trim() || issue.kind !== issueKind) { logger.debug("Issue updated"); await bitbucketHttp.putJson(`/2.0/repositories/${config.repository}/issues/${issue.id}`, { body: { kind: issueKind, content: { raw: readOnlyIssueBody(description), markup: "markdown" } } }); return "updated"; } } else { logger.info("Issue created"); await bitbucketHttp.postJson(`/2.0/repositories/${config.repository}/issues`, { body: { title, kind: issueKind, content: { raw: readOnlyIssueBody(description), markup: "markdown" } } }); return "created"; } } catch (err) /* v8 ignore next */ { if (err.message.startsWith("Repository has no issue tracker.")) logger.debug(`Issues are disabled, so could not create issue: ${title}`); else logger.warn({ err }, "Could not ensure issue"); } return null; } /* v8 ignore next */ async function getIssueList() { logger.debug(`getIssueList()`); if (!config.has_issues) { logger.debug("Issues are disabled - cannot getIssueList"); return []; } try { const filters = ["(state = \"new\" OR state = \"open\")"]; if (renovateUserUuid) filters.push(`reporter.uuid="${renovateUserUuid}"`); const filter = encodeURIComponent(filters.join(" AND ")); const url = `/2.0/repositories/${config.repository}/issues?q=${filter}`; return (await bitbucketHttp.getJsonUnchecked(url, { cacheProvider: repoCacheProvider })).body.values || []; } catch (err) { logger.warn({ err }, "Error finding issues"); return []; } } async function ensureIssueClosing(title) { /* v8 ignore next */ if (!config.has_issues) { logger.debug("Issues are disabled - cannot ensureIssueClosing"); return; } const issues = await findOpenIssues(title); for (const issue of issues) await closeIssue(issue.id); } function addAssignees(_prNr, _assignees) { logger.warn("Cannot add assignees"); return Promise.resolve(); } async function addReviewers(prId, reviewers) { logger.debug(`Adding reviewers '${reviewers.join(", ")}' to #${prId}`); const { title } = await getPr(prId); const body = { title, reviewers: reviewers.map((username) => { return { [username.startsWith("{") && username.endsWith("}") && UUIDRegex.test(username.slice(1, -1)) ? "uuid" : "username"]: username }; }) }; await bitbucketHttp.putJson(`/2.0/repositories/${config.repository}/pullrequests/${prId}`, { body }); } /* v8 ignore next */ function deleteLabel() { throw new Error("deleteLabel not implemented"); } function ensureComment({ number, topic, content }) { return ensureComment$1({ config, number, topic, content: sanitize(content) }); } function ensureCommentRemoval(deleteConfig) { return ensureCommentRemoval$1(config, deleteConfig); } async function sanitizeReviewers(reviewers, err) { if (err.statusCode === 400 && err.body?.error?.fields?.reviewers) { const sanitizedReviewers = []; const MSG_AUTHOR_AND_REVIEWER = "is the author and cannot be included as a reviewer."; const MSG_MALFORMED_REVIEWERS_LIST = "Malformed reviewers list"; const MSG_NOT_WORKSPACE_MEMBER = "is not a member of this workspace and cannot be added to this pull request"; for (const msg of err.body.error.fields.reviewers) if (msg === MSG_MALFORMED_REVIEWERS_LIST) { logger.debug({ err }, "PR contains reviewers that may be either inactive or no longer a member of this workspace. Will try setting only active reviewers"); for (const reviewer of reviewers) if ((await bitbucketHttp.getJsonUnchecked(`/2.0/users/${reviewer.uuid}`, { cacheProvider: aggressiveRepoCacheProvider })).body.account_status === "active") { if (await isAccountMemberOfWorkspace(reviewer, config.repository)) sanitizedReviewers.push(reviewer); } } else if (msg.endsWith(MSG_NOT_WORKSPACE_MEMBER)) { logger.debug({ err }, "PR contains reviewer accounts which are no longer member of this workspace. Will try setting only member reviewers"); for (const reviewer of reviewers) if (await isAccountMemberOfWorkspace(reviewer, config.repository)) sanitizedReviewers.push(reviewer); } else if (msg.endsWith(MSG_AUTHOR_AND_REVIEWER)) { logger.debug({ err }, "PR contains reviewer accounts which are also the author. Will try setting only non-author reviewers"); const author = msg.replace(MSG_AUTHOR_AND_REVIEWER, "").trim(); for (const reviewer of reviewers) if (reviewer.display_name !== author) sanitizedReviewers.push(reviewer); } else return; return sanitizedReviewers; } } async function isAccountMemberOfWorkspace(reviewer, repository) { const workspace = repository.split("/")[0]; try { await bitbucketHttp.get(`/2.0/workspaces/${workspace}/members/${reviewer.uuid}`, { cacheProvider: aggressiveRepoCacheProvider }); return true; } catch (err) { if (err.statusCode === 404) { logger.debug({ err }, `User ${reviewer.display_name} is not a member of the workspace ${workspace}. Will be removed from the PR`); return false; } throw err; } } async function createPr({ sourceBranch, targetBranch, prTitle: title, prBody: description, platformPrOptions }) { const base = targetBranch; logger.debug({ repository: config.repository, title, base }, "Creating PR"); let reviewers = []; if (platformPrOptions?.bbUseDefaultReviewers) reviewers = (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/effective-default-reviewers`, { paginate: true, cacheProvider: aggressiveRepoCacheProvider })).body.values.map((reviewer) => ({ uuid: reviewer.user.uuid, display_name: reviewer.user.display_name })); const body = { title, description: sanitize(description), source: { branch: { name: sourceBranch } }, destination: { branch: { name: base } }, close_source_branch: true, reviewers }; try { const prRes = (await bitbucketHttp.postJson(`/2.0/repositories/${config.repository}/pullrequests`, { body })).body; const pr = prInfo(prRes); await BitbucketPrCache.setPr(bitbucketHttp, config.repository, renovateUserUuid, pr); if (platformPrOptions?.bbAutoResolvePrTasks) await autoResolvePrTasks(pr); return pr; } catch (err) /* v8 ignore next */ { const sanitizedReviewers = await sanitizeReviewers(reviewers, err); if (sanitizedReviewers === void 0) { logger.warn({ err }, "Error creating pull request"); throw err; } else { const prRes = (await bitbucketHttp.postJson(`/2.0/repositories/${config.repository}/pullrequests`, { body: { ...body, reviewers: sanitizedReviewers } })).body; const pr = prInfo(prRes); await BitbucketPrCache.setPr(bitbucketHttp, config.repository, renovateUserUuid, pr); if (platformPrOptions?.bbAutoResolvePrTasks) await autoResolvePrTasks(pr); return pr; } } } async function autoResolvePrTasks(pr) { logger.debug(`Auto resolve PR tasks in #${pr.number}`); try { const unResolvedTasks = (await bitbucketHttp.getJson(`/2.0/repositories/${config.repository}/pullrequests/${pr.number}/tasks`, { paginate: true, pagelen: 100 }, UnresolvedPrTasks)).body; logger.trace({ prId: pr.number, listTaskRes: unResolvedTasks }, "List PR tasks"); for (const task of unResolvedTasks) { const res = await bitbucketHttp.putJson(`/2.0/repositories/${config.repository}/pullrequests/${pr.number}/tasks/${task.id}`, { body: { state: "RESOLVED", content: { raw: task.content.raw } } }); logger.trace({ prId: pr.number, updateTaskResponse: res }, "Put PR tasks - mark resolved"); } } catch (err) { logger.warn({ prId: pr.number, err }, "Error resolving PR tasks"); } } async function updatePr({ number: prNo, prTitle: title, prBody: description, state, targetBranch }) { logger.debug(`updatePr(${prNo}, ${title}, body)`); const pr = (await bitbucketHttp.getJsonUnchecked(`/2.0/repositories/${config.repository}/pullrequests/${prNo}`)).body; let updatedPrRes; try { const body = { title, description: sanitize(description), reviewers: pr.reviewers }; if (targetBranch) body.destination = { branch: { name: targetBranch } }; updatedPrRes = (await bitbucketHttp.putJson(`/2.0/repositories/${config.repository}/pullrequests/${prNo}`, { body })).body; } catch (err) { const sanitizedReviewers = await sanitizeReviewers(pr.reviewers, err); if (sanitizedReviewers === void 0) throw err; else updatedPrRes = (await bitbucketHttp.putJson(`/2.0/repositories/${config.repository}/pullrequests/${prNo}`, { body: { title, description: sanitize(description), reviewers: sanitizedReviewers } })).body; } if (state === "closed" && pr) await bitbucketHttp.postJson(`/2.0/repositories/${config.repository}/pullrequests/${prNo}/decline`); await BitbucketPrCache.setPr(bitbucketHttp, config.repository, renovateUserUuid, prInfo({ ...updatedPrRes, ...state && { state } })); } async function mergePr({ branchName, id: prNo, strategy: mergeStrategy }) { logger.debug(`mergePr(${prNo}, ${branchName}, ${mergeStrategy})`); if (mergeStrategy === "rebase") { logger.warn("Bitbucket Cloud does not support a \"rebase\" strategy."); return false; } try { await bitbucketHttp.postJson(`/2.0/repositories/${config.repository}/pullrequests/${prNo}/merge`, { body: mergeBodyTransformer(mergeStrategy) }); logger.debug("Automerging succeeded"); } catch (err) /* v8 ignore next */ { logger.debug({ err }, `PR merge error`); logger.info({ pr: prNo }, "PR automerge failed"); return false; } return true; } //#endregion export { addAssignees, addReviewers, bitbucket_exports, createPr, deleteLabel, ensureComment, ensureCommentRemoval, ensureIssue, ensureIssueClosing, findIssue, findPr, getBranchPr, getBranchStatus, getBranchStatusCheck, getIssueList, getJsonFile, getPr, getPrList, getRawFile, getRepos, id, initPlatform, initRepo, massageMarkdown, maxBodyLength, mergePr, resetPlatform, setBranchStatus, updatePr }; //# sourceMappingURL=index.js.map