UNPKG

renovate

Version:

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

1,033 lines • 42.8 kB
"use strict"; 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 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 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; utils_1.defaults.hostType = 'gitlab'; utils_1.defaults.endpoint = 'https://gitlab.com/api/v4/'; utils_1.defaults.version = '0.0.0'; (0, gitlab_1.setBaseUrl)(utils_1.defaults.endpoint); } 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) { utils_1.defaults.endpoint = (0, url_1.ensureTrailingSlash)(endpoint); (0, gitlab_1.setBaseUrl)(utils_1.defaults.endpoint); } else { logger_1.logger.debug('Using default GitLab endpoint: ' + utils_1.defaults.endpoint); } const platformConfig = { endpoint: utils_1.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('-'); utils_1.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(utils_1.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); } // 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'; config.mergeTrainsEnabled = res.body.merge_trains_enabled ?? false; 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 = (0, utils_1.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, utils_1.defaults.endpoint), }; return repoConfig; } function getBranchForceRebase() { const forceRebase = config?.mergeMethod !== 'merge' && !config.mergeTrainsEnabled; 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) { const env = (0, env_1.getEnv)(); const opts = {}; if (env.RENOVATE_X_GITLAB_AUTO_APPROVE_TOKEN) { opts.token = env.RENOVATE_X_GITLAB_AUTO_APPROVE_TOKEN; } logger_1.logger.debug(`approveMr(${mrNumber})`); try { await http_1.gitlabApi.postJson(`projects/${config.repository}/merge_requests/${mrNumber}/approve`, opts); } 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(utils_1.defaults.version, '13.4.0')) { logger_1.logger.debug({ version: utils_1.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(utils_1.defaults.version, '13.9.0')) { logger_1.logger.warn({ version: utils_1.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