renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
655 lines (654 loc) • 27.8 kB
JavaScript
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