renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
814 lines • 34.5 kB
JavaScript
;
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