UNPKG

renovate

Version:

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

805 lines (804 loc) • 33.4 kB
import { __exportAll } from "../../../_virtual/_rolldown/runtime.js"; import { REPOSITORY_ACCESS_FORBIDDEN, REPOSITORY_ARCHIVED, REPOSITORY_CHANGED, REPOSITORY_DISABLED, REPOSITORY_EMPTY, REPOSITORY_MIRRORED, REPOSITORY_NOT_FOUND, TEMPORARY_ERROR } from "../../../constants/error-messages.js"; import { getEnv } from "../../../util/env.js"; import { regEx } from "../../../util/regex.js"; import { GlobalConfig } from "../../../config/global.js"; import { sanitize } from "../../../util/sanitize.js"; import { logger } from "../../../logger/index.js"; import { ensureTrailingSlash, getQueryString, parseUrl } from "../../../util/url.js"; import { noLeadingAtSymbol, parseJson } from "../../../util/common.js"; import { coerceArray } from "../../../util/array.js"; import { parseInteger } from "../../../util/number.js"; import { all } from "../../../util/promises.js"; import { memCacheProvider } from "../../../util/http/cache/memory-http-cache-provider.js"; import { branchExists, getBranchCommit, initRepo as initRepo$1 } from "../../../util/git/index.js"; import { setBaseUrl } from "../../../util/http/gitlab.js"; import { repoFingerprint } from "../util.js"; import { smartTruncate } from "../utils/pr-body.js"; import { getMemberUserIDs, getMemberUsernames, getUserID, gitlabApi, isUserBusy } from "./http.js"; import { getMR, updateMR } from "./merge-request.js"; import { LastPipelineId } from "./schema.js"; import { DRAFT_PREFIX, DRAFT_PREFIX_DEPRECATED, defaults, getRepoUrl, prInfo } from "./utils.js"; import { GitlabPrCache } from "./pr-cache.js"; import { extractRulesFromCodeOwnersLines } from "./code-owners.js"; import { isArray, isEmptyArray, isNonEmptyArray } from "@sindresorhus/is"; import semver from "semver"; import { setTimeout } from "node:timers/promises"; import pMap from "p-map"; //#region lib/modules/platform/gitlab/index.ts var gitlab_exports = /* @__PURE__ */ __exportAll({ addAssignees: () => addAssignees, addReviewers: () => addReviewers, createPr: () => createPr, deleteLabel: () => deleteLabel, ensureComment: () => ensureComment, ensureCommentRemoval: () => ensureCommentRemoval, ensureIssue: () => ensureIssue, ensureIssueClosing: () => ensureIssueClosing, expandGroupMembers: () => expandGroupMembers, extractRulesFromCodeOwnersLines: () => extractRulesFromCodeOwnersLines, filterUnavailableUsers: () => filterUnavailableUsers, findIssue: () => findIssue, findPr: () => findPr, getBranchForceRebase: () => getBranchForceRebase, getBranchPr: () => getBranchPr, getBranchStatus: () => getBranchStatus, getBranchStatusCheck: () => getBranchStatusCheck, getIssue: () => getIssue, getIssueList: () => getIssueList, getJsonFile: () => getJsonFile, getPr: () => getPr, getPrList: () => getPrList, getRawFile: () => getRawFile, getRepos: () => getRepos, id: () => id, initPlatform: () => initPlatform, initRepo: () => initRepo, labelCharLimit: () => labelCharLimit, massageMarkdown: () => massageMarkdown, maxBodyLength: () => maxBodyLength, mergePr: () => mergePr, reattemptPlatformAutomerge: () => reattemptPlatformAutomerge, resetPlatform: () => resetPlatform, setBranchStatus: () => setBranchStatus, updatePr: () => updatePr }); let config = {}; function resetPlatform() { config = {}; draftPrefix = DRAFT_PREFIX; defaults.hostType = "gitlab"; defaults.endpoint = "https://gitlab.com/api/v4/"; defaults.version = "0.0.0"; setBaseUrl(defaults.endpoint); } const id = "gitlab"; let draftPrefix = 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) logger.debug(`Using default GitLab endpoint: ${defaults.endpoint}`); else if (parseUrl(endpoint) === null) throw new Error(`Invalid GitLab endpoint URL: ${endpoint}`); else { defaults.endpoint = ensureTrailingSlash(endpoint); setBaseUrl(defaults.endpoint); } const platformConfig = { endpoint: defaults.endpoint }; let gitlabVersion; try { if (!gitAuthor) { const user = (await gitlabApi.getJsonUnchecked(`user`, { token })).body; platformConfig.gitAuthor = `${user.name} <${user.commit_email ?? user.email}>`; botUserName = user.name; } const env = getEnv(); /* v8 ignore next: experimental feature */ if (env.RENOVATE_X_PLATFORM_VERSION) gitlabVersion = env.RENOVATE_X_PLATFORM_VERSION; else gitlabVersion = (await gitlabApi.getJsonUnchecked("version", { token })).body.version; logger.debug(`GitLab version is: ${gitlabVersion}`); [gitlabVersion] = gitlabVersion.split("-"); defaults.version = gitlabVersion; } catch (err) { logger.debug({ err }, "Error authenticating with GitLab. Check that your token includes \"api\" permissions"); throw new Error("Init: Authentication failure"); } draftPrefix = semver.lt(defaults.version, "13.2.0") ? DRAFT_PREFIX_DEPRECATED : DRAFT_PREFIX; botUserName ??= username; return platformConfig; } async function getRepos(config) { logger.debug("Autodiscovering GitLab repositories"); const queryParams = { membership: true, per_page: 100, with_merge_requests_enabled: true, min_access_level: 30, archived: false, ...config?.sort && { order_by: config.sort }, ...config?.order && { sort: config.order } }; 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?${getQueryString(queryParams)}`)); } else urls.push(`projects?${getQueryString(queryParams)}`); try { const repos = (await pMap(urls, (url) => gitlabApi.getJsonUnchecked(url, { paginate: true }), { concurrency: 2 })).flatMap((response) => response.body); logger.debug(`Discovered ${repos.length} project(s)`); return repos.filter((repo) => !repo.mirror || config?.includeMirrors).map((repo) => repo.path_with_namespace); } catch (err) { logger.error({ err }, `GitLab getRepos error`); throw err; } } function urlEscape(str) { return str?.replace(regEx(/\//g), "%2F"); } async function getRawFile(fileName, repoName, branchOrTag) { const escapedFileName = urlEscape(fileName); const url = `projects/${urlEscape(repoName) ?? config.repository}/repository/files/${escapedFileName}?ref=${branchOrTag ?? `HEAD`}`; const buf = (await gitlabApi.getJsonUnchecked(url, { cacheProvider: memCacheProvider })).body.content; return Buffer.from(buf, "base64").toString(); } async function getJsonFile(fileName, repoName, branchOrTag) { return parseJson(await getRawFile(fileName, repoName, branchOrTag), fileName); } async function initRepo({ repository, cloneSubmodules, cloneSubmodulesFilter, gitUrl }) { config = {}; config.repository = urlEscape(repository); config.cloneSubmodules = cloneSubmodules; config.cloneSubmodulesFilter = cloneSubmodulesFilter; config.ignorePrAuthor = GlobalConfig.get("ignorePrAuthor"); let res; try { res = await gitlabApi.getJsonUnchecked(`projects/${config.repository}`); if (res.body.archived) { logger.debug("Repository is archived - throwing error to abort renovation"); throw new Error(REPOSITORY_ARCHIVED); } if (res.body.mirror && GlobalConfig.get("includeMirrors") !== true) { logger.debug("Repository is a mirror - throwing error to abort renovation"); throw new Error(REPOSITORY_MIRRORED); } if (res.body.repository_access_level === "disabled") { logger.debug("Repository portion of project is disabled - throwing error to abort renovation"); throw new Error(REPOSITORY_DISABLED); } if (res.body.merge_requests_access_level === "disabled") { logger.debug("MRs are disabled for the project - throwing error to abort renovation"); throw new Error(REPOSITORY_DISABLED); } if (res.body.default_branch === null || res.body.empty_repo) throw new Error(REPOSITORY_EMPTY); config.defaultBranch = res.body.default_branch; /* v8 ignore next */ if (!config.defaultBranch) { logger.warn({ resBody: res.body }, "Error fetching GitLab project"); throw new Error(TEMPORARY_ERROR); } 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.debug(`${repository} default branch = ${config.defaultBranch}`); logger.debug("Enabling Git FS"); const url = getRepoUrl(repository, gitUrl, res); await initRepo$1({ ...config, url }); } catch (err) /* v8 ignore next */ { logger.debug({ err }, "Caught initRepo error"); if (err.message.includes("HEAD is not a symbolic ref")) throw new Error(REPOSITORY_EMPTY); if (["archived", "empty"].includes(err.message)) throw err; if (err.statusCode === 403) throw new Error(REPOSITORY_ACCESS_FORBIDDEN); if (err.statusCode === 404) throw new Error(REPOSITORY_NOT_FOUND); if (err.message === "disabled") throw err; logger.debug({ err }, "Unknown GitLab initRepo error"); throw err; } return { defaultBranch: config.defaultBranch, isFork: !!res.body.forked_from_project, repoFingerprint: repoFingerprint(res.body.id, defaults.endpoint) }; } function getBranchForceRebase() { const forceRebase = config?.mergeMethod !== "merge" && !config.mergeTrainsEnabled; if (forceRebase) 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 = getBranchCommit(branchName); try { const url = `projects/${config.repository}/repository/commits/${branchSha}/statuses`; const opts = { paginate: true }; if (useCache) opts.cacheProvider = memCacheProvider; else opts.memCache = false; return (await gitlabApi.getJsonUnchecked(url, opts)).body; } catch (err) /* v8 ignore next */ { logger.debug({ err }, "Error getting commit status"); if (err.response?.statusCode === 404) throw new Error(REPOSITORY_CHANGED); throw err; } } const gitlabToRenovateStatusMapping = { pending: "yellow", created: "yellow", manual: "yellow", running: "yellow", waiting_for_resource: "yellow", success: "green", failed: "red", canceled: "red", skipped: "red", scheduled: "yellow" }; async function getBranchStatus(branchName, internalChecksAsSuccess) { logger.debug(`getBranchStatus(${branchName})`); if (!branchExists(branchName)) throw new Error(REPOSITORY_CHANGED); const branchStatuses = await getStatus(branchName); /* v8 ignore next */ if (!isArray(branchStatuses)) { logger.warn({ branchName, branchStatuses }, "Empty or unexpected branch statuses"); return "yellow"; } logger.debug(`Got res with ${branchStatuses.length} results`); const mr = await getBranchPr(branchName); if (mr && mr.sha !== mr.headPipelineSha && mr.headPipelineStatus) { logger.debug("Merge request head pipeline has different sha to commit, assuming merged results pipeline"); branchStatuses.push({ status: mr.headPipelineStatus, name: "head_pipeline" }); } const res = branchStatuses.filter((check) => check.status !== "skipped"); if (res.length === 0) return "yellow"; if (!internalChecksAsSuccess && branchStatuses.every((check) => check.name?.startsWith("renovate/") && gitlabToRenovateStatusMapping[check.status] === "green")) { logger.debug("Successful checks are all internal renovate/ checks, so returning \"pending\" branch status"); return "yellow"; } let status = "green"; res.filter((check) => !check.allow_failure).forEach((check) => { // v8 ignore else -- TODO: add test #40625 if (status !== "red") { let mappedStatus = gitlabToRenovateStatusMapping[check.status]; if (!mappedStatus) { logger.warn({ check }, "Could not map GitLab check.status to Renovate status"); mappedStatus = "yellow"; } if (mappedStatus !== "green") { logger.trace({ check }, "Found non-green check"); status = mappedStatus; } } }); return status; } async function getPrList() { return await GitlabPrCache.getPrs(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 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 all(existingRegularApproverRules.map((rule) => async () => { await gitlabApi.deleteJson(`${url}/${rule.id}`); })); if (existingAnyApproverRule) { await gitlabApi.putJson(`${url}/${existingAnyApproverRule.id}`, { body: { ...existingAnyApproverRule, approvals_required: 0 } }); return; } if (!rules?.find(({ name }) => name === ruleName)) await gitlabApi.postJson(url, { body: { name: ruleName, approvals_required: 0 } }); } catch (err) { logger.warn({ err }, "GitLab: Error adding approval rule"); } } async function tryPrAutomerge(pr, platformPrOptions) { try { if (platformPrOptions?.gitLabIgnoreApprovals) await ignoreApprovals(pr); if (platformPrOptions?.usePlatformAutomerge) { const desiredDetailedMergeStatus = [ "mergeable", "ci_still_running", "not_approved" ]; const desiredPipelineStatus = ["failed", "running"]; const desiredStatus = "can_be_merged"; const env = getEnv(); const retryTimes = parseInteger(env.RENOVATE_X_GITLAB_AUTO_MERGEABLE_CHECK_ATTEMPS, 5); const mergeDelay = parseInteger(env.RENOVATE_X_GITLAB_MERGE_REQUEST_DELAY, 250); for (let attempt = 1; attempt <= retryTimes; attempt += 1) { const { body } = await gitlabApi.getJsonUnchecked(`projects/${config.repository}/merge_requests/${pr}`, { memCache: false }); if (body.merge_when_pipeline_succeeds === true) { logger.debug("Skipping automerge retry - merge_when_pipeline_succeeds already enabled"); return; } const use_detailed_merge_status = !!body.detailed_merge_status; const detailed_merge_status_check = use_detailed_merge_status && desiredDetailedMergeStatus.includes(body.detailed_merge_status); const deprecated_merge_status_check = !use_detailed_merge_status && body.merge_status === desiredStatus; if ((detailed_merge_status_check || deprecated_merge_status_check) && body.pipeline !== null && desiredPipelineStatus.includes(body.pipeline.status)) break; logger.debug(`PR not yet in mergeable state. Retrying ${attempt}`); await setTimeout(mergeDelay * attempt ** 2); } const useMergeTrain = config.mergeTrainsEnabled && !semver.lt(defaults.version, "17.11.0"); if (config.mergeTrainsEnabled && !useMergeTrain) logger.once.warn({ version: defaults.version }, "Merge trains require GitLab 17.11.0 or later, falling back to /merge endpoint"); for (let attempt = 1; attempt <= retryTimes; attempt += 1) { try { if (useMergeTrain) await gitlabApi.postJson(`projects/${config.repository}/merge_trains/merge_requests/${pr}`, { body: { auto_merge: true } }); else await gitlabApi.putJson(`projects/${config.repository}/merge_requests/${pr}/merge`, { body: { should_remove_source_branch: true, merge_when_pipeline_succeeds: true } }); break; } catch (err) { logger.debug({ err }, `Automerge on PR creation failed. Retrying ${attempt}`); } await setTimeout(mergeDelay * attempt ** 2); } } } catch (err) /* v8 ignore next */ { logger.debug({ err }, "Automerge on PR creation failed"); } } async function approveMr(mrNumber) { const env = getEnv(); const opts = {}; if (env.RENOVATE_X_GITLAB_AUTO_APPROVE_TOKEN) opts.token = env.RENOVATE_X_GITLAB_AUTO_APPROVE_TOKEN; logger.debug(`approveMr(${mrNumber})`); try { await gitlabApi.postJson(`projects/${config.repository}/merge_requests/${mrNumber}/approve`, opts); } catch (err) { 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 = sanitize(rawDescription); logger.debug(`Creating Merge Request: ${title}`); const pr = prInfo((await 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 } })).body); await GitlabPrCache.setPr(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.debug(`getPr(${iid})`); return prInfo(await getMR(config.repository, iid)); } 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" }[state]; const body = { title, description: 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 gitlabApi.putJson(`projects/${config.repository}/merge_requests/${iid}`, { body })).body; const updatedPr = prInfo(updatedPrInfo); await GitlabPrCache.setPr(gitlabApi, config.repository, botUserName, updatedPr, !!config.ignorePrAuthor); if (platformPrOptions?.autoApprove) await approveMr(iid); } async function reattemptPlatformAutomerge({ number: iid, platformPrOptions }) { await tryPrAutomerge(iid, platformPrOptions); logger.debug(`PR platform automerge re-attempted...prNo: ${iid}`); } async function mergePr({ id }) { try { await gitlabApi.putJson(`projects/${config.repository}/merge_requests/${id}/merge`, { body: { should_remove_source_branch: true } }); return true; } catch (err) /* v8 ignore next */ { if (err.statusCode === 401) { logger.debug("No permissions to merge PR"); return false; } if (err.statusCode === 406) { logger.debug({ err }, "PR not acceptable for merging"); return false; } logger.debug({ err }, "merge PR error"); logger.debug("PR merge failed"); return false; } } function massageMarkdown(input) { return smartTruncate(input.replace(regEx(/Pull Request/g), "Merge Request").replace(regEx(/\bPR: #/g), "MR: !").replace(regEx(/\bPR\b/g), "MR").replace(regEx(/\bPRs\b/g), "MRs").replace(regEx(/\]\(\.\.\/pull\//g), "](!").replace(regEx(/\]\(\.\.\/issues\//g), "](#").replace(regEx(/\u0000/g), ""), maxBodyLength()); } function maxBodyLength() { if (semver.lt(defaults.version, "13.4.0")) { logger.debug({ version: defaults.version }, "GitLab versions earlier than 13.4 have issues with long descriptions, truncating to 25K characters"); return 25e3; } else return 1e6; } /* v8 ignore next: no need to test */ function labelCharLimit() { return 255; } 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.debug(`findPr(${branchName}, ${prTitle}, ${state})`); if (includeOtherAuthors) { const { body: mrList } = await gitlabApi.getJsonUnchecked(`projects/${config.repository}/merge_requests?source_branch=${branchName}&state=opened`); if (!mrList.length) { logger.debug(`No MR found for branch ${branchName}`); return null; } return prInfo(mrList[0]); } return (await getPrList()).find((p) => p.sourceBranch === branchName && (!prTitle || p.title.toUpperCase() === prTitle.toUpperCase()) && matchesState(p.state, state)) ?? null; } async function getBranchPr(branchName) { logger.debug(`getBranchPr(${branchName})`); const existingPr = await findPr({ branchName, state: "open" }); return existingPr ? getPr(existingPr.number) : null; } async function getBranchStatusCheck(branchName, context) { const res = await getStatus(branchName, false); 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 }) { const branchSha = getBranchCommit(branchName); if (!branchSha) { logger.warn("Failed to get the branch commit SHA"); return; } 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 }; // v8 ignore else -- TODO: add test #40625 if (targetUrl) options.target_url = targetUrl; const env = getEnv(); const retryTimes = 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 gitlabApi.getJsonSafe(commitUrl, { memCache: false }, LastPipelineId).onValue((pipelineId) => { options.pipeline_id = pipelineId; }); if (options.pipeline_id !== void 0) break; if (attempt >= retryTimes + 1) logger.debug(`Pipeline not yet created after ${attempt} attempts`); else logger.debug(`Pipeline not yet created. Retrying ${attempt}`); await setTimeout(parseInteger(env.RENOVATE_X_GITLAB_BRANCH_STATUS_DELAY, 1e3)); } } catch (err) { logger.debug({ err }); logger.warn("Failed to retrieve commit pipeline"); } if (options.pipeline_id === void 0 && env.RENOVATE_X_GITLAB_SKIP_STATUS_WITHOUT_PIPELINE === "true") { logger.debug("Skipping branch status update because no pipeline was found"); return; } try { await gitlabApi.postJson(url, { body: options }); await getStatus(branchName, false); } catch (err) /* v8 ignore next */ { if (err.body?.message?.startsWith("Cannot transition status via :enqueue from :pending")) logger.debug("Ignoring status transition error"); else { logger.debug({ err }); logger.warn("Failed to set branch status"); } } } async function getIssueList() { // v8 ignore else -- TODO: add test #40625 if (!config.issueList) { const searchParams = { per_page: "100", state: "opened" }; // v8 ignore else -- TODO: add test #40625 if (!config.ignorePrAuthor) searchParams.scope = "created_by_me"; const query = getQueryString(searchParams); const res = await gitlabApi.getJsonUnchecked(`projects/${config.repository}/issues?${query}`, { memCache: false, paginate: true }); /* v8 ignore next */ if (!isArray(res.body)) { logger.warn({ responseBody: res.body }, "Could not retrieve issue list"); return []; } 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 next: temporary code */ if (useCache) opts.cacheProvider = memCacheProvider; else opts.memCache = false; return { number, body: (await gitlabApi.getJsonUnchecked(`projects/${config.repository}/issues/${number}`, opts)).body.description }; } catch (err) /* v8 ignore next */ { logger.debug({ err, number }, "Error getting issue"); return null; } } async function findIssue(title) { logger.debug(`findIssue(${title})`); try { const issue = (await getIssueList()).find((i) => i.title === title); if (!issue) return null; return await getIssue(issue.iid); } catch /* v8 ignore next */ { logger.warn("Error finding issue"); return null; } } async function ensureIssue({ title, reuseTitle, body, labels, confidential }) { logger.debug(`ensureIssue()`); const description = massageMarkdown(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 gitlabApi.getJsonUnchecked(`projects/${config.repository}/issues/${issue.iid}`)).body.description; if (issue.title !== title || existingDescription !== description) { logger.debug("Updating issue"); await gitlabApi.putJson(`projects/${config.repository}/issues/${issue.iid}`, { body: { title, description, labels: (labels ?? issue.labels ?? []).join(","), confidential: confidential ?? false } }); return "updated"; } } else { await gitlabApi.postJson(`projects/${config.repository}/issues`, { body: { title, description, labels: (labels ?? []).join(","), confidential: confidential ?? false } }); logger.info("Issue created"); delete config.issueList; return "created"; } } catch (err) /* v8 ignore next */ { if (err.message.startsWith("Issues are disabled for this repo")) logger.debug(`Could not create issue: ${err.message}`); else logger.warn({ err }, "Could not ensure issue"); } return null; } async function ensureIssueClosing(title) { logger.debug(`ensureIssueClosing()`); const issueList = await getIssueList(); for (const issue of issueList) if (issue.title === title) { logger.debug({ issue }, "Closing issue"); await gitlabApi.putJson(`projects/${config.repository}/issues/${issue.iid}`, { body: { state_event: "close" } }); } } async function addAssignees(iid, assignees) { try { logger.debug(`Adding assignees '${assignees.join(", ")}' to #${iid}`); const assigneeIds = []; for (const assignee of assignees) try { const userId = await getUserID(assignee); assigneeIds.push(userId); } catch (err) { logger.debug({ assignee, err }, "getUserID() error"); logger.warn({ assignee }, "Failed to add assignee - could not get ID"); } const url = `projects/${config.repository}/merge_requests/${iid}?${getQueryString({ "assignee_ids[]": assigneeIds })}`; await gitlabApi.putJson(url); } catch (err) { logger.debug({ err }, "addAssignees error"); logger.warn({ iid, assignees }, "Failed to add assignees"); } } async function addReviewers(iid, reviewers) { logger.debug(`Adding reviewers '${reviewers.join(", ")}' to #${iid}`); if (semver.lt(defaults.version, "13.9.0")) { logger.warn({ version: defaults.version }, "Adding reviewers is only available in GitLab 13.9 and onwards"); return; } let mr; try { mr = await getMR(config.repository, iid); } catch (err) { logger.warn({ err }, "Failed to get existing reviewers"); return; } mr.reviewers = coerceArray(mr.reviewers); const existingReviewers = mr.reviewers.map((r) => r.username); const existingReviewerIDs = mr.reviewers.map((r) => r.id); const newReviewers = reviewers.filter((r) => !existingReviewers.includes(r)); let newReviewerIDs; newReviewerIDs = (await all(newReviewers.map((r) => async () => { try { return [await getUserID(r)]; } catch { return getMemberUserIDs(r); } }))).flat(); if (isNonEmptyArray(newReviewers) && isEmptyArray(newReviewerIDs)) { logger.warn("Failed to get IDs of the new reviewers"); return; } newReviewerIDs = [...new Set(newReviewerIDs)]; try { await updateMR(config.repository, iid, { reviewer_ids: [...existingReviewerIDs, ...newReviewerIDs] }); } catch (err) { logger.warn({ err }, "Failed to add reviewers"); } } async function deleteLabel(issueNo, label) { logger.debug(`Deleting label ${label} from #${issueNo}`); try { const labels = coerceArray((await getPr(issueNo)).labels).filter((l) => l !== label).join(","); await gitlabApi.putJson(`projects/${config.repository}/merge_requests/${issueNo}`, { body: { labels } }); } catch (err) /* v8 ignore next */ { logger.warn({ err, issueNo, label }, "Failed to delete label"); } } async function getComments(issueNo) { logger.debug(`Getting comments for #${issueNo}`); const url = `projects/${config.repository}/merge_requests/${issueNo}/notes`; const comments = (await gitlabApi.getJsonUnchecked(url, { paginate: true })).body; logger.debug(`Found ${comments.length} comments`); return comments; } async function addComment(issueNo, body) { await gitlabApi.postJson(`projects/${config.repository}/merge_requests/${issueNo}/notes`, { body: { body } }); } async function editComment(issueNo, commentId, body) { await gitlabApi.putJson(`projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`, { body: { body } }); } async function deleteComment(issueNo, commentId) { await gitlabApi.deleteJson(`projects/${config.repository}/merge_requests/${issueNo}/notes/${commentId}`); } async function ensureComment({ number, topic, content }) { const sanitizedContent = sanitize(content); const massagedTopic = topic ? topic.replace(regEx(/Pull Request/g), "Merge Request").replace(regEx(/PR/g), "MR") : topic; const comments = await getComments(number); let body; let commentId; let commentNeedsUpdating; if (topic) { logger.debug(`Ensuring comment "${massagedTopic}" in #${number}`); body = `### ${topic}\n\n${sanitizedContent}`; body = smartTruncate(body.replace(regEx(/Pull Request/g), "Merge Request").replace(regEx(/PR/g), "MR"), maxBodyLength()); comments.forEach((comment) => { // v8 ignore else -- TODO: add test #40625 if (comment.body.startsWith(`### ${massagedTopic}\n\n`)) { commentId = comment.id; commentNeedsUpdating = comment.body !== body; } }); } else { logger.debug(`Ensuring content-only comment in #${number}`); body = smartTruncate(`${sanitizedContent}`, maxBodyLength()); comments.forEach((comment) => { // v8 ignore else -- TODO: add test #40625 if (comment.body === body) { commentId = comment.id; commentNeedsUpdating = false; } }); } if (!commentId) { await addComment(number, body); logger.debug({ repository: config.repository, issueNo: number }, "Added comment"); } else if (commentNeedsUpdating) { await editComment(number, commentId, body); logger.debug({ repository: config.repository, issueNo: number }, "Updated comment"); } else logger.debug("Comment is already up-to-date"); return true; } async function ensureCommentRemoval(deleteConfig) { const { number: issueNo } = deleteConfig; const key = deleteConfig.type === "by-topic" ? deleteConfig.topic : deleteConfig.content; logger.debug(`Ensuring comment "${key}" in #${issueNo} is removed`); const comments = await getComments(issueNo); let commentId = null; // v8 ignore else -- TODO: add test #40625 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; } // v8 ignore else -- TODO: add test #40625 if (commentId) await deleteComment(issueNo, commentId); } async function filterUnavailableUsers(users) { const filteredUsers = []; for (const user of users) if (!await isUserBusy(user)) filteredUsers.push(user); return filteredUsers; } async function expandGroupMembers(reviewersOrAssignees) { const expandedReviewersOrAssignees = []; const normalizedReviewersOrAssigneesWithoutEmails = []; for (const reviewerOrAssignee of reviewersOrAssignees) { if (reviewerOrAssignee.indexOf("@") > 0) { expandedReviewersOrAssignees.push(reviewerOrAssignee); continue; } normalizedReviewersOrAssigneesWithoutEmails.push(noLeadingAtSymbol(reviewerOrAssignee)); } for (const reviewerOrAssignee of normalizedReviewersOrAssigneesWithoutEmails) try { const members = await getMemberUsernames(reviewerOrAssignee); expandedReviewersOrAssignees.push(...members); } catch (err) { if (err.statusCode !== 404) logger.debug({ err, reviewerOrAssignee }, "Unable to fetch group"); expandedReviewersOrAssignees.push(reviewerOrAssignee); } return expandedReviewersOrAssignees; } //#endregion export { addAssignees, addReviewers, createPr, deleteLabel, ensureComment, ensureCommentRemoval, ensureIssue, ensureIssueClosing, expandGroupMembers, extractRulesFromCodeOwnersLines, filterUnavailableUsers, findIssue, findPr, getBranchForceRebase, getBranchPr, getBranchStatus, getBranchStatusCheck, getIssue, getIssueList, getJsonFile, getPr, getPrList, getRawFile, getRepos, gitlab_exports, id, initPlatform, initRepo, labelCharLimit, massageMarkdown, maxBodyLength, mergePr, reattemptPlatformAutomerge, resetPlatform, setBranchStatus, updatePr }; //# sourceMappingURL=index.js.map