renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
1,077 lines • 44.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.id = exports.extractRulesFromCodeOwnersLines = void 0;
exports.resetPlatform = resetPlatform;
exports.initPlatform = initPlatform;
exports.getRepos = getRepos;
exports.getRawFile = getRawFile;
exports.getJsonFile = getJsonFile;
exports.initRepo = initRepo;
exports.getBranchForceRebase = getBranchForceRebase;
exports.getBranchStatus = getBranchStatus;
exports.getPrList = getPrList;
exports.createPr = createPr;
exports.getPr = getPr;
exports.updatePr = updatePr;
exports.reattemptPlatformAutomerge = reattemptPlatformAutomerge;
exports.mergePr = mergePr;
exports.massageMarkdown = massageMarkdown;
exports.maxBodyLength = maxBodyLength;
exports.labelCharLimit = labelCharLimit;
exports.findPr = findPr;
exports.getBranchPr = getBranchPr;
exports.getBranchStatusCheck = getBranchStatusCheck;
exports.setBranchStatus = setBranchStatus;
exports.getIssueList = getIssueList;
exports.getIssue = getIssue;
exports.findIssue = findIssue;
exports.ensureIssue = ensureIssue;
exports.ensureIssueClosing = ensureIssueClosing;
exports.addAssignees = addAssignees;
exports.addReviewers = addReviewers;
exports.deleteLabel = deleteLabel;
exports.ensureComment = ensureComment;
exports.ensureCommentRemoval = ensureCommentRemoval;
exports.filterUnavailableUsers = filterUnavailableUsers;
exports.expandGroupMembers = expandGroupMembers;
const tslib_1 = require("tslib");
const node_url_1 = tslib_1.__importDefault(require("node:url"));
const promises_1 = require("timers/promises");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const p_map_1 = tslib_1.__importDefault(require("p-map"));
const semver_1 = tslib_1.__importDefault(require("semver"));
const error_messages_1 = require("../../../constants/error-messages");
const logger_1 = require("../../../logger");
const array_1 = require("../../../util/array");
const common_1 = require("../../../util/common");
const env_1 = require("../../../util/env");
const git = tslib_1.__importStar(require("../../../util/git"));
const hostRules = tslib_1.__importStar(require("../../../util/host-rules"));
const memory_http_cache_provider_1 = require("../../../util/http/cache/memory-http-cache-provider");
const gitlab_1 = require("../../../util/http/gitlab");
const number_1 = require("../../../util/number");
const p = tslib_1.__importStar(require("../../../util/promises"));
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 http_1 = require("./http");
const merge_request_1 = require("./merge-request");
const pr_cache_1 = require("./pr-cache");
const schema_1 = require("./schema");
const utils_1 = require("./utils");
var code_owners_1 = require("./code-owners");
Object.defineProperty(exports, "extractRulesFromCodeOwnersLines", { enumerable: true, get: function () { return code_owners_1.extractRulesFromCodeOwnersLines; } });
let config = {};
function resetPlatform() {
config = {};
draftPrefix = utils_1.DRAFT_PREFIX;
defaults.hostType = 'gitlab';
defaults.endpoint = 'https://gitlab.com/api/v4/';
defaults.version = '0.0.0';
(0, gitlab_1.setBaseUrl)(defaults.endpoint);
}
const defaults = {
hostType: 'gitlab',
endpoint: 'https://gitlab.com/api/v4/',
version: '0.0.0',
};
exports.id = 'gitlab';
let draftPrefix = utils_1.DRAFT_PREFIX;
let botUserName;
async function initPlatform({ endpoint, username, token, gitAuthor, }) {
if (!token) {
throw new Error('Init: You must configure a GitLab personal access token');
}
if (endpoint) {
defaults.endpoint = (0, url_1.ensureTrailingSlash)(endpoint);
(0, gitlab_1.setBaseUrl)(defaults.endpoint);
}
else {
logger_1.logger.debug('Using default GitLab endpoint: ' + defaults.endpoint);
}
const platformConfig = {
endpoint: defaults.endpoint,
};
let gitlabVersion;
try {
if (!gitAuthor) {
const user = (await http_1.gitlabApi.getJsonUnchecked(`user`, { token })).body;
platformConfig.gitAuthor = `${user.name} <${user.commit_email ?? user.email}>`;
botUserName = user.name;
}
const env = (0, env_1.getEnv)();
/* v8 ignore start: experimental feature */
if (env.RENOVATE_X_PLATFORM_VERSION) {
gitlabVersion = env.RENOVATE_X_PLATFORM_VERSION;
} /* v8 ignore stop */
else {
const version = (await http_1.gitlabApi.getJsonUnchecked('version', {
token,
})).body;
gitlabVersion = version.version;
}
logger_1.logger.debug('GitLab version is: ' + gitlabVersion);
// version is 'x.y.z-edition', so not strictly semver; need to strip edition
[gitlabVersion] = gitlabVersion.split('-');
defaults.version = gitlabVersion;
}
catch (err) {
logger_1.logger.debug({ err }, 'Error authenticating with GitLab. Check that your token includes "api" permissions');
throw new Error('Init: Authentication failure');
}
draftPrefix = semver_1.default.lt(defaults.version, '13.2.0')
? utils_1.DRAFT_PREFIX_DEPRECATED
: utils_1.DRAFT_PREFIX;
botUserName ??= username;
return platformConfig;
}
// Get all repositories that the user has access to
async function getRepos(config) {
logger_1.logger.debug('Autodiscovering GitLab repositories');
const queryParams = {
membership: true,
per_page: 100,
with_merge_requests_enabled: true,
min_access_level: 30,
archived: false,
};
if (config?.topics?.length) {
queryParams.topic = config.topics.join(',');
}
const urls = [];
if (config?.namespaces?.length) {
queryParams.with_shared = false;
queryParams.include_subgroups = true;
urls.push(...config.namespaces.map((namespace) => `groups/${urlEscape(namespace)}/projects?${(0, url_1.getQueryString)(queryParams)}`));
}
else {
urls.push('projects?' + (0, url_1.getQueryString)(queryParams));
}
try {
const repos = (await (0, p_map_1.default)(urls, (url) => http_1.gitlabApi.getJsonUnchecked(url, {
paginate: true,
}), {
concurrency: 2,
})).flatMap((response) => response.body);
logger_1.logger.debug(`Discovered ${repos.length} project(s)`);
return repos
.filter((repo) => !repo.mirror || config?.includeMirrors)
.map((repo) => repo.path_with_namespace);
}
catch (err) {
logger_1.logger.error({ err }, `GitLab getRepos error`);
throw err;
}
}
function urlEscape(str) {
return str?.replace((0, regex_1.regEx)(/\//g), '%2F');
}
async function getRawFile(fileName, repoName, branchOrTag) {
const escapedFileName = urlEscape(fileName);
const repo = urlEscape(repoName) ?? config.repository;
const url = `projects/${repo}/repository/files/${escapedFileName}?ref=` +
(branchOrTag ?? `HEAD`);
const res = await http_1.gitlabApi.getJsonUnchecked(url, {
cacheProvider: memory_http_cache_provider_1.memCacheProvider,
});
const buf = res.body.content;
const str = Buffer.from(buf, 'base64').toString();
return str;
}
async function getJsonFile(fileName, repoName, branchOrTag) {
const raw = await getRawFile(fileName, repoName, branchOrTag);
return (0, common_1.parseJson)(raw, fileName);
}
function getRepoUrl(repository, gitUrl, res) {
if (gitUrl === 'ssh') {
if (!res.body.ssh_url_to_repo) {
throw new Error(error_messages_1.CONFIG_GIT_URL_UNAVAILABLE);
}
logger_1.logger.debug(`Using ssh URL: ${res.body.ssh_url_to_repo}`);
return res.body.ssh_url_to_repo;
}
const opts = hostRules.find({
hostType: defaults.hostType,
url: defaults.endpoint,
});
const env = (0, env_1.getEnv)();
if (gitUrl === 'endpoint' ||
is_1.default.nonEmptyString(env.GITLAB_IGNORE_REPO_URL) ||
res.body.http_url_to_repo === null) {
if (res.body.http_url_to_repo === null) {
logger_1.logger.debug('no http_url_to_repo found. Falling back to old behavior.');
}
if (env.GITLAB_IGNORE_REPO_URL) {
logger_1.logger.warn('GITLAB_IGNORE_REPO_URL environment variable is deprecated. Please use "gitUrl" option.');
}
// TODO: null check (#22198)
const { protocol, host, pathname } = (0, url_1.parseUrl)(defaults.endpoint);
const newPathname = pathname.slice(0, pathname.indexOf('/api'));
const url = node_url_1.default.format({
protocol:
/* v8 ignore next: should never happen */
protocol.slice(0, -1) || 'https',
// TODO: types (#22198)
auth: `oauth2:${opts.token}`,
host,
pathname: `${newPathname}/${repository}.git`,
});
logger_1.logger.debug(`Using URL based on configured endpoint, url:${url}`);
return url;
}
logger_1.logger.debug(`Using http URL: ${res.body.http_url_to_repo}`);
const repoUrl = node_url_1.default.parse(`${res.body.http_url_to_repo}`);
// TODO: types (#22198)
repoUrl.auth = `oauth2:${opts.token}`;
return node_url_1.default.format(repoUrl);
}
// Initialize GitLab by getting base branch
async function initRepo({ repository, cloneSubmodules, cloneSubmodulesFilter, ignorePrAuthor, gitUrl, endpoint, includeMirrors, }) {
config = {};
config.repository = urlEscape(repository);
config.cloneSubmodules = cloneSubmodules;
config.cloneSubmodulesFilter = cloneSubmodulesFilter;
config.ignorePrAuthor = ignorePrAuthor;
let res;
try {
res = await http_1.gitlabApi.getJsonUnchecked(`projects/${config.repository}`);
if (res.body.archived) {
logger_1.logger.debug('Repository is archived - throwing error to abort renovation');
throw new Error(error_messages_1.REPOSITORY_ARCHIVED);
}
if (res.body.mirror && includeMirrors !== true) {
logger_1.logger.debug('Repository is a mirror - throwing error to abort renovation');
throw new Error(error_messages_1.REPOSITORY_MIRRORED);
}
if (res.body.repository_access_level === 'disabled') {
logger_1.logger.debug('Repository portion of project is disabled - throwing error to abort renovation');
throw new Error(error_messages_1.REPOSITORY_DISABLED);
}
if (res.body.merge_requests_access_level === 'disabled') {
logger_1.logger.debug('MRs are disabled for the project - throwing error to abort renovation');
throw new Error(error_messages_1.REPOSITORY_DISABLED);
}
if (res.body.default_branch === null || res.body.empty_repo) {
throw new Error(error_messages_1.REPOSITORY_EMPTY);
}
config.defaultBranch = res.body.default_branch;
/* v8 ignore start */
if (!config.defaultBranch) {
logger_1.logger.warn({ resBody: res.body }, 'Error fetching GitLab project');
throw new Error(error_messages_1.TEMPORARY_ERROR);
} /* v8 ignore stop */
config.mergeMethod = res.body.merge_method || 'merge';
if (res.body.squash_option) {
config.squash =
res.body.squash_option === 'always' ||
res.body.squash_option === 'default_on';
}
logger_1.logger.debug(`${repository} default branch = ${config.defaultBranch}`);
logger_1.logger.debug('Enabling Git FS');
const url = getRepoUrl(repository, gitUrl, res);
await git.initRepo({
...config,
url,
});
}
catch (err) /* v8 ignore start */ {
logger_1.logger.debug({ err }, 'Caught initRepo error');
if (err.message.includes('HEAD is not a symbolic ref')) {
throw new Error(error_messages_1.REPOSITORY_EMPTY);
}
if ([error_messages_1.REPOSITORY_ARCHIVED, error_messages_1.REPOSITORY_EMPTY].includes(err.message)) {
throw err;
}
if (err.statusCode === 403) {
throw new Error(error_messages_1.REPOSITORY_ACCESS_FORBIDDEN);
}
if (err.statusCode === 404) {
throw new Error(error_messages_1.REPOSITORY_NOT_FOUND);
}
if (err.message === error_messages_1.REPOSITORY_DISABLED) {
throw err;
}
logger_1.logger.debug({ err }, 'Unknown GitLab initRepo error');
throw err;
} /* v8 ignore stop */
const repoConfig = {
defaultBranch: config.defaultBranch,
isFork: !!res.body.forked_from_project,
repoFingerprint: (0, util_1.repoFingerprint)(res.body.id, defaults.endpoint),
};
return repoConfig;
}
function getBranchForceRebase() {
const forceRebase = config?.mergeMethod !== 'merge';
if (forceRebase) {
logger_1.logger.once.debug(`mergeMethod is ${config.mergeMethod} so PRs will be kept up-to-date with base branch`);
}
return Promise.resolve(forceRebase);
}
async function getStatus(branchName, useCache = true) {
const branchSha = git.getBranchCommit(branchName);
try {
// TODO: types (#22198)
const url = `projects/${config.repository}/repository/commits/${branchSha}/statuses`;
const opts = { paginate: true };
if (useCache) {
opts.cacheProvider = memory_http_cache_provider_1.memCacheProvider;
}
else {
opts.memCache = false;
}
return (await http_1.gitlabApi.getJsonUnchecked(url, opts))
.body;
}
catch (err) /* v8 ignore start */ {
logger_1.logger.debug({ err }, 'Error getting commit status');
if (err.response?.statusCode === 404) {
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
throw err;
} /* v8 ignore stop */
}
const gitlabToRenovateStatusMapping = {
pending: 'yellow',
created: 'yellow',
manual: 'yellow',
running: 'yellow',
waiting_for_resource: 'yellow',
success: 'green',
failed: 'red',
canceled: 'red',
skipped: 'red',
scheduled: 'yellow',
};
// Returns the combined status for a branch.
async function getBranchStatus(branchName, internalChecksAsSuccess) {
logger_1.logger.debug(`getBranchStatus(${branchName})`);
if (!git.branchExists(branchName)) {
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
const branchStatuses = await getStatus(branchName);
/* v8 ignore start */
if (!is_1.default.array(branchStatuses)) {
logger_1.logger.warn({ branchName, branchStatuses }, 'Empty or unexpected branch statuses');
return 'yellow';
} /* v8 ignore stop */
logger_1.logger.debug(`Got res with ${branchStatuses.length} results`);
const mr = await getBranchPr(branchName);
if (mr && mr.sha !== mr.headPipelineSha && mr.headPipelineStatus) {
logger_1.logger.debug('Merge request head pipeline has different sha to commit, assuming merged results pipeline');
branchStatuses.push({
status: mr.headPipelineStatus,
name: 'head_pipeline',
});
}
// ignore all skipped jobs
const res = branchStatuses.filter((check) => check.status !== 'skipped');
if (res.length === 0) {
// Return 'pending' if we have no status checks
return 'yellow';
}
if (!internalChecksAsSuccess &&
branchStatuses.every((check) => check.name?.startsWith('renovate/') &&
gitlabToRenovateStatusMapping[check.status] === 'green')) {
logger_1.logger.debug('Successful checks are all internal renovate/ checks, so returning "pending" branch status');
return 'yellow';
}
let status = 'green'; // default to green
res
.filter((check) => !check.allow_failure)
.forEach((check) => {
if (status !== 'red') {
// if red, stay red
let mappedStatus = gitlabToRenovateStatusMapping[check.status];
if (!mappedStatus) {
logger_1.logger.warn({ check }, 'Could not map GitLab check.status to Renovate status');
mappedStatus = 'yellow';
}
if (mappedStatus !== 'green') {
logger_1.logger.trace({ check }, 'Found non-green check');
status = mappedStatus;
}
}
});
return status;
}
// Pull Request
async function getPrList() {
return await pr_cache_1.GitlabPrCache.getPrs(http_1.gitlabApi, config.repository, botUserName, !!config.ignorePrAuthor);
}
async function ignoreApprovals(pr) {
try {
const url = `projects/${config.repository}/merge_requests/${pr}/approval_rules`;
const { body: rules } = await http_1.gitlabApi.getJsonUnchecked(url);
const ruleName = 'renovateIgnoreApprovals';
const existingAnyApproverRule = rules?.find(({ rule_type }) => rule_type === 'any_approver');
const existingRegularApproverRules = rules?.filter(({ rule_type, name }) => rule_type !== 'any_approver' &&
name !== ruleName &&
rule_type !== 'report_approver' &&
rule_type !== 'code_owner');
if (existingRegularApproverRules?.length) {
await p.all(existingRegularApproverRules.map((rule) => async () => {
await http_1.gitlabApi.deleteJson(`${url}/${rule.id}`);
}));
}
if (existingAnyApproverRule) {
await http_1.gitlabApi.putJson(`${url}/${existingAnyApproverRule.id}`, {
body: { ...existingAnyApproverRule, approvals_required: 0 },
});
return;
}
const zeroApproversRule = rules?.find(({ name }) => name === ruleName);
if (!zeroApproversRule) {
await http_1.gitlabApi.postJson(url, {
body: {
name: ruleName,
approvals_required: 0,
},
});
}
}
catch (err) {
logger_1.logger.warn({ err }, 'GitLab: Error adding approval rule');
}
}
async function tryPrAutomerge(pr, platformPrOptions) {
try {
if (platformPrOptions?.gitLabIgnoreApprovals) {
await ignoreApprovals(pr);
}
if (platformPrOptions?.usePlatformAutomerge) {
// https://docs.gitlab.com/ee/api/merge_requests.html#merge-status
const desiredDetailedMergeStatus = [
'mergeable',
'ci_still_running',
'not_approved',
];
const desiredPipelineStatus = [
'failed', // don't lose time if pipeline failed
'running', // pipeline is running, no need to wait for it
];
const desiredStatus = 'can_be_merged';
const env = (0, env_1.getEnv)();
// The default value of 5 attempts results in max. 13.75 seconds timeout if no pipeline created.
const retryTimes = (0, number_1.parseInteger)(env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS, 5);
const mergeDelay = (0, number_1.parseInteger)(env.RENOVATE_X_GITLAB_MERGE_REQUEST_DELAY, 250);
// Check for correct merge request status before setting `merge_when_pipeline_succeeds` to `true`.
for (let attempt = 1; attempt <= retryTimes; attempt += 1) {
const { body } = await http_1.gitlabApi.getJsonUnchecked(`projects/${config.repository}/merge_requests/${pr}`, {
memCache: false,
});
// detailed_merge_status is available with Gitlab >=15.6.0
const use_detailed_merge_status = !!body.detailed_merge_status;
const detailed_merge_status_check = use_detailed_merge_status &&
desiredDetailedMergeStatus.includes(body.detailed_merge_status);
// merge_status is deprecated with Gitlab >= 15.6
const deprecated_merge_status_check = !use_detailed_merge_status && body.merge_status === desiredStatus;
// Only continue if the merge request can be merged and has a pipeline.
if ((detailed_merge_status_check || deprecated_merge_status_check) &&
body.pipeline !== null &&
desiredPipelineStatus.includes(body.pipeline.status)) {
break;
}
logger_1.logger.debug(`PR not yet in mergeable state. Retrying ${attempt}`);
await (0, promises_1.setTimeout)(mergeDelay * attempt ** 2); // exponential backoff
}
// Even if Gitlab returns a "merge-able" merge request status, enabling auto-merge sometimes
// returns a 405 Method Not Allowed. It seems to be a timing issue within Gitlab.
for (let attempt = 1; attempt <= retryTimes; attempt += 1) {
try {
await http_1.gitlabApi.putJson(`projects/${config.repository}/merge_requests/${pr}/merge`, {
body: {
should_remove_source_branch: true,
merge_when_pipeline_succeeds: true,
},
});
break;
}
catch (err) {
logger_1.logger.debug({ err }, `Automerge on PR creation failed. Retrying ${attempt}`);
}
await (0, promises_1.setTimeout)(mergeDelay * attempt ** 2); // exponential backoff
}
}
}
catch (err) /* v8 ignore start */ {
logger_1.logger.debug({ err }, 'Automerge on PR creation failed');
} /* v8 ignore stop */
}
async function approveMr(mrNumber) {
logger_1.logger.debug(`approveMr(${mrNumber})`);
try {
await http_1.gitlabApi.postJson(`projects/${config.repository}/merge_requests/${mrNumber}/approve`);
}
catch (err) {
logger_1.logger.warn({ err }, 'GitLab: Error approving merge request');
}
}
async function createPr({ sourceBranch, targetBranch, prTitle, prBody: rawDescription, draftPR, labels, platformPrOptions, }) {
let title = prTitle;
if (draftPR) {
title = draftPrefix + title;
}
const description = (0, sanitize_1.sanitize)(rawDescription);
logger_1.logger.debug(`Creating Merge Request: ${title}`);
const res = await http_1.gitlabApi.postJson(`projects/${config.repository}/merge_requests`, {
body: {
source_branch: sourceBranch,
target_branch: targetBranch,
remove_source_branch: true,
title,
description,
labels: (labels ?? []).join(','),
squash: config.squash,
},
});
const pr = (0, utils_1.prInfo)(res.body);
await pr_cache_1.GitlabPrCache.setPr(http_1.gitlabApi, config.repository, botUserName, pr, !!config.ignorePrAuthor);
if (platformPrOptions?.autoApprove) {
await approveMr(pr.number);
}
await tryPrAutomerge(pr.number, platformPrOptions);
return pr;
}
async function getPr(iid) {
logger_1.logger.debug(`getPr(${iid})`);
const mr = await (0, merge_request_1.getMR)(config.repository, iid);
// Harmonize fields with GitHub
return (0, utils_1.prInfo)(mr);
}
async function updatePr({ number: iid, prTitle, prBody: description, addLabels, removeLabels, state, platformPrOptions, targetBranch, }) {
let title = prTitle;
if ((await getPrList()).find((pr) => pr.number === iid)?.isDraft) {
title = draftPrefix + title;
}
const newState = {
['closed']: 'close',
['open']: 'reopen',
// TODO: null check (#22198)
}[state];
const body = {
title,
description: (0, sanitize_1.sanitize)(description),
...(newState && { state_event: newState }),
};
if (targetBranch) {
body.target_branch = targetBranch;
}
if (addLabels) {
body.add_labels = addLabels;
}
if (removeLabels) {
body.remove_labels = removeLabels;
}
const updatedPrInfo = (await http_1.gitlabApi.putJson(`projects/${config.repository}/merge_requests/${iid}`, { body })).body;
const updatedPr = (0, utils_1.prInfo)(updatedPrInfo);
await pr_cache_1.GitlabPrCache.setPr(http_1.gitlabApi, config.repository, botUserName, updatedPr, !!config.ignorePrAuthor);
if (platformPrOptions?.autoApprove) {
await approveMr(iid);
}
}
async function reattemptPlatformAutomerge({ number: iid, platformPrOptions, }) {
await tryPrAutomerge(iid, platformPrOptions);
logger_1.logger.debug(`PR platform automerge re-attempted...prNo: ${iid}`);
}
async function mergePr({ id }) {
try {
await http_1.gitlabApi.putJson(`projects/${config.repository}/merge_requests/${id}/merge`, {
body: {
should_remove_source_branch: true,
},
});
return true;
}
catch (err) /* v8 ignore start */ {
if (err.statusCode === 401) {
logger_1.logger.debug('No permissions to merge PR');
return false;
}
if (err.statusCode === 406) {
logger_1.logger.debug({ err }, 'PR not acceptable for merging');
return false;
}
logger_1.logger.debug({ err }, 'merge PR error');
logger_1.logger.debug('PR merge failed');
return false;
} /* v8 ignore stop */
}
function massageMarkdown(input) {
const desc = input
.replace((0, regex_1.regEx)(/Pull Request/g), 'Merge Request')
.replace((0, regex_1.regEx)(/\bPR\b/g), 'MR')
.replace((0, regex_1.regEx)(/\bPRs\b/g), 'MRs')
.replace((0, regex_1.regEx)(/\]\(\.\.\/pull\//g), '](!')
// Strip unicode null characters as GitLab markdown does not permit them
.replace((0, regex_1.regEx)(/\u0000/g), ''); // eslint-disable-line no-control-regex
return (0, pr_body_1.smartTruncate)(desc, maxBodyLength());
}
function maxBodyLength() {
if (semver_1.default.lt(defaults.version, '13.4.0')) {
logger_1.logger.debug({ version: defaults.version }, 'GitLab versions earlier than 13.4 have issues with long descriptions, truncating to 25K characters');
return 25000;
}
else {
return 1000000;
}
}
/* v8 ignore start: no need to test */
function labelCharLimit() {
return 255;
}
/* v8 ignore stop */
// Branch
function matchesState(state, desiredState) {
if (desiredState === 'all') {
return true;
}
if (desiredState.startsWith('!')) {
return state !== desiredState.substring(1);
}
return state === desiredState;
}
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 MR list
const response = await http_1.gitlabApi.getJsonUnchecked(`projects/${config.repository}/merge_requests?source_branch=${branchName}&state=opened`);
const { body: mrList } = response;
if (!mrList.length) {
logger_1.logger.debug(`No MR found for branch ${branchName}`);
return null;
}
return (0, utils_1.prInfo)(mrList[0]);
}
const prList = await getPrList();
return (prList.find((p) => p.sourceBranch === branchName &&
(!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) &&
matchesState(p.state, state)) ?? null);
}
// 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;
}
async function getBranchStatusCheck(branchName, context) {
// cache-bust in case we have rebased
const res = await getStatus(branchName, false);
logger_1.logger.debug(`Got res with ${res.length} results`);
for (const check of res) {
if (check.name === context) {
return gitlabToRenovateStatusMapping[check.status] || 'yellow';
}
}
return null;
}
async function setBranchStatus({ branchName, context, description, state: renovateState, url: targetUrl, }) {
// First, get the branch commit SHA
const branchSha = git.getBranchCommit(branchName);
if (!branchSha) {
logger_1.logger.warn('Failed to get the branch commit SHA');
return;
}
// Now, check the statuses for that commit
const url = `projects/${config.repository}/statuses/${branchSha}`;
let state = 'success';
if (renovateState === 'yellow') {
state = 'pending';
}
else if (renovateState === 'red') {
state = 'failed';
}
const options = {
state,
description,
context,
};
if (targetUrl) {
options.target_url = targetUrl;
}
const env = (0, env_1.getEnv)();
const retryTimes = (0, number_1.parseInteger)(env.RENOVATE_X_GITLAB_BRANCH_STATUS_CHECK_ATTEMPTS, 2);
try {
for (let attempt = 1; attempt <= retryTimes + 1; attempt += 1) {
const commitUrl = `projects/${config.repository}/repository/commits/${branchSha}`;
await http_1.gitlabApi
.getJsonSafe(commitUrl, { memCache: false }, schema_1.LastPipelineId)
.onValue((pipelineId) => {
options.pipeline_id = pipelineId;
});
if (options.pipeline_id !== undefined) {
break;
}
if (attempt >= retryTimes + 1) {
logger_1.logger.debug(`Pipeline not yet created after ${attempt} attempts`);
}
else {
logger_1.logger.debug(`Pipeline not yet created. Retrying ${attempt}`);
}
// give gitlab some time to create pipelines for the sha
await (0, promises_1.setTimeout)((0, number_1.parseInteger)(env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY, 1000));
}
}
catch (err) {
logger_1.logger.debug({ err });
logger_1.logger.warn('Failed to retrieve commit pipeline');
}
try {
await http_1.gitlabApi.postJson(url, { body: options });
// update status cache
await getStatus(branchName, false);
}
catch (err) /* v8 ignore start */ {
if (err.body?.message?.startsWith('Cannot transition status via :enqueue from :pending')) {
// https://gitlab.com/gitlab-org/gitlab-foss/issues/25807
logger_1.logger.debug('Ignoring status transition error');
}
else {
logger_1.logger.debug({ err });
logger_1.logger.warn('Failed to set branch status');
}
} /* v8 ignore stop */
}
// Issue
async function getIssueList() {
if (!config.issueList) {
const searchParams = {
per_page: '100',
state: 'opened',
};
if (!config.ignorePrAuthor) {
searchParams.scope = 'created_by_me';
}
const query = (0, url_1.getQueryString)(searchParams);
const res = await http_1.gitlabApi.getJsonUnchecked(`projects/${config.repository}/issues?${query}`, {
memCache: false,
paginate: true,
});
/* v8 ignore start */
if (!is_1.default.array(res.body)) {
logger_1.logger.warn({ responseBody: res.body }, 'Could not retrieve issue list');
return [];
} /* v8 ignore stop */
config.issueList = res.body.map((i) => ({
iid: i.iid,
title: i.title,
labels: i.labels,
}));
}
return config.issueList;
}
async function getIssue(number, useCache = true) {
try {
const opts = {};
/* v8 ignore start: temporary code */
if (useCache) {
opts.cacheProvider = memory_http_cache_provider_1.memCacheProvider;
}
else {
opts.memCache = false;
} /* v8 ignore stop */
const issueBody = (await http_1.gitlabApi.getJsonUnchecked(`projects/${config.repository}/issues/${number}`, opts)).body.description;
return {
number,
body: issueBody,
};
}
catch (err) /* v8 ignore start */ {
logger_1.logger.debug({ err, number }, 'Error getting issue');
return null;
} /* v8 ignore stop */
}
async function findIssue(title) {
logger_1.logger.debug(`findIssue(${title})`);
try {
const issueList = await getIssueList();
const issue = issueList.find((i) => i.title === title);
if (!issue) {
return null;
}
return await getIssue(issue.iid);
}
catch /* v8 ignore start */ {
logger_1.logger.warn('Error finding issue');
return null;
} /* v8 ignore stop */
}
async function ensureIssue({ title, reuseTitle, body, labels, confidential, }) {
logger_1.logger.debug(`ensureIssue()`);
const description = massageMarkdown((0, sanitize_1.sanitize)(body));
try {
const issueList = await getIssueList();
let issue = issueList.find((i) => i.title === title);
issue ??= issueList.find((i) => i.title === reuseTitle);
if (issue) {
const existingDescription = (await http_1.gitlabApi.getJsonUnchecked(`projects/${config.repository}/issues/${issue.iid}`)).body.description;
if (issue.title !== title || existingDescription !== description) {
logger_1.logger.debug('Updating issue');
await http_1.gitlabApi.putJson(`projects/${config.repository}/issues/${issue.iid}`, {
body: {
title,
description,
labels: (labels ?? issue.labels ?? []).join(','),
confidential: confidential ?? false,
},
});
return 'updated';
}
}
else {
await http_1.gitlabApi.postJson(`projects/${config.repository}/issues`, {
body: {
title,
description,
labels: (labels ?? []).join(','),
confidential: confidential ?? false,
},
});
logger_1.logger.info('Issue created');
// delete issueList so that it will be refetched as necessary
delete config.issueList;
return 'created';
}
}
catch (err) /* v8 ignore start */ {
if (err.message.startsWith('Issues are disabled for this repo')) {
logger_1.logger.debug(`Could not create issue: ${err.message}`);
}
else {
logger_1.logger.warn({ err }, 'Could not ensure issue');
}
} /* v8 ignore stop */
return null;
}
async function ensureIssueClosing(title) {
logger_1.logger.debug(`ensureIssueClosing()`);
const issueList = await getIssueList();
for (const issue of issueList) {
if (issue.title === title) {
logger_1.logger.debug({ issue }, 'Closing issue');
await http_1.gitlabApi.putJson(`projects/${config.repository}/issues/${issue.iid}`, {
body: { state_event: 'close' },
});
}
}
}
async function addAssignees(iid, assignees) {
try {
logger_1.logger.debug(`Adding assignees '${assignees.join(', ')}' to #${iid}`);
const assigneeIds = [];
for (const assignee of assignees) {
try {
const userId = await (0, http_1.getUserID)(assignee);
assigneeIds.push(userId);
}
catch (err) {
logger_1.logger.debug({ assignee, err }, 'getUserID() error');
logger_1.logger.warn({ assignee }, 'Failed to add assignee - could not get ID');
}
}
const url = `projects/${config.repository}/merge_requests/${iid}?${(0, url_1.getQueryString)({
'assignee_ids[]': assigneeIds,
})}`;
await http_1.gitlabApi.putJson(url);
}
catch (err) {
logger_1.logger.debug({ err }, 'addAssignees error');
logger_1.logger.warn({ iid, assignees }, 'Failed to add assignees');
}
}
async function addReviewers(iid, reviewers) {
logger_1.logger.debug(`Adding reviewers '${reviewers.join(', ')}' to #${iid}`);
if (semver_1.default.lt(defaults.version, '13.9.0')) {
logger_1.logger.warn({ version: defaults.version }, 'Adding reviewers is only available in GitLab 13.9 and onwards');
return;
}
let mr;
try {
mr = await (0, merge_request_1.getMR)(config.repository, iid);
}
catch (err) {
logger_1.logger.warn({ err }, 'Failed to get existing reviewers');
return;
}
mr.reviewers = (0, array_1.coerceArray)(mr.reviewers);
const existingReviewers = mr.reviewers.map((r) => r.username);
const existingReviewerIDs = mr.reviewers.map((r) => r.id);
// Figure out which reviewers (of the ones we want to add) are not already on the MR as a reviewer
const newReviewers = reviewers.filter((r) => !existingReviewers.includes(r));
// Gather the IDs for all the reviewers we want to add
let newReviewerIDs;
try {
newReviewerIDs = (await p.all(newReviewers.map((r) => async () => {
try {
return [await (0, http_1.getUserID)(r)];
}
catch {
// Unable to fetch userId, try resolve as a group
return (0, http_1.getMemberUserIDs)(r);
}
}))).flat();
}
catch (err) {
logger_1.logger.warn({ err }, 'Failed to get IDs of the new reviewers');
return;
}
// Multiple groups may have the same members, so
// filter out non-distinct values
newReviewerIDs = [...new Set(newReviewerIDs)];
try {
await (0, merge_request_1.updateMR)(config.repository, iid, {
reviewer_ids: [...existingReviewerIDs, ...newReviewerIDs],
});
}
catch (err) {
logger_1.logger.warn({ err }, 'Failed to add reviewers');
}
}
async function deleteLabel(issueNo, label) {
logger_1.logger.debug(`Deleting label ${label} from #${issueNo}`);
try {
const pr = await getPr(issueNo);
const labels = (0, array_1.coerceArray)(pr.labels)
.filter((l) => l !== label)
.join(',');
await http_1.gitlabApi.putJson(`projects/${config.repository}/merge_requests/${issueNo}`, {
body: { labels },
});
}
catch (err) /* v8 ignore start */ {
logger_1.logger.warn({ err, issueNo, label }, 'Failed to delete label');
} /* v8 ignore stop */
}
async function getComments(issueNo) {
// GET projects/:owner/:repo/merge_requests/:number/notes
logger_1.logger.debug(`Getting comments for #${issueNo}`);
const url = `projects/${config.repository}/merge_requests/${issueNo}/notes`;
const comments = (await http_1.gitlabApi.getJsonUnchecked(url, { paginate: true })).body;
logger_1.logger.debug(`Found ${comments.length} comments`);
return comments;
}
async function addComment(issueNo, body) {
// POST projects/:owner/:repo/merge_requests/:number/notes
await http_1.gitlabApi.postJson(`projects/${config.repository}/merge_requests/${issueNo}/notes`, {
body: { body },
});
}
async function editComment(issueNo, commentId, body) {
// PUT projects/:owner/:repo/merge_requests/:number/notes/:id
await http_1.gitlabApi.putJson(`projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`, {
body: { body },
});
}
async function deleteComment(issueNo, commentId) {
// DELETE projects/:owner/:repo/merge_requests/:number/notes/:id
await http_1.gitlabApi.deleteJson(`projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`);
}
async function ensureComment({ number, topic, content, }) {
const sanitizedContent = (0, sanitize_1.sanitize)(content);
const massagedTopic = topic
? topic
.replace((0, regex_1.regEx)(/Pull Request/g), 'Merge Request')
.replace((0, regex_1.regEx)(/PR/g), 'MR')
: topic;
const comments = await getComments(number);
let body;
let commentId;
let commentNeedsUpdating;
// TODO: types (#22198)
if (topic) {
logger_1.logger.debug(`Ensuring comment "${massagedTopic}" in #${number}`);
body = `### ${topic}\n\n${sanitizedContent}`;
body = (0, pr_body_1.smartTruncate)(body
.replace((0, regex_1.regEx)(/Pull Request/g), 'Merge Request')
.replace((0, regex_1.regEx)(/PR/g), 'MR'), maxBodyLength());
comments.forEach((comment) => {
if (comment.body.startsWith(`### ${massagedTopic}\n\n`)) {
commentId = comment.id;
commentNeedsUpdating = comment.body !== body;
}
});
}
else {
logger_1.logger.debug(`Ensuring content-only comment in #${number}`);
body = (0, pr_body_1.smartTruncate)(`${sanitizedContent}`, maxBodyLength());
comments.forEach((comment) => {
if (comment.body === body) {
commentId = comment.id;
commentNeedsUpdating = false;
}
});
}
if (!commentId) {
await addComment(number, body);
logger_1.logger.debug({ repository: config.repository, issueNo: number }, 'Added comment');
}
else if (commentNeedsUpdating) {
await editComment(number, commentId, body);
logger_1.logger.debug({ repository: config.repository, issueNo: number }, 'Updated comment');
}
else {
logger_1.logger.debug('Comment is already update-to-date');
}
return true;
}
async function ensureCommentRemoval(deleteConfig) {
const { number: issueNo } = deleteConfig;
const key = deleteConfig.type === 'by-topic'
? deleteConfig.topic
: deleteConfig.content;
logger_1.logger.debug(`Ensuring comment "${key}" in #${issueNo} is removed`);
const comments = await getComments(issueNo);
let commentId = null;
if (deleteConfig.type === 'by-topic') {
const byTopic = (comment) => comment.body.startsWith(`### ${deleteConfig.topic}\n\n`);
commentId = comments.find(byTopic)?.id;
}
else if (deleteConfig.type === 'by-content') {
const byContent = (comment) => comment.body.trim() === deleteConfig.content;
commentId = comments.find(byContent)?.id;
}
if (commentId) {
await deleteComment(issueNo, commentId);
}
}
async function filterUnavailableUsers(users) {
const filteredUsers = [];
for (const user of users) {
if (!(await (0, http_1.isUserBusy)(user))) {
filteredUsers.push(user);
}
}
return filteredUsers;
}
async function expandGroupMembers(reviewersOrAssignees) {
const expandedReviewersOrAssignees = [];
const normalizedReviewersOrAssigneesWithoutEmails = [];
// Skip passing user emails to Gitlab API, but include them in the final result
for (const reviewerOrAssignee of reviewersOrAssignees) {
if (reviewerOrAssignee.indexOf('@') > 0) {
expandedReviewersOrAssignees.push(reviewerOrAssignee);
continue;
}
// Normalize the potential group names before passing to Gitlab API
normalizedReviewersOrAssigneesWithoutEmails.push((0, common_1.noLeadingAtSymbol)(reviewerOrAssignee));
}
for (const reviewerOrAssignee of normalizedReviewersOrAssigneesWithoutEmails) {
try {
const members = await (0, http_1.getMemberUsernames)(reviewerOrAssignee);
expandedReviewersOrAssignees.push(...members);
}
catch (err) {
if (err.statusCode !== 404) {
logger_1.logger.debug({ err, reviewerOrAssignee }, 'Unable to fetch group');
}
expandedReviewersOrAssignees.push(reviewerOrAssignee);
}
}
return expandedReviewersOrAssignees;
}
//# sourceMappingURL=index.js.map