renovate
Version:
Automated dependency updates. Flexible so you don't need to be.
1,178 lines • 68 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.id = void 0;
exports.resetConfigs = resetConfigs;
exports.isGHApp = isGHApp;
exports.detectGhe = detectGhe;
exports.initPlatform = initPlatform;
exports.getRepos = getRepos;
exports.getRawFile = getRawFile;
exports.getJsonFile = getJsonFile;
exports.listForks = listForks;
exports.findFork = findFork;
exports.createFork = createFork;
exports.initRepo = initRepo;
exports.getBranchForceRebase = getBranchForceRebase;
exports.getPr = getPr;
exports.getPrList = getPrList;
exports.findPr = findPr;
exports.getBranchPr = getBranchPr;
exports.tryReuseAutoclosedPr = tryReuseAutoclosedPr;
exports.getBranchStatus = getBranchStatus;
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.addLabels = addLabels;
exports.deleteLabel = deleteLabel;
exports.ensureComment = ensureComment;
exports.ensureCommentRemoval = ensureCommentRemoval;
exports.createPr = createPr;
exports.updatePr = updatePr;
exports.reattemptPlatformAutomerge = reattemptPlatformAutomerge;
exports.mergePr = mergePr;
exports.massageMarkdown = massageMarkdown;
exports.maxBodyLength = maxBodyLength;
exports.getVulnerabilityAlerts = getVulnerabilityAlerts;
exports.commitFiles = commitFiles;
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 semver_1 = tslib_1.__importDefault(require("semver"));
const error_messages_1 = require("../../../constants/error-messages");
const logger_1 = require("../../../logger");
const external_host_error_1 = require("../../../types/errors/external-host-error");
const check_token_1 = require("../../../util/check-token");
const coerce_1 = require("../../../util/coerce");
const common_1 = require("../../../util/common");
const env_1 = require("../../../util/env");
const git = tslib_1.__importStar(require("../../../util/git"));
const git_1 = require("../../../util/git");
const hostRules = tslib_1.__importStar(require("../../../util/host-rules"));
const memory_http_cache_provider_1 = require("../../../util/http/cache/memory-http-cache-provider");
const repository_http_cache_provider_1 = require("../../../util/http/cache/repository-http-cache-provider");
const githubHttp = tslib_1.__importStar(require("../../../util/http/github"));
const object_1 = require("../../../util/object");
const regex_1 = require("../../../util/regex");
const sanitize_1 = require("../../../util/sanitize");
const string_1 = require("../../../util/string");
const url_1 = require("../../../util/url");
const limits_1 = require("../../../workers/global/limits");
const util_1 = require("../util");
const github_alerts_1 = require("../utils/github-alerts");
const pr_body_1 = require("../utils/pr-body");
const branch_1 = require("./branch");
const common_2 = require("./common");
const graphql_1 = require("./graphql");
const issue_1 = require("./issue");
const massage_markdown_links_1 = require("./massage-markdown-links");
const pr_1 = require("./pr");
const schema_1 = require("./schema");
const user_1 = require("./user");
exports.id = 'github';
let config;
let platformConfig;
// GitHub's max is 60k but in the hosted app we've observed that content-length is ~1k longer
const GitHubMaxPrBodyLen = 58000;
function resetConfigs() {
config = {};
platformConfig = {
hostType: 'github',
endpoint: 'https://api.github.com/',
};
}
resetConfigs();
function escapeHash(input) {
return input?.replace((0, regex_1.regEx)(/#/g), '%23');
}
function isGHApp() {
return !!platformConfig.isGHApp;
}
async function detectGhe(token) {
platformConfig.isGhe =
node_url_1.default.parse(platformConfig.endpoint).host !== 'api.github.com';
if (platformConfig.isGhe) {
const gheHeaderKey = 'x-github-enterprise-version';
const gheQueryRes = await common_2.githubApi.headJson('/', { token });
const gheHeaders = (0, object_1.coerceObject)(gheQueryRes?.headers);
const [, gheVersion] = Object.entries(gheHeaders).find(([k]) => k.toLowerCase() === gheHeaderKey) ?? [];
platformConfig.gheVersion = semver_1.default.valid(gheVersion) ?? null;
logger_1.logger.debug(`Detected GitHub Enterprise Server, version: ${platformConfig.gheVersion}`);
}
}
async function initPlatform({ endpoint, token: originalToken, username, gitAuthor, }) {
let token = originalToken;
if (!token) {
throw new Error('Init: You must configure a GitHub token');
}
token = token.replace(/^ghs_/, 'x-access-token:ghs_');
platformConfig.isGHApp = token.startsWith('x-access-token:');
if (endpoint) {
platformConfig.endpoint = (0, url_1.ensureTrailingSlash)(endpoint);
githubHttp.setBaseUrl(platformConfig.endpoint);
}
else {
logger_1.logger.debug('Using default github endpoint: ' + platformConfig.endpoint);
}
await detectGhe(token);
/**
* GHE requires version >=3.10 to support fine-grained access tokens
* https://docs.github.com/en/enterprise-server@3.10/admin/release-notes#authentication
*/
if ((0, check_token_1.isGithubFineGrainedPersonalAccessToken)(token) &&
platformConfig.isGhe &&
(!platformConfig.gheVersion ||
semver_1.default.lt(platformConfig.gheVersion, '3.10.0'))) {
throw new Error('Init: Fine-grained Personal Access Tokens do not support GitHub Enterprise Server API version <3.10 and cannot be used with Renovate.');
}
let renovateUsername;
if (username) {
renovateUsername = username;
}
else if (platformConfig.isGHApp) {
platformConfig.userDetails ??= await (0, user_1.getAppDetails)(token);
renovateUsername = platformConfig.userDetails.username;
}
else {
platformConfig.userDetails ??= await (0, user_1.getUserDetails)(platformConfig.endpoint, token);
renovateUsername = platformConfig.userDetails.username;
}
let discoveredGitAuthor;
if (!gitAuthor) {
if (platformConfig.isGHApp) {
platformConfig.userDetails ??= await (0, user_1.getAppDetails)(token);
const ghHostname = platformConfig.isGhe
? node_url_1.default.parse(platformConfig.endpoint).hostname
: 'github.com';
discoveredGitAuthor = `${platformConfig.userDetails.name} <${platformConfig.userDetails.id}+${platformConfig.userDetails.username}@users.noreply.${ghHostname}>`;
}
else {
platformConfig.userDetails ??= await (0, user_1.getUserDetails)(platformConfig.endpoint, token);
platformConfig.userEmail ??= await (0, user_1.getUserEmail)(platformConfig.endpoint, token);
if (platformConfig.userEmail) {
discoveredGitAuthor = `${platformConfig.userDetails.name} <${platformConfig.userEmail}>`;
}
}
}
logger_1.logger.debug({ platformConfig, renovateUsername }, 'Platform config');
const platformResult = {
endpoint: platformConfig.endpoint,
gitAuthor: gitAuthor ?? discoveredGitAuthor,
renovateUsername,
token,
};
if ((0, env_1.getEnv)().RENOVATE_X_GITHUB_HOST_RULES &&
platformResult.endpoint === 'https://api.github.com/') {
logger_1.logger.debug('Adding GitHub token as GHCR password');
platformResult.hostRules = [
{
matchHost: 'ghcr.io',
hostType: 'docker',
username: 'USERNAME',
password: token.replace(/^x-access-token:/, ''),
},
];
logger_1.logger.debug('Adding GitHub token as npm.pkg.github.com Basic token');
platformResult.hostRules.push({
matchHost: 'npm.pkg.github.com',
hostType: 'npm',
token: token.replace(/^x-access-token:/, ''),
});
const usernamePasswordHostTypes = ['rubygems', 'maven', 'nuget'];
for (const hostType of usernamePasswordHostTypes) {
logger_1.logger.debug(`Adding GitHub token as ${hostType}.pkg.github.com password`);
platformResult.hostRules.push({
hostType,
matchHost: `${hostType}.pkg.github.com`,
username: renovateUsername,
password: token.replace(/^x-access-token:/, ''),
});
}
}
return platformResult;
}
async function fetchRepositories() {
try {
if (isGHApp()) {
const res = await common_2.githubApi.getJsonUnchecked(`installation/repositories?per_page=100`, {
paginationField: 'repositories',
paginate: 'all',
});
return res.body.repositories;
}
else {
const res = await common_2.githubApi.getJsonUnchecked(`user/repos?per_page=100`, { paginate: 'all' });
return res.body;
}
}
catch (err) /* v8 ignore start */ {
logger_1.logger.error({ err }, `GitHub getRepos error`);
throw err;
} /* v8 ignore stop */
}
// Get all repositories that the user has access to
async function getRepos(config) {
logger_1.logger.debug('Autodiscovering GitHub repositories');
const nonEmptyRepositories = (await fetchRepositories()).filter(is_1.default.nonEmptyObject);
const nonArchivedRepositories = nonEmptyRepositories.filter((repo) => !repo.archived);
if (nonArchivedRepositories.length < nonEmptyRepositories.length) {
logger_1.logger.debug(`Filtered out ${nonEmptyRepositories.length - nonArchivedRepositories.length} archived repositories`);
}
if (!config?.topics) {
return nonArchivedRepositories.map((repo) => repo.full_name);
}
logger_1.logger.debug({ topics: config.topics }, 'Filtering by topics');
const topicRepositories = nonArchivedRepositories.filter((repo) => repo.topics?.some((topic) => config?.topics?.includes(topic)));
if (topicRepositories.length < nonArchivedRepositories.length) {
logger_1.logger.debug(`Filtered out ${nonArchivedRepositories.length - topicRepositories.length} repositories not matching topic filters`);
}
return topicRepositories.map((repo) => repo.full_name);
}
async function getBranchProtection(branchName) {
/* v8 ignore start */
if (config.parentRepo) {
return {};
} /* v8 ignore stop */
const res = await common_2.githubApi.getJsonUnchecked(`repos/${config.repository}/branches/${escapeHash(branchName)}/protection`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider });
return res.body;
}
async function getRawFile(fileName, repoName, branchOrTag) {
const repo = repoName ?? config.repository;
// only use cache for the same org
const httpOptions = {};
const isSameOrg = repo?.split('/')?.[0] === config.repositoryOwner;
if (isSameOrg) {
httpOptions.cacheProvider = repository_http_cache_provider_1.repoCacheProvider;
}
let url = `repos/${repo}/contents/${fileName}`;
if (branchOrTag) {
url += `?ref=` + branchOrTag;
}
const res = await common_2.githubApi.getJsonUnchecked(url, httpOptions);
const buf = res.body.content;
const str = (0, string_1.fromBase64)(buf);
return str;
}
async function getJsonFile(fileName, repoName, branchOrTag) {
const raw = await getRawFile(fileName, repoName, branchOrTag);
return (0, common_1.parseJson)(raw, fileName);
}
async function listForks(token, repository) {
try {
// Get list of existing repos
const url = `repos/${repository}/forks?per_page=100`;
const repos = (await common_2.githubApi.getJsonUnchecked(url, {
token,
paginate: true,
pageLimit: 100,
})).body;
logger_1.logger.debug(`Found ${repos.length} forked repo(s)`);
return repos;
}
catch (err) {
if (err.statusCode === 404) {
logger_1.logger.debug('Cannot list repo forks - it is likely private');
}
else {
logger_1.logger.debug({ err }, 'Unknown error listing repository forks');
}
throw new Error(error_messages_1.REPOSITORY_CANNOT_FORK);
}
}
async function findFork(token, repository, forkOrg) {
const forks = await listForks(token, repository);
if (forkOrg) {
logger_1.logger.debug(`Searching for forked repo in forkOrg (${forkOrg})`);
const forkedRepo = forks.find((repo) => repo.owner.login === forkOrg);
if (forkedRepo) {
logger_1.logger.debug(`Found repo in forkOrg: ${forkedRepo.full_name}`);
return forkedRepo;
}
logger_1.logger.debug(`No repo found in forkOrg`);
}
logger_1.logger.debug(`Searching for forked repo in user account`);
try {
const { username } = await (0, user_1.getUserDetails)(platformConfig.endpoint, token);
const forkedRepo = forks.find((repo) => repo.owner.login === username);
if (forkedRepo) {
logger_1.logger.debug(`Found repo in user account: ${forkedRepo.full_name}`);
return forkedRepo;
}
}
catch {
throw new Error(error_messages_1.REPOSITORY_CANNOT_FORK);
}
logger_1.logger.debug(`No repo found in user account`);
return null;
}
async function createFork(token, repository, forkOrg) {
let forkedRepo;
try {
forkedRepo = (await common_2.githubApi.postJson(`repos/${repository}/forks`, {
token,
body: {
organization: forkOrg ?? undefined,
name: config.parentRepo.replace('/', '-_-'),
default_branch_only: true, // no baseBranches support yet
},
})).body;
}
catch (err) {
logger_1.logger.debug({ err }, 'Error creating fork');
}
if (!forkedRepo) {
throw new Error(error_messages_1.REPOSITORY_CANNOT_FORK);
}
logger_1.logger.info({ forkedRepo: forkedRepo.full_name }, 'Created forked repo');
logger_1.logger.debug(`Sleeping 30s after creating fork`);
await (0, promises_1.setTimeout)(30000);
return forkedRepo;
}
// Initialize GitHub by getting base branch and SHA
async function initRepo({ endpoint, repository, forkCreation, forkOrg, forkToken, renovateUsername, cloneSubmodules, cloneSubmodulesFilter, ignorePrAuthor, }) {
logger_1.logger.debug(`initRepo("${repository}")`);
// config is used by the platform api itself, not necessary for the app layer to know
config = {
repository,
cloneSubmodules,
cloneSubmodulesFilter,
ignorePrAuthor,
};
/* v8 ignore start */
if (endpoint) {
// Necessary for Renovate Pro - do not remove
logger_1.logger.debug(`Overriding default GitHub endpoint with ${endpoint}`);
platformConfig.endpoint = endpoint;
githubHttp.setBaseUrl(endpoint);
} /* v8 ignore stop */
const opts = hostRules.find({
hostType: 'github',
url: platformConfig.endpoint,
readOnly: true,
});
config.renovateUsername = renovateUsername;
[config.repositoryOwner, config.repositoryName] = repository.split('/');
let repo;
try {
let infoQuery = graphql_1.repoInfoQuery;
// GitHub Enterprise Server <3.3.0 doesn't support autoMergeAllowed and hasIssuesEnabled objects
// TODO #22198
if (platformConfig.isGhe &&
// semver not null safe, accepts null and undefined
semver_1.default.satisfies(platformConfig.gheVersion, '<3.3.0')) {
infoQuery = infoQuery.replace(/\n\s*autoMergeAllowed\s*\n/, '\n');
infoQuery = infoQuery.replace(/\n\s*hasIssuesEnabled\s*\n/, '\n');
}
// GitHub Enterprise Server <3.9.0 doesn't support hasVulnerabilityAlertsEnabled objects
if (platformConfig.isGhe &&
// semver not null safe, accepts null and undefined
semver_1.default.satisfies(platformConfig.gheVersion, '<3.9.0')) {
infoQuery = infoQuery.replace(/\n\s*hasVulnerabilityAlertsEnabled\s*\n/, '\n');
}
const res = await common_2.githubApi.requestGraphql(infoQuery, {
variables: {
owner: config.repositoryOwner,
name: config.repositoryName,
...(!ignorePrAuthor && { user: renovateUsername }),
},
readOnly: true,
});
if (res?.errors) {
if (res.errors.find((err) => err.type === 'RATE_LIMITED')) {
logger_1.logger.debug({ res }, 'Graph QL rate limit exceeded.');
throw new Error(error_messages_1.PLATFORM_RATE_LIMIT_EXCEEDED);
}
logger_1.logger.debug({ res }, 'Unexpected Graph QL errors');
throw new Error(error_messages_1.PLATFORM_UNKNOWN_ERROR);
}
repo = res?.data?.repository;
/* v8 ignore start */
if (!repo) {
logger_1.logger.debug({ res }, 'No repository returned');
throw new Error(error_messages_1.REPOSITORY_NOT_FOUND);
} /* v8 ignore stop */
/* v8 ignore start */
if (!repo.defaultBranchRef?.name) {
logger_1.logger.debug({ res }, 'No default branch returned - treating repo as empty');
throw new Error(error_messages_1.REPOSITORY_EMPTY);
} /* v8 ignore stop */
if (repo.nameWithOwner &&
repo.nameWithOwner.toUpperCase() !== repository.toUpperCase()) {
logger_1.logger.debug({ desiredRepo: repository, foundRepo: repo.nameWithOwner }, 'Repository has been renamed');
throw new Error(error_messages_1.REPOSITORY_RENAMED);
}
if (repo.isArchived) {
logger_1.logger.debug('Repository is archived - throwing error to abort renovation');
throw new Error(error_messages_1.REPOSITORY_ARCHIVED);
}
// Use default branch as PR target unless later overridden.
config.defaultBranch = repo.defaultBranchRef.name;
// Base branch may be configured but defaultBranch is always fixed
logger_1.logger.debug(`${repository} default branch = ${config.defaultBranch}`);
// GitHub allows administrators to block certain types of merge, so we need to check it
if (repo.squashMergeAllowed) {
config.mergeMethod = 'squash';
}
else if (repo.mergeCommitAllowed) {
config.mergeMethod = 'merge';
}
else if (repo.rebaseMergeAllowed) {
config.mergeMethod = 'rebase';
}
else {
// This happens if we don't have Administrator read access, it is not a critical error
logger_1.logger.debug('Could not find allowed merge methods for repo');
}
config.autoMergeAllowed = repo.autoMergeAllowed;
config.hasIssuesEnabled = repo.hasIssuesEnabled;
config.hasVulnerabilityAlertsEnabled = repo.hasVulnerabilityAlertsEnabled;
const recentIssues = issue_1.GithubIssue.array()
.catch([])
.parse(res?.data?.repository?.issues?.nodes);
issue_1.GithubIssueCache.addIssuesToReconcile(recentIssues);
}
catch (err) /* v8 ignore start */ {
logger_1.logger.debug({ err }, 'Caught initRepo error');
if (err.message === error_messages_1.REPOSITORY_ARCHIVED ||
err.message === error_messages_1.REPOSITORY_RENAMED ||
err.message === error_messages_1.REPOSITORY_NOT_FOUND) {
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.startsWith('Repository access blocked')) {
throw new Error(error_messages_1.REPOSITORY_BLOCKED);
}
if (err.message === error_messages_1.REPOSITORY_FORK_MODE_FORKED) {
throw err;
}
if (err.message === error_messages_1.REPOSITORY_FORKED) {
throw err;
}
if (err.message === error_messages_1.REPOSITORY_DISABLED) {
throw err;
}
if (err.message === 'Response code 451 (Unavailable for Legal Reasons)') {
throw new Error(error_messages_1.REPOSITORY_ACCESS_FORBIDDEN);
}
logger_1.logger.debug({ err }, 'Unknown GitHub initRepo error');
throw err;
} /* v8 ignore stop */
// This shouldn't be necessary, but occasional strange errors happened until it was added
config.prList = null;
if (forkToken) {
logger_1.logger.debug('Bot is in fork mode');
if (repo.isFork) {
logger_1.logger.debug(`Forked repos cannot be processed when running with a forkToken, so this repo will be skipped`);
logger_1.logger.debug(`Parent repo for this forked repo is ${repo.parent?.nameWithOwner}`);
throw new Error(error_messages_1.REPOSITORY_FORKED);
}
config.forkOrg = forkOrg;
config.forkToken = forkToken;
// save parent name then delete
config.parentRepo = config.repository;
config.repository = null;
let forkedRepo = await findFork(forkToken, repository, forkOrg);
if (forkedRepo) {
config.repository = forkedRepo.full_name;
const forkDefaultBranch = forkedRepo.default_branch;
if (forkDefaultBranch !== config.defaultBranch) {
const body = {
ref: `refs/heads/${config.defaultBranch}`,
sha: repo.defaultBranchRef.target.oid,
};
logger_1.logger.debug({
defaultBranch: config.defaultBranch,
forkDefaultBranch,
body,
}, 'Fork has different default branch to parent, attempting to create branch');
try {
await common_2.githubApi.postJson(`repos/${config.repository}/git/refs`, {
body,
token: forkToken,
});
logger_1.logger.debug('Created new default branch in fork');
}
catch (err) /* v8 ignore start */ {
if (err.response?.body?.message === 'Reference already exists') {
logger_1.logger.debug(`Branch ${config.defaultBranch} already exists in the fork`);
}
else {
logger_1.logger.warn({ err, body: err.response?.body }, 'Could not create parent defaultBranch in fork');
}
} /* v8 ignore stop */
logger_1.logger.debug(`Setting ${config.defaultBranch} as default branch for ${config.repository}`);
try {
await common_2.githubApi.patchJson(`repos/${config.repository}`, {
body: {
name: config.repository.split('/')[1],
default_branch: config.defaultBranch,
},
token: forkToken,
});
logger_1.logger.debug('Successfully changed default branch for fork');
}
catch (err) /* v8 ignore start */ {
logger_1.logger.warn({ err }, 'Could not set default branch');
} /* v8 ignore stop */
}
}
else if (forkCreation) {
logger_1.logger.debug('Forked repo is not found - attempting to create it');
forkedRepo = await createFork(forkToken, repository, forkOrg);
config.repository = forkedRepo.full_name;
}
else {
logger_1.logger.debug('Forked repo is not found and forkCreation is disabled');
throw new Error(error_messages_1.REPOSITORY_FORK_MISSING);
}
}
const parsedEndpoint = node_url_1.default.parse(platformConfig.endpoint);
if (forkToken) {
logger_1.logger.debug('Using forkToken for git init');
parsedEndpoint.auth = (0, coerce_1.coerceToNull)(config.forkToken);
} /* v8 ignore start */
else {
const tokenType = opts.token?.startsWith('x-access-token:')
? 'app'
: 'personal access';
logger_1.logger.debug(`Using ${tokenType} token for git init`);
parsedEndpoint.auth = opts.token ?? null;
} /* v8 ignore stop */
// TODO: null checks (#22198)
parsedEndpoint.host = parsedEndpoint.host.replace('api.github.com', 'github.com');
parsedEndpoint.pathname = `${config.repository}.git`;
const url = node_url_1.default.format(parsedEndpoint);
let upstreamUrl = undefined;
if (forkCreation && config.parentRepo) {
parsedEndpoint.pathname = config.parentRepo + '.git';
upstreamUrl = node_url_1.default.format(parsedEndpoint);
}
await git.initRepo({
...config,
url,
upstreamUrl,
});
const repoConfig = {
defaultBranch: config.defaultBranch,
isFork: repo.isFork === true,
repoFingerprint: (0, util_1.repoFingerprint)(repo.id, platformConfig.endpoint),
};
return repoConfig;
}
async function getBranchForceRebase(branchName) {
config.branchForceRebase ??= {};
if (config.branchForceRebase[branchName] === undefined) {
try {
config.branchForceRebase[branchName] = false;
const branchProtection = await getBranchProtection(branchName);
logger_1.logger.debug(`Found branch protection for branch ${branchName}`);
if (branchProtection?.required_status_checks?.strict) {
logger_1.logger.debug(`Branch protection: PRs must be up-to-date before merging for ${branchName}`);
config.branchForceRebase[branchName] = true;
}
}
catch (err) {
if (err.statusCode === 404) {
logger_1.logger.debug(`No branch protection found for ${branchName}`);
}
else if (err.message === error_messages_1.PLATFORM_INTEGRATION_UNAUTHORIZED ||
err.statusCode === 403) {
logger_1.logger.once.debug('Branch protection: Do not have permissions to detect branch protection');
}
else {
throw err;
}
}
}
return !!config.branchForceRebase[branchName];
}
function cachePr(pr) {
config.prList ??= [];
if (pr) {
(0, pr_1.updatePrCache)(pr);
for (let idx = 0; idx < config.prList.length; idx += 1) {
const cachedPr = config.prList[idx];
if (cachedPr.number === pr.number) {
config.prList[idx] = pr;
return;
}
}
config.prList.push(pr);
}
}
// Fetch fresh Pull Request and cache it when possible
async function fetchPr(prNo) {
try {
const { body: ghRestPr } = await common_2.githubApi.getJsonUnchecked(`repos/${config.parentRepo ?? config.repository}/pulls/${prNo}`);
const result = (0, common_2.coerceRestPr)(ghRestPr);
cachePr(result);
return result;
}
catch (err) {
logger_1.logger.warn({ err, prNo }, `GitHub fetchPr error`);
return null;
}
}
// Gets details for a PR
async function getPr(prNo) {
if (!prNo) {
return null;
}
const prList = await getPrList();
let pr = prList.find(({ number }) => number === prNo) ?? null;
if (pr) {
logger_1.logger.debug('Returning PR from cache');
}
pr ??= await fetchPr(prNo);
return pr;
}
function matchesState(state, desiredState) {
if (desiredState === 'all') {
return true;
}
if (desiredState.startsWith('!')) {
return state !== desiredState.substring(1);
}
return state === desiredState;
}
async function getPrList() {
if (!config.prList) {
const repo = config.parentRepo ?? config.repository;
let username = config.renovateUsername;
if (config.forkToken || config.ignorePrAuthor) {
username = undefined;
}
// TODO: check null `repo` (#22198)
const prCache = await (0, pr_1.getPrCache)(common_2.githubApi, repo, username);
config.prList = Object.values(prCache).sort(({ number: a }, { number: b }) => b - a);
}
return config.prList;
}
async function findPr({ branchName, prTitle, state = 'all', includeOtherAuthors, }) {
logger_1.logger.debug(`findPr(${branchName}, ${prTitle}, ${state})`);
if (includeOtherAuthors) {
const repo = config.parentRepo ?? config.repository;
const org = repo?.split('/')[0];
// PR might have been created by anyone, so don't use the cached Renovate PR list
const { body: prList } = await common_2.githubApi.getJsonUnchecked(`repos/${repo}/pulls?head=${org}:${branchName}&state=open`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider });
if (!prList.length) {
logger_1.logger.debug(`No PR found for branch ${branchName}`);
return null;
}
return (0, common_2.coerceRestPr)(prList[0]);
}
const prList = await getPrList();
const pr = prList.find((p) => {
if (p.sourceBranch !== branchName) {
return false;
}
if (prTitle && prTitle.toUpperCase() !== p.title.toUpperCase()) {
return false;
}
if (!matchesState(p.state, state)) {
return false;
}
if (!config.forkToken && !(0, string_1.looseEquals)(config.repository, p.sourceRepo)) {
return false;
}
return true;
});
if (pr) {
logger_1.logger.debug(`Found PR #${pr.number}`);
}
return pr ?? null;
}
async function ensureBranchSha(branchName, sha) {
const repository = config.repository;
try {
const commitUrl = `/repos/${repository}/git/commits/${sha}`;
await common_2.githubApi.head(commitUrl, { memCache: false });
}
catch (err) {
logger_1.logger.error({ err, sha, branchName }, 'Commit not found');
throw err;
}
const refUrl = `/repos/${config.repository}/git/refs/heads/${branchName}`;
const branchExists = await (0, branch_1.remoteBranchExists)(repository, branchName);
if (branchExists) {
try {
await common_2.githubApi.patchJson(refUrl, { body: { sha, force: true } });
return;
}
catch (err) {
if (err.err?.response?.statusCode === 422) {
logger_1.logger.debug({ err }, 'Branch update failed due to reference not existing - will try to create');
}
else {
logger_1.logger.warn({ refUrl, err }, 'Error updating branch');
throw err;
}
}
}
await common_2.githubApi.postJson(`/repos/${repository}/git/refs`, {
body: { sha, ref: `refs/heads/${branchName}` },
});
}
// Returns the Pull Request for a branch. Null if not exists.
async function getBranchPr(branchName) {
logger_1.logger.debug(`getBranchPr(${branchName})`);
const openPr = await findPr({
branchName,
state: 'open',
});
if (openPr) {
return openPr;
}
return null;
}
async function tryReuseAutoclosedPr(autoclosedPr) {
const { sha, number, sourceBranch: branchName } = autoclosedPr;
try {
await ensureBranchSha(branchName, sha);
logger_1.logger.debug(`Recreated autoclosed branch ${branchName} with sha ${sha}`);
}
catch (err) {
logger_1.logger.debug({ err, branchName, sha, autoclosedPr }, 'Could not recreate autoclosed branch - skipping reopen');
return null;
}
try {
const title = autoclosedPr.title.replace((0, regex_1.regEx)(/ - autoclosed$/), '');
const { body: ghPr } = await common_2.githubApi.patchJson(`repos/${config.repository}/pulls/${number}`, {
body: {
state: 'open',
title,
},
});
logger_1.logger.info({ branchName, title, number }, 'Successfully reopened autoclosed PR');
const result = (0, common_2.coerceRestPr)(ghPr);
cachePr(result);
return result;
}
catch {
logger_1.logger.debug('Could not reopen autoclosed PR');
return null;
}
}
async function getStatus(branchName, useCache = true) {
const branch = escapeHash(branchName);
const url = `repos/${config.repository}/commits/${branch}/status`;
const { body: status } = await common_2.githubApi.getJsonUnchecked(url, {
memCache: useCache,
cacheProvider: repository_http_cache_provider_1.repoCacheProvider,
});
return status;
}
// Returns the combined status for a branch.
async function getBranchStatus(branchName, internalChecksAsSuccess) {
logger_1.logger.debug(`getBranchStatus(${branchName})`);
let commitStatus;
try {
commitStatus = await getStatus(branchName);
}
catch (err) /* v8 ignore start */ {
if (err.statusCode === 404) {
logger_1.logger.debug('Received 404 when checking branch status, assuming that branch has been deleted');
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
logger_1.logger.debug('Unknown error when checking branch status');
throw err;
} /* v8 ignore stop */
logger_1.logger.debug({ state: commitStatus.state, statuses: commitStatus.statuses }, 'branch status check result');
if (commitStatus.statuses && !internalChecksAsSuccess) {
commitStatus.statuses = commitStatus.statuses.filter((status) => status.state !== 'success' || !status.context?.startsWith('renovate/'));
if (!commitStatus.statuses.length) {
logger_1.logger.debug('Successful checks are all internal renovate/ checks, so returning "pending" branch status');
commitStatus.state = 'pending';
}
}
let checkRuns = [];
// API is supported in oldest available GHE version 2.19
try {
const checkRunsUrl = `repos/${config.repository}/commits/${escapeHash(branchName)}/check-runs?per_page=100`;
const opts = {
headers: {
accept: 'application/vnd.github.antiope-preview+json',
},
paginate: true,
paginationField: 'check_runs',
cacheProvider: memory_http_cache_provider_1.memCacheProvider,
};
const checkRunsRaw = (await common_2.githubApi.getJsonUnchecked(checkRunsUrl, opts)).body;
if (checkRunsRaw.check_runs?.length) {
checkRuns = checkRunsRaw.check_runs.map((run) => ({
name: run.name,
status: run.status,
conclusion: run.conclusion,
}));
logger_1.logger.debug({ checkRuns }, 'check runs result');
} /* v8 ignore start */
else {
logger_1.logger.debug({ result: checkRunsRaw }, 'No check runs found');
} /* v8 ignore stop */
}
catch (err) /* v8 ignore start */ {
if (err instanceof external_host_error_1.ExternalHostError) {
throw err;
}
if (err.statusCode === 403 ||
err.message === error_messages_1.PLATFORM_INTEGRATION_UNAUTHORIZED) {
logger_1.logger.debug('No permission to view check runs');
}
else {
logger_1.logger.warn({ err }, 'Error retrieving check runs');
}
} /* v8 ignore stop */
if (checkRuns.length === 0) {
if (commitStatus.state === 'success') {
return 'green';
}
if (commitStatus.state === 'failure') {
return 'red';
}
return 'yellow';
}
if (commitStatus.state === 'failure' ||
checkRuns.some((run) => run.conclusion === 'failure')) {
return 'red';
}
if ((commitStatus.state === 'success' || commitStatus.statuses.length === 0) &&
checkRuns.every((run) => ['skipped', 'neutral', 'success'].includes(run.conclusion))) {
return 'green';
}
return 'yellow';
}
async function getStatusCheck(branchName, useCache = true) {
const branchCommit = git.getBranchCommit(branchName);
const url = `repos/${config.repository}/commits/${branchCommit}/statuses`;
const opts = useCache
? { cacheProvider: memory_http_cache_provider_1.memCacheProvider }
: { memCache: false };
return (await common_2.githubApi.getJsonUnchecked(url, opts)).body;
}
const githubToRenovateStatusMapping = {
success: 'green',
error: 'red',
failure: 'red',
pending: 'yellow',
};
async function getBranchStatusCheck(branchName, context) {
try {
const res = await getStatusCheck(branchName);
for (const check of res) {
if (check.context === context) {
return githubToRenovateStatusMapping[check.state] || 'yellow';
}
}
return null;
}
catch (err) /* v8 ignore start */ {
if (err.statusCode === 404) {
logger_1.logger.debug('Commit not found when checking statuses');
throw new Error(error_messages_1.REPOSITORY_CHANGED);
}
throw err;
} /* v8 ignore stop */
}
async function setBranchStatus({ branchName, context, description, state, url: targetUrl, }) {
/* v8 ignore start */
if (config.parentRepo) {
logger_1.logger.debug('Cannot set branch status when in forking mode');
return;
} /* v8 ignore stop */
const existingStatus = await getBranchStatusCheck(branchName, context);
if (existingStatus === state) {
return;
}
logger_1.logger.debug({ branch: branchName, context, state }, 'Setting branch status');
let url;
try {
const branchCommit = git.getBranchCommit(branchName);
url = `repos/${config.repository}/statuses/${branchCommit}`;
const renovateToGitHubStateMapping = {
green: 'success',
yellow: 'pending',
red: 'failure',
};
const options = {
state: renovateToGitHubStateMapping[state],
description,
context,
};
if (targetUrl) {
options.target_url = targetUrl;
}
await common_2.githubApi.postJson(url, { body: options });
// update status cache
await getStatus(branchName, false);
await getStatusCheck(branchName, false);
}
catch (err) /* v8 ignore start */ {
logger_1.logger.debug({ err, url }, 'Caught error setting branch status - aborting');
throw new Error(error_messages_1.REPOSITORY_CHANGED);
} /* v8 ignore stop */
}
// Issue
async function getIssues() {
const result = await common_2.githubApi.queryRepoField(graphql_1.getIssuesQuery, 'issues', {
variables: {
owner: config.repositoryOwner,
name: config.repositoryName,
...(!config.ignorePrAuthor && { user: config.renovateUsername }),
},
readOnly: true,
});
logger_1.logger.debug(`Retrieved ${result.length} issues`);
return issue_1.GithubIssue.array().parse(result);
}
async function getIssueList() {
/* v8 ignore start */
if (config.hasIssuesEnabled === false) {
return [];
} /* v8 ignore stop */
let issueList = issue_1.GithubIssueCache.getIssues();
if (!issueList) {
logger_1.logger.debug('Retrieving issueList');
issueList = await getIssues();
issue_1.GithubIssueCache.setIssues(issueList);
}
return issueList;
}
async function getIssue(number) {
if (config.hasIssuesEnabled === false) {
return null;
}
try {
const repo = config.parentRepo ?? config.repository;
const { body: issue } = await common_2.githubApi.getJson(`repos/${repo}/issues/${number}`, {
cacheProvider: repository_http_cache_provider_1.repoCacheProvider,
}, issue_1.GithubIssue);
issue_1.GithubIssueCache.updateIssue(issue);
return issue;
}
catch (err) {
logger_1.logger.debug({ err, number }, 'Error getting issue');
if (err.response?.statusCode === 410) {
logger_1.logger.debug(`Issue #${number} has been deleted`);
issue_1.GithubIssueCache.deleteIssue(number);
}
return null;
}
}
async function findIssue(title) {
logger_1.logger.debug(`findIssue(${title})`);
const [issue] = (await getIssueList()).filter((i) => i.state === 'open' && i.title === title);
if (!issue) {
return null;
}
logger_1.logger.debug(`Found issue ${issue.number}`);
return getIssue(issue.number);
}
async function closeIssue(issueNumber) {
logger_1.logger.debug(`closeIssue(${issueNumber})`);
const repo = config.parentRepo ?? config.repository;
const { body: closedIssue } = await common_2.githubApi.patchJson(`repos/${repo}/issues/${issueNumber}`, { body: { state: 'closed' } }, issue_1.GithubIssue);
issue_1.GithubIssueCache.updateIssue(closedIssue);
}
async function ensureIssue({ title, reuseTitle, body: rawBody, labels, once = false, shouldReOpen = true, }) {
logger_1.logger.debug(`ensureIssue(${title})`);
/* v8 ignore start */
if (config.hasIssuesEnabled === false) {
logger_1.logger.info('Cannot ensure issue because issues are disabled in this repository');
return null;
} /* v8 ignore stop */
const body = (0, sanitize_1.sanitize)(rawBody);
try {
const issueList = await getIssueList();
let issues = issueList.filter((i) => i.title === title);
if (!issues.length) {
issues = issueList.filter((i) => i.title === reuseTitle);
if (issues.length) {
logger_1.logger.debug(`Reusing issue title: "${reuseTitle}"`);
}
}
if (issues.length) {
let issue = issues.find((i) => i.state === 'open');
if (!issue) {
if (once) {
logger_1.logger.debug('Issue already closed - skipping recreation');
return null;
}
if (shouldReOpen) {
logger_1.logger.debug('Reopening previously closed issue');
}
issue = issues[issues.length - 1];
}
for (const i of issues) {
if (i.state === 'open' && i.number !== issue.number) {
logger_1.logger.warn({ issueNo: i.number }, 'Closing duplicate issue');
await closeIssue(i.number);
}
}
const repo = config.parentRepo ?? config.repository;
const { body: serverIssue } = await common_2.githubApi.getJson(`repos/${repo}/issues/${issue.number}`, { cacheProvider: repository_http_cache_provider_1.repoCacheProvider }, issue_1.GithubIssue);
issue_1.GithubIssueCache.updateIssue(serverIssue);
if (issue.title === title &&
serverIssue.body === body &&
issue.state === 'open') {
logger_1.logger.debug('Issue is open and up to date - nothing to do');
return null;
}
if (shouldReOpen) {
logger_1.logger.debug('Patching issue');
const data = { body, state: 'open', title };
if (labels) {
data.labels = labels;
}
const repo = config.parentRepo ?? config.repository;
const { body: updatedIssue } = await common_2.githubApi.patchJson(`repos/${repo}/issues/${issue.number}`, { body: data }, issue_1.GithubIssue);
issue_1.GithubIssueCache.updateIssue(updatedIssue);
logger_1.logger.debug('Issue updated');
return 'updated';
}
}
const { body: createdIssue } = await common_2.githubApi.postJson(`repos/${config.parentRepo ?? config.repository}/issues`, {
body: {
title,
body,
labels: labels ?? [],
},
}, issue_1.GithubIssue);
logger_1.logger.info('Issue created');
// reset issueList so that it will be fetched again as-needed
issue_1.GithubIssueCache.updateIssue(createdIssue);
return 'created';
}
catch (err) /* v8 ignore start */ {
if (err.body?.message?.startsWith('Issues are disabled for this repo')) {
logger_1.logger.debug(`Issues are disabled, so could not create issue: ${title}`);
}
else {
logger_1.logger.warn({ err }, 'Could not ensure issue');
}
} /* v8 ignore stop */
return null;
}
async function ensureIssueClosing(title) {
logger_1.logger.trace(`ensureIssueClosing(${title})`);
/* v8 ignore start */
if (config.hasIssuesEnabled === false) {
return;
} /* v8 ignore stop */
const issueList = await getIssueList();
for (const issue of issueList) {
if (issue.state === 'open' && issue.title === title) {
await closeIssue(issue.number);
logger_1.logger.debug(`Issue closed, issueNo: ${issue.number}`);
}
}
}
async function tryAddMilestone(issueNo, milestoneNo) {
if (!milestoneNo) {
return;
}
logger_1.logger.debug({
milestone: milestoneNo,
pr: issueNo,
}, 'Adding milestone to PR');
try {
const repo = config.parentRepo ?? config.repository;
const { body: updatedIssue } = await common_2.githubApi.patchJson(`repos/${repo}/issues/${issueNo}`, { body: { milestone: milestoneNo } }, issue_1.GithubIssue);
issue_1.GithubIssueCache.updateIssue(updatedIssue);
}
catch (err) {
/* v8 ignore next */
const actualError = err.response?.body ?? err;
logger_1.logger.warn({
milestone: milestoneNo,
pr: issueNo,
err: actualError,
}, 'Unable to add milestone to PR');
}
}
async function addAssignees(issueNo, assignees) {
logger_1.logger.debug(`Adding assignees '${assignees.join(', ')}' to #${issueNo}`);
const repository = config.parentRepo ?? config.repository;
const { body: updatedIssue } = await common_2.githubApi.postJson(`repos/${repository}/issues/${issueNo}/assignees`, { body: { assignees } }, issue_1.GithubIssue);
issue_1.GithubIssueCache.updateIssue(updatedIssue);
}
async function addReviewers(prNo, reviewers) {
logger_1.logger.debug(`Adding reviewers '${reviewers.join(', ')}' to #${prNo}`);
const userReviewers = reviewers.filter((e) => !e.startsWith('team:'));
const teamReviewers = reviewers
.filter((e) => e.startsWith('team:'))
.map((e) => e.replace((0, regex_1.regEx)(/^team:/), ''));
try {
await common_2.githubApi.postJson(`repos/${config.parentRepo ?? config.repository}/pulls/${prNo}/requested_reviewers`, {
body: {
reviewers: userReviewers,
team_reviewers: teamReviewers,
},
});
}
catch (err) /* v8 ignore start */ {
logger_1.logger.warn({ err }, 'Failed to assign reviewer');
} /* v8 ignore stop */
}
async function addLabels(issueNo, labels) {
logger_1.logger.debug(`Adding labels '${labels?.join(', ')}' to #${issueNo}`);
try {
const repository = config.parentRepo ?? config.repository;
if (is_1.default.array(labels) && labels.length) {
await common_2.githubApi.postJson(`repos/${repository}/issues/${issueNo}/labels`, {
body: labels,
});
}
}
catch (err) /* v8 ignore start */ {
logger_1.logger.warn({ err, issueNo, labels }, 'Error while adding labels. Skipping');
} /* v8 ignore stop */
}
async function deleteLabel(issueNo, label) {
logger_1.logger.debug(`Deleting label ${label} from #${issueNo}`);
const repository = config.parentRepo ?? config.repository;
try {
await common_2.githubApi.deleteJson(`repos/${repository}/issues/${issueNo}/labels/${label}`);
}
catch (err) /* v8 ignore start */ {
logger_1.logger.warn({ err, issueNo, label }, 'Failed to delete label');
} /* v8 ignore stop */
}
async function addComment(issueNo, body) {
// POST /repos/:owner/:repo/issues/:number/comments
await common_2.githubApi.postJson(`repos/${config.parentRepo ?? config.repository}/issues/${issueNo}/comments`, {
body: { body },
});
}
async function editComment(commentId, body) {
// PATCH /repos/:owner/:repo/issues/comments/:id
await common_2.githubApi.patchJson(`repos/${config.parentRepo ?? config.repository}/issues/comments/${commentId}`, {
body: { body },
});
}
async function deleteComment(commentId) {
// DELETE /repos/:owner/:repo/issues/comments/:id
await common_2.githubApi.deleteJson(`repos/${config.parentRepo ?? config.repository}/issues/comments/${commentId}`);
}
async function getComments(issueNo) {
// GET /repos/:owner/:repo/issues/:number/comments
logger_1.logger.debug(`Getting comments for #${issueNo}`);
const repo = config.parentRepo ?? config.repository;
const url = `repos/${repo}/issues/${issueNo}/comments?per_page=100`;
try {
const { body: comments } = await common_2.githubApi.getJsonUnchecked(url, {
paginate: true,
cacheProvider: repository_http_cache_provider_1.repoCacheProvider,
});
logger_1.logger.debug(`Found ${comments.length} comments`);
return comm