mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-01 18:19:51 +00:00
8d08b51db0
* refactor(onboarding): return repository result from ensureOnboarding Rename ensureOnboardingPr to ensureOnboarding and have it return a new 'onboarding' RepositoryResult. renovateRepository now branches on repoIsOnboarded, so exactly one of ensureOnboarding/updateRepo produces the repository result and the non-null assertion on the update result (TODO #22198) is no longer needed. updateRepo/update no longer return undefined for non-onboarded repositories. * refactor(onboarding): keep ensureOnboardingPr name
413 lines
14 KiB
TypeScript
413 lines
14 KiB
TypeScript
import { isNumber, isString } from '@sindresorhus/is';
|
|
import { GlobalConfig } from '../../../../config/global.ts';
|
|
import type { RenovateConfig } from '../../../../config/types.ts';
|
|
import { REPOSITORY_CLOSED_ONBOARDING } from '../../../../constants/error-messages.ts';
|
|
import { logger } from '../../../../logger/index.ts';
|
|
import type { PackageFile } from '../../../../modules/manager/types.ts';
|
|
import { ensureComment } from '../../../../modules/platform/comment.ts';
|
|
import type { Pr } from '../../../../modules/platform/index.ts';
|
|
import { platform } from '../../../../modules/platform/index.ts';
|
|
import { hashBody } from '../../../../modules/platform/pr-body.ts';
|
|
import { scm } from '../../../../modules/platform/scm.ts';
|
|
import { getInheritedOrGlobal } from '../../../../util/common.ts';
|
|
import { getElapsedDays } from '../../../../util/date.ts';
|
|
import { emojify } from '../../../../util/emoji.ts';
|
|
import { getFile } from '../../../../util/git/index.ts';
|
|
import { toSha256 } from '../../../../util/hash.ts';
|
|
import * as template from '../../../../util/template/index.ts';
|
|
import type { BranchConfig } from '../../../types.ts';
|
|
import {
|
|
getDepWarningsOnboardingPR,
|
|
getErrors,
|
|
getWarnings,
|
|
} from '../../errors-warnings.ts';
|
|
import type { RepositoryResult } from '../../result.ts';
|
|
import { getPlatformPrOptions } from '../../update/pr/index.ts';
|
|
import { prepareLabels } from '../../update/pr/labels.ts';
|
|
import { addParticipants } from '../../update/pr/participants.ts';
|
|
import { isOnboardingBranchConflicted } from '../branch/onboarding-branch-cache.ts';
|
|
import {
|
|
OnboardingState,
|
|
getDefaultConfigFileName,
|
|
getSemanticCommitPrTitle,
|
|
} from '../common.ts';
|
|
import { getBaseBranchDesc } from './base-branch.ts';
|
|
import { getConfigDesc } from './config-description.ts';
|
|
import {
|
|
getPackageFilesDesc,
|
|
getPackageFilesSummary,
|
|
} from './package-files.ts';
|
|
import { getExpectedPrList, getExpectedPrListSummary } from './pr-list.ts';
|
|
import type { OnboardingPrSections } from './types.ts';
|
|
|
|
/**
|
|
* Given an existing PR, if onboardingAutoCloseAge has passed, close the PR.
|
|
*
|
|
* Returns true if the PR was closed.
|
|
*/
|
|
async function ensureOnboardingAutoCloseAge(existingPr: Pr): Promise<boolean> {
|
|
// check if the existing pr crosses the onboarding autoclose age
|
|
const ageOfOnboardingPr = getElapsedDays(existingPr.createdAt!, false);
|
|
const onboardingAutoCloseAge = getInheritedOrGlobal('onboardingAutoCloseAge');
|
|
if (onboardingAutoCloseAge) {
|
|
logger.debug(
|
|
{
|
|
onboardingAutoCloseAge,
|
|
createdAt: existingPr.createdAt!,
|
|
ageOfOnboardingPr,
|
|
},
|
|
`Determining that the onboarding PR created at \`${existingPr.createdAt!}\` was created ${ageOfOnboardingPr.toFixed(2)} days ago`,
|
|
);
|
|
}
|
|
if (
|
|
isNumber(onboardingAutoCloseAge) &&
|
|
ageOfOnboardingPr > onboardingAutoCloseAge
|
|
) {
|
|
// close the pr
|
|
await platform.updatePr({
|
|
number: existingPr.number,
|
|
state: 'closed',
|
|
prTitle: existingPr.title,
|
|
});
|
|
// ensure comment
|
|
await ensureComment({
|
|
number: existingPr.number,
|
|
topic: `Renovate is disabled`,
|
|
content: `Renovate is disabled because the onboarding PR has been unmerged for more than ${onboardingAutoCloseAge} days. To enable Renovate, you can either (a) change this PR's title to get a new onboarding PR, and merge the new onboarding PR, or (b) create a Renovate config file, and commit that file to your base branch.`,
|
|
});
|
|
logger.debug(
|
|
{
|
|
ageOfOnboardingPr,
|
|
onboardingAutoCloseAge,
|
|
},
|
|
`Renovate is being disabled for this repository as the onboarding PR has been unmerged for more than ${onboardingAutoCloseAge} days`,
|
|
);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Full onboarding PRs list package files before the PR list.
|
|
*/
|
|
const FULL_SECTION_ORDER = [
|
|
'PACKAGE FILES',
|
|
'CONFIG',
|
|
'BASEBRANCH',
|
|
'PRLIST',
|
|
'WARNINGS',
|
|
'ERRORS',
|
|
] as const;
|
|
|
|
/**
|
|
* "Summarised" onboarding PRs list PRs before other sections.
|
|
*/
|
|
const SUMMARY_SECTION_ORDER = [
|
|
'PRLIST',
|
|
'CONFIG',
|
|
'BASEBRANCH',
|
|
'PACKAGE FILES',
|
|
'WARNINGS',
|
|
'ERRORS',
|
|
] as const;
|
|
|
|
function buildOnboardingPrTemplate(
|
|
config: RenovateConfig,
|
|
configFile: string,
|
|
rebaseCheckBox: string,
|
|
sectionOrder: readonly string[],
|
|
): string {
|
|
let prTemplate = `Welcome to [Renovate](${
|
|
GlobalConfig.get('productLinks').homepage
|
|
})! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin.\n\n`;
|
|
prTemplate +=
|
|
getInheritedOrGlobal('requireConfig') === 'required'
|
|
? emojify(
|
|
`:vertical_traffic_light: To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged.\n\n`,
|
|
)
|
|
: emojify(
|
|
`:vertical_traffic_light: Renovate will begin keeping your dependencies up-to-date only once you merge or close this Pull Request.\n\n`,
|
|
);
|
|
|
|
prTemplate += emojify(
|
|
`:books: See our [Reading List](${GlobalConfig.get('productLinks').documentation}reading-list/) for relevant documentation you may be interested in reading.\n\n`,
|
|
);
|
|
|
|
prTemplate += emojify(
|
|
`:abcd: Do you want to change how Renovate upgrades your dependencies?`,
|
|
);
|
|
prTemplate += ` Add your custom config to \`${configFile}\` in this branch${
|
|
config.onboardingRebaseCheckbox
|
|
? ' and select the Retry/Rebase checkbox below'
|
|
: ''
|
|
}. Renovate will update the Pull Request description the next time it runs.`;
|
|
prTemplate += '\n\n';
|
|
// TODO #22198
|
|
prTemplate += emojify(
|
|
`
|
|
|
|
---
|
|
${sectionOrder.map((section) => `{{${section}}}`).join('\n')}
|
|
|
|
---
|
|
|
|
:question: Got questions? Check out Renovate's [Docs](${
|
|
GlobalConfig.get('productLinks').documentation
|
|
}), particularly the Getting Started section.
|
|
If you need any further assistance then you can also [request help here](${
|
|
GlobalConfig.get('productLinks').help
|
|
}).
|
|
`,
|
|
);
|
|
prTemplate += rebaseCheckBox;
|
|
return prTemplate;
|
|
}
|
|
|
|
function fillOnboardingPrBody(
|
|
prTemplate: string,
|
|
sections: OnboardingPrSections,
|
|
): string {
|
|
let prBody = prTemplate;
|
|
if (sections.packageFiles) {
|
|
prBody = `${prBody.replace('{{PACKAGE FILES}}', sections.packageFiles)}\n`;
|
|
} else {
|
|
prBody = prBody.replace('{{PACKAGE FILES}}\n', '');
|
|
}
|
|
prBody = prBody.replace('{{CONFIG}}\n', sections.config);
|
|
prBody = prBody.replace('{{WARNINGS}}\n', sections.warnings);
|
|
prBody = prBody.replace('{{ERRORS}}\n', sections.errors);
|
|
prBody = prBody.replace('{{BASEBRANCH}}\n', sections.baseBranch);
|
|
prBody = prBody.replace('{{PRLIST}}\n', sections.prList);
|
|
return prBody;
|
|
}
|
|
|
|
function finalizeOnboardingPrBody(
|
|
prBody: string,
|
|
config: RenovateConfig,
|
|
onboardingConfigHashComment: string,
|
|
): string {
|
|
let finalPrBody = prBody;
|
|
if (isString(config.prHeader)) {
|
|
finalPrBody = `${template.compile(config.prHeader, config)}\n\n${finalPrBody}`;
|
|
}
|
|
if (isString(config.prFooter)) {
|
|
finalPrBody = `${finalPrBody}\n---\n\n${template.compile(config.prFooter, config)}\n`;
|
|
}
|
|
return finalPrBody + onboardingConfigHashComment;
|
|
}
|
|
|
|
export async function ensureOnboardingPr(
|
|
config: RenovateConfig,
|
|
packageFiles: Record<string, PackageFile[]> | null,
|
|
branches: BranchConfig[],
|
|
): Promise<RepositoryResult> {
|
|
if (config.onboardingRebaseCheckbox && !OnboardingState.prUpdateRequested) {
|
|
return 'onboarding';
|
|
}
|
|
logger.debug('ensureOnboardingPr()');
|
|
logger.trace({ config });
|
|
// TODO #22198
|
|
const onboardingBranch = getInheritedOrGlobal('onboardingBranch')!;
|
|
const existingPr = await platform.getBranchPr(
|
|
onboardingBranch,
|
|
config.defaultBranch,
|
|
);
|
|
if (existingPr) {
|
|
const wasClosed = await ensureOnboardingAutoCloseAge(existingPr);
|
|
if (wasClosed) {
|
|
throw new Error(REPOSITORY_CLOSED_ONBOARDING);
|
|
}
|
|
|
|
// skip pr-update if branch is conflicted
|
|
if (
|
|
await isOnboardingBranchConflicted(
|
|
config.defaultBranch!,
|
|
onboardingBranch,
|
|
)
|
|
) {
|
|
if (GlobalConfig.get('dryRun')) {
|
|
logger.info(
|
|
'DRY-RUN: Would comment that Onboarding PR is conflicted and needs manual resolving',
|
|
);
|
|
return 'onboarding';
|
|
}
|
|
await ensureComment({
|
|
number: existingPr.number,
|
|
topic: 'Branch Conflicted',
|
|
content: emojify(
|
|
`:warning: This PR has a merge conflict which Renovate is unable to automatically resolve, so updates to this PR description are now paused. Please resolve the merge conflict manually.\n\n`,
|
|
),
|
|
});
|
|
return 'onboarding';
|
|
}
|
|
}
|
|
|
|
if (OnboardingState.onboardingCacheValid) {
|
|
return 'onboarding';
|
|
}
|
|
|
|
const onboardingConfigHashComment = await getOnboardingConfigHashComment();
|
|
const rebaseCheckBox = getRebaseCheckbox(config.onboardingRebaseCheckbox);
|
|
const configFile = getDefaultConfigFileName();
|
|
logger.debug('Filling in onboarding PR template');
|
|
|
|
const packageFilesDesc = getPackageFilesDesc(packageFiles);
|
|
let configDesc = '';
|
|
if (GlobalConfig.get('dryRun')) {
|
|
// TODO: types (#22198)
|
|
logger.info(`DRY-RUN: Would check branch ${onboardingBranch}`);
|
|
} else {
|
|
configDesc = getConfigDesc(config, packageFiles!);
|
|
}
|
|
const warnings =
|
|
getWarnings(config) + getDepWarningsOnboardingPR(packageFiles!, config);
|
|
const errors = getErrors(config);
|
|
const baseBranchDesc = getBaseBranchDesc(config);
|
|
const prList = getExpectedPrList(config, branches);
|
|
|
|
let prBody = finalizeOnboardingPrBody(
|
|
fillOnboardingPrBody(
|
|
buildOnboardingPrTemplate(
|
|
config,
|
|
configFile,
|
|
rebaseCheckBox,
|
|
FULL_SECTION_ORDER,
|
|
),
|
|
{
|
|
packageFiles: packageFilesDesc,
|
|
config: configDesc,
|
|
baseBranch: baseBranchDesc,
|
|
prList,
|
|
warnings,
|
|
errors,
|
|
},
|
|
),
|
|
config,
|
|
onboardingConfigHashComment,
|
|
);
|
|
|
|
if (prBody.length > platform.maxBodyLength()) {
|
|
logger.debug(
|
|
'Onboarding PR body exceeds platform limit, switching to summary PR list and package files',
|
|
);
|
|
const prListSummary = getExpectedPrListSummary(config, branches);
|
|
if (packageFilesDesc) {
|
|
const packageFilesSummary = `### Detected Package Files\n\n${getPackageFilesSummary(packageFiles)}`;
|
|
// "What to Expect" should render before "Detected Package Files" in the summary view,
|
|
// so rebuild from a template with that section order rather than reordering the rendered body.
|
|
prBody = finalizeOnboardingPrBody(
|
|
fillOnboardingPrBody(
|
|
buildOnboardingPrTemplate(
|
|
config,
|
|
configFile,
|
|
rebaseCheckBox,
|
|
SUMMARY_SECTION_ORDER,
|
|
),
|
|
{
|
|
packageFiles: packageFilesSummary,
|
|
config: configDesc,
|
|
baseBranch: baseBranchDesc,
|
|
prList: prListSummary,
|
|
warnings,
|
|
errors,
|
|
},
|
|
),
|
|
config,
|
|
onboardingConfigHashComment,
|
|
);
|
|
} else {
|
|
prBody = prBody.replace(prList, prListSummary);
|
|
}
|
|
}
|
|
|
|
logger.trace(`prBody:\n${prBody}`);
|
|
|
|
prBody = platform.massageMarkdown(prBody, config.rebaseLabel);
|
|
|
|
if (existingPr) {
|
|
logger.debug('Found open onboarding PR');
|
|
// Check if existing PR needs updating
|
|
const prBodyHash = hashBody(prBody);
|
|
if (existingPr.bodyStruct?.hash === prBodyHash) {
|
|
logger.debug(`Pull Request #${existingPr.number} does not need updating`);
|
|
return 'onboarding';
|
|
}
|
|
// PR must need updating
|
|
if (GlobalConfig.get('dryRun')) {
|
|
logger.info('DRY-RUN: Would update onboarding PR');
|
|
} else {
|
|
await platform.updatePr({
|
|
number: existingPr.number,
|
|
prTitle: existingPr.title,
|
|
prBody,
|
|
});
|
|
logger.info({ pr: existingPr.number }, 'Onboarding PR updated');
|
|
}
|
|
return 'onboarding';
|
|
}
|
|
logger.debug('Creating onboarding PR');
|
|
const labels: string[] = prepareLabels(config);
|
|
try {
|
|
if (GlobalConfig.get('dryRun')) {
|
|
logger.info('DRY-RUN: Would create onboarding PR');
|
|
} else {
|
|
// TODO #22198
|
|
const prTitle =
|
|
config.semanticCommits === 'enabled'
|
|
? getSemanticCommitPrTitle(config)
|
|
: getInheritedOrGlobal('onboardingPrTitle')!;
|
|
const pr = await platform.createPr({
|
|
sourceBranch: onboardingBranch,
|
|
targetBranch: config.defaultBranch!,
|
|
prTitle,
|
|
prBody,
|
|
labels,
|
|
platformPrOptions: getPlatformPrOptions({
|
|
...config,
|
|
automerge: false,
|
|
}),
|
|
});
|
|
logger.info(
|
|
{ pr: `Pull Request #${pr!.number}` },
|
|
'Onboarding PR created',
|
|
);
|
|
await addParticipants(config, pr!);
|
|
}
|
|
} catch (err) {
|
|
if (
|
|
err.response?.statusCode === 422 &&
|
|
err.response?.body?.errors?.[0]?.message?.startsWith(
|
|
'A pull request already exists',
|
|
)
|
|
) {
|
|
logger.warn(
|
|
'Onboarding PR already exists but cannot find it. It was probably created by a different user.',
|
|
);
|
|
await scm.deleteBranch(onboardingBranch);
|
|
return 'onboarding';
|
|
}
|
|
throw err;
|
|
}
|
|
|
|
return 'onboarding';
|
|
}
|
|
|
|
function getRebaseCheckbox(onboardingRebaseCheckbox?: boolean): string {
|
|
let rebaseCheckBox = '';
|
|
if (onboardingRebaseCheckbox) {
|
|
// Create markdown checkbox
|
|
rebaseCheckBox = `\n\n---\n\n - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.\n`;
|
|
}
|
|
|
|
return rebaseCheckBox;
|
|
}
|
|
|
|
async function getOnboardingConfigHashComment(): Promise<string> {
|
|
const configFile = getDefaultConfigFileName();
|
|
const existingContents =
|
|
(await getFile(configFile, getInheritedOrGlobal('onboardingBranch'))) ?? '';
|
|
const hash = toSha256(existingContents);
|
|
|
|
return `\n<!--renovate-config-hash:${hash}-->\n`;
|
|
}
|