diff --git a/lib/modules/platform/azure/index.spec.ts b/lib/modules/platform/azure/index.spec.ts index bfeb72ebd043d1..f96de6bc1da6e2 100644 --- a/lib/modules/platform/azure/index.spec.ts +++ b/lib/modules/platform/azure/index.spec.ts @@ -1985,4 +1985,16 @@ describe('modules/platform/azure/index', () => { expect(res).toBeNull(); }); }); + + describe('maxBodyLength()', () => { + it('returns 4000', () => { + expect(azure.maxBodyLength()).toBe(4000); + }); + }); + + describe('maxCommentLength()', () => { + it('returns 150000', () => { + expect(azure.maxCommentLength()).toBe(150000); + }); + }); }); diff --git a/lib/modules/platform/azure/index.ts b/lib/modules/platform/azure/index.ts index 13356c9211da50..937f57a12f10e0 100644 --- a/lib/modules/platform/azure/index.ts +++ b/lib/modules/platform/azure/index.ts @@ -42,7 +42,6 @@ import type { UpdatePrConfig, } from '../types'; import { getNewBranchName, repoFingerprint } from '../util'; -import { smartTruncate } from '../utils/pr-body'; import * as azureApi from './azure-got-wrapper'; import * as azureHelper from './azure-helper'; import type { AzurePr } from './types'; @@ -811,7 +810,7 @@ export async function mergePr({ export function massageMarkdown(input: string): string { // Remove any HTML we use - return smartTruncate(input, maxBodyLength()) + return input .replace( 'you tick the rebase/retry checkbox', 'rename PR to start with "rebase!"', @@ -828,6 +827,10 @@ export function maxBodyLength(): number { return 4000; } +export function maxCommentLength(): number { + return 150000; +} + /* istanbul ignore next */ export function findIssue(): Promise { // TODO: Needs implementation (#9592) diff --git a/lib/modules/platform/bitbucket-server/index.spec.ts b/lib/modules/platform/bitbucket-server/index.spec.ts index 61bdbb2a287bb8..f8c954c600fc6c 100644 --- a/lib/modules/platform/bitbucket-server/index.spec.ts +++ b/lib/modules/platform/bitbucket-server/index.spec.ts @@ -2470,6 +2470,18 @@ Followed by some information. await expect(bitbucket.getJsonFile('file.json')).rejects.toThrow(); }); }); + + describe('maxBodyLength()', () => { + it('returns 30000', () => { + expect(bitbucket.maxBodyLength()).toBe(30000); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(bitbucket.maxCommentLength()).toBe(Infinity); + }); + }); }); }); }); diff --git a/lib/modules/platform/bitbucket-server/index.ts b/lib/modules/platform/bitbucket-server/index.ts index 3811ee5e09ce18..e5e3211de24232 100644 --- a/lib/modules/platform/bitbucket-server/index.ts +++ b/lib/modules/platform/bitbucket-server/index.ts @@ -39,7 +39,6 @@ import type { UpdatePrConfig, } from '../types'; import { getNewBranchName, repoFingerprint } from '../util'; -import { smartTruncate } from '../utils/pr-body'; import { UserSchema } from './schema'; import type { BbsConfig, @@ -1104,7 +1103,7 @@ export async function mergePr({ export function massageMarkdown(input: string): string { logger.debug(`massageMarkdown(${input.split(newlineRegex)[0]})`); // Remove any HTML we use - return smartTruncate(input, maxBodyLength()) + return input .replace( 'you tick the rebase/retry checkbox', 'rename PR to start with "rebase!"', @@ -1122,3 +1121,7 @@ export function massageMarkdown(input: string): string { export function maxBodyLength(): number { return 30000; } + +export function maxCommentLength(): number { + return Infinity; +} diff --git a/lib/modules/platform/bitbucket/index.spec.ts b/lib/modules/platform/bitbucket/index.spec.ts index 9de64419b236dc..c253e11c174932 100644 --- a/lib/modules/platform/bitbucket/index.spec.ts +++ b/lib/modules/platform/bitbucket/index.spec.ts @@ -1896,4 +1896,16 @@ describe('modules/platform/bitbucket/index', () => { await expect(bitbucket.getJsonFile('file.json')).rejects.toThrow(); }); }); + + describe('maxBodyLength()', () => { + it('returns 50000', () => { + expect(bitbucket.maxBodyLength()).toBe(50000); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(bitbucket.maxCommentLength()).toBe(Infinity); + }); + }); }); diff --git a/lib/modules/platform/bitbucket/index.ts b/lib/modules/platform/bitbucket/index.ts index 4ebd23684d6031..0284f9a0660d0d 100644 --- a/lib/modules/platform/bitbucket/index.ts +++ b/lib/modules/platform/bitbucket/index.ts @@ -31,7 +31,6 @@ import type { UpdatePrConfig, } from '../types'; import { repoFingerprint } from '../util'; -import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; import * as comments from './comments'; import { BitbucketPrCache } from './pr-cache'; @@ -570,7 +569,7 @@ async function closeIssue(issueNumber: number): Promise { export function massageMarkdown(input: string): string { // Remove any HTML we use - return smartTruncate(input, maxBodyLength()) + return input .replace( 'you tick the rebase/retry checkbox', 'by renaming this PR to start with "rebase!"', @@ -590,6 +589,10 @@ export function maxBodyLength(): number { return 50000; } +export function maxCommentLength(): number { + return Infinity; +} + export async function ensureIssue({ title, reuseTitle, diff --git a/lib/modules/platform/codecommit/index.spec.ts b/lib/modules/platform/codecommit/index.spec.ts index 75ff969775db8f..0645b0f46c7e42 100644 --- a/lib/modules/platform/codecommit/index.spec.ts +++ b/lib/modules/platform/codecommit/index.spec.ts @@ -1327,4 +1327,16 @@ describe('modules/platform/codecommit/index', () => { ); }); }); + + describe('maxBodyLength()', () => { + it('returns Infinity', () => { + expect(codeCommit.maxBodyLength()).toBe(Infinity); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(codeCommit.maxCommentLength()).toBe(Infinity); + }); + }); }); diff --git a/lib/modules/platform/codecommit/index.ts b/lib/modules/platform/codecommit/index.ts index c10e5d16012af4..a38134c2feec9f 100644 --- a/lib/modules/platform/codecommit/index.ts +++ b/lib/modules/platform/codecommit/index.ts @@ -329,6 +329,10 @@ export function maxBodyLength(): number { return Infinity; } +export function maxCommentLength(): number { + return Infinity; +} + export async function getJsonFile( fileName: string, repoName?: string, diff --git a/lib/modules/platform/gerrit/index.spec.ts b/lib/modules/platform/gerrit/index.spec.ts index 025f1d1e2e3f7e..8be8ea682cd20b 100644 --- a/lib/modules/platform/gerrit/index.spec.ts +++ b/lib/modules/platform/gerrit/index.spec.ts @@ -792,4 +792,16 @@ describe('modules/platform/gerrit/index', () => { await expect(gerrit.getIssueList()).resolves.toStrictEqual([]); }); }); + + describe('maxBodyLength()', () => { + it('returns 16384', () => { + expect(gerrit.maxBodyLength()).toBe(16384); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(gerrit.maxCommentLength()).toBe(Infinity); + }); + }); }); diff --git a/lib/modules/platform/gerrit/index.ts b/lib/modules/platform/gerrit/index.ts index 20360a761d497d..fa26286bb34746 100644 --- a/lib/modules/platform/gerrit/index.ts +++ b/lib/modules/platform/gerrit/index.ts @@ -25,7 +25,6 @@ import type { } from '../types'; import { repoFingerprint } from '../util'; -import { smartTruncate } from '../utils/pr-body'; import { readOnlyIssueBody } from '../utils/read-only-issue-body'; import { client } from './client'; import { configureScm } from './scm'; @@ -396,7 +395,7 @@ export async function ensureComment( export function massageMarkdown(prBody: string): string { //TODO: do more Gerrit specific replacements? - return smartTruncate(readOnlyIssueBody(prBody), maxBodyLength()) + return readOnlyIssueBody(prBody) .replace(regEx(/Pull Request(s)?/g), 'Change-Request$1') .replace(regEx(/\bPR(s)?\b/g), 'Change-Request$1') .replace(regEx(/<\/?summary>/g), '**') @@ -423,6 +422,10 @@ export function maxBodyLength(): number { return 16384; //TODO: check the real gerrit limit (max. chars) } +export function maxCommentLength(): number { + return Infinity; +} + export function deleteLabel(number: number, label: string): Promise { return Promise.resolve(); } diff --git a/lib/modules/platform/gitea/index.spec.ts b/lib/modules/platform/gitea/index.spec.ts index 198ea0962ad7a5..4cef3dfe68dcbb 100644 --- a/lib/modules/platform/gitea/index.spec.ts +++ b/lib/modules/platform/gitea/index.spec.ts @@ -2989,4 +2989,16 @@ describe('modules/platform/gitea/index', () => { await expect(gitea.getJsonFile('file.json')).rejects.toThrow(); }); }); + + describe('maxBodyLength()', () => { + it('returns 1000000', () => { + expect(gitea.maxBodyLength()).toBe(1000000); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(gitea.maxCommentLength()).toBe(Infinity); + }); + }); }); diff --git a/lib/modules/platform/gitea/index.ts b/lib/modules/platform/gitea/index.ts index d67cf0ceea290e..8d0e00c86faa1d 100644 --- a/lib/modules/platform/gitea/index.ts +++ b/lib/modules/platform/gitea/index.ts @@ -39,7 +39,6 @@ import type { UpdatePrConfig, } from '../types'; import { repoFingerprint } from '../util'; -import { smartTruncate } from '../utils/pr-body'; import * as helper from './gitea-helper'; import { giteaHttp } from './gitea-helper'; import { GiteaPrCache } from './pr-cache'; @@ -998,16 +997,21 @@ const platform: Platform = { }, massageMarkdown(prBody: string): string { - return smartTruncate(smartLinks(prBody), maxBodyLength()); + return smartLinks(prBody); }, maxBodyLength, + maxCommentLength, }; export function maxBodyLength(): number { return 1000000; } +export function maxCommentLength(): number { + return Infinity; +} + /* eslint-disable @typescript-eslint/unbound-method */ export const { addAssignees, diff --git a/lib/modules/platform/github/index.spec.ts b/lib/modules/platform/github/index.spec.ts index 47127a5504c371..311f752ee74881 100644 --- a/lib/modules/platform/github/index.spec.ts +++ b/lib/modules/platform/github/index.spec.ts @@ -4203,4 +4203,16 @@ describe('modules/platform/github/index', () => { expect(res).toBeNull(); }); }); + + describe('maxBodyLength()', () => { + it('returns 60000', () => { + expect(github.maxBodyLength()).toBe(60000); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(github.maxCommentLength()).toBe(Infinity); + }); + }); }); diff --git a/lib/modules/platform/github/index.ts b/lib/modules/platform/github/index.ts index 23a8210574053a..e1d2bd37f4bcf6 100644 --- a/lib/modules/platform/github/index.ts +++ b/lib/modules/platform/github/index.ts @@ -66,7 +66,6 @@ import type { } from '../types'; import { repoFingerprint } from '../util'; import { normalizeNamePerEcosystem } from '../utils/github-alerts'; -import { smartTruncate } from '../utils/pr-body'; import { remoteBranchExists } from './branch'; import { coerceRestPr, githubApi } from './common'; import { @@ -1941,7 +1940,7 @@ export async function mergePr({ export function massageMarkdown(input: string): string { if (platformConfig.isGhe) { - return smartTruncate(input, maxBodyLength()); + return input; } const massagedInput = massageMarkdownLinks(input) // to be safe, replace all github.com links with redirect.github.com @@ -1961,13 +1960,17 @@ export function massageMarkdown(input: string): string { .replace('> ⚠ **Warning**\n> \n', '> [!WARNING]\n') .replace('> ⚠️ **Warning**\n> \n', '> [!WARNING]\n') .replace('> ❗ **Important**\n> \n', '> [!IMPORTANT]\n'); - return smartTruncate(massagedInput, maxBodyLength()); + return massagedInput; } export function maxBodyLength(): number { return GitHubMaxPrBodyLen; } +export function maxCommentLength(): number { + return Infinity; +} + export async function getVulnerabilityAlerts(): Promise { if (config.hasVulnerabilityAlertsEnabled === false) { logger.debug('No vulnerability alerts enabled for repo'); diff --git a/lib/modules/platform/gitlab/index.spec.ts b/lib/modules/platform/gitlab/index.spec.ts index 671e64080280d8..74b51fd8f0f3ae 100644 --- a/lib/modules/platform/gitlab/index.spec.ts +++ b/lib/modules/platform/gitlab/index.spec.ts @@ -3068,25 +3068,23 @@ These updates have all been created already. Click a checkbox below to force a r expect(gitlab.massageMarkdown(prBody)).toMatchSnapshot(); expect(smartTruncate).not.toHaveBeenCalled(); }); + }); - it('truncates description if too low API version', async () => { - jest.doMock('../utils/pr-body'); - const { smartTruncate } = await import('../utils/pr-body'); - + describe('maxBodyLength()', () => { + it('maxBodyLength is 25000 if too low API version', async () => { await initFakePlatform('13.3.0'); - gitlab.massageMarkdown(prBody); - expect(smartTruncate).toHaveBeenCalledTimes(1); - expect(smartTruncate).toHaveBeenCalledWith(expect.any(String), 25000); + expect(gitlab.maxBodyLength()).toBe(25000); }); - it('truncates description for API version gt 13.4', async () => { - jest.doMock('../utils/pr-body'); - const { smartTruncate } = await import('../utils/pr-body'); - + it('maxBodyLength is 1000000 description for API version gt 13.4', async () => { await initFakePlatform('13.4.1'); - gitlab.massageMarkdown(prBody); - expect(smartTruncate).toHaveBeenCalledTimes(1); - expect(smartTruncate).toHaveBeenCalledWith(expect.any(String), 1000000); + expect(gitlab.maxBodyLength()).toBe(1000000); + }); + }); + + describe('maxCommentLength()', () => { + it('returns Infinity', () => { + expect(gitlab.maxCommentLength()).toBe(Infinity); }); }); diff --git a/lib/modules/platform/gitlab/index.ts b/lib/modules/platform/gitlab/index.ts index 264a05bb10b7ed..7dc082f55f5038 100644 --- a/lib/modules/platform/gitlab/index.ts +++ b/lib/modules/platform/gitlab/index.ts @@ -54,7 +54,6 @@ import type { UpdatePrConfig, } from '../types'; import { repoFingerprint } from '../util'; -import { smartTruncate } from '../utils/pr-body'; import { getMemberUserIDs, getMemberUsernames, @@ -904,7 +903,7 @@ export function massageMarkdown(input: string): string { .replace(regEx(/\]\(\.\.\/pull\//g), '](!') // Strip unicode null characters as GitLab markdown does not permit them .replace(regEx(/\u0000/g), ''); // eslint-disable-line no-control-regex - return smartTruncate(desc, maxBodyLength()); + return desc; } export function maxBodyLength(): number { @@ -919,6 +918,10 @@ export function maxBodyLength(): number { } } +export function maxCommentLength(): number { + return Infinity; +} + // Branch function matchesState(state: string, desiredState: string): boolean { diff --git a/lib/modules/platform/local/index.spec.ts b/lib/modules/platform/local/index.spec.ts index 140f38ec6f509d..edb38d57608fb4 100644 --- a/lib/modules/platform/local/index.spec.ts +++ b/lib/modules/platform/local/index.spec.ts @@ -69,6 +69,10 @@ describe('modules/platform/local/index', () => { expect(platform.maxBodyLength()).toBe(Infinity); }); + it('maxCommentLength', () => { + expect(platform.maxCommentLength()).toBe(Infinity); + }); + it('updatePr', async () => { expect(await platform.updatePr()).toBeUndefined(); }); diff --git a/lib/modules/platform/local/index.ts b/lib/modules/platform/local/index.ts index c3646ebb4f8241..f421d034a15d2f 100644 --- a/lib/modules/platform/local/index.ts +++ b/lib/modules/platform/local/index.ts @@ -69,6 +69,10 @@ export function maxBodyLength(): number { return Infinity; } +export function maxCommentLength(): number { + return Infinity; +} + export function updatePr(): Promise { return Promise.resolve(); } diff --git a/lib/modules/platform/types.ts b/lib/modules/platform/types.ts index 8a98fe9b4b2cc0..0d9d7c8591f159 100644 --- a/lib/modules/platform/types.ts +++ b/lib/modules/platform/types.ts @@ -279,6 +279,7 @@ export interface Platform { expandGroupMembers?(reviewersOrAssignees: string[]): Promise; maxBodyLength(): number; + maxCommentLength(): number; } export interface PlatformScm { diff --git a/lib/workers/repository/config-migration/pr/index.spec.ts b/lib/workers/repository/config-migration/pr/index.spec.ts index 162d60b5ce07e2..f8f328ab282091 100644 --- a/lib/workers/repository/config-migration/pr/index.spec.ts +++ b/lib/workers/repository/config-migration/pr/index.spec.ts @@ -249,6 +249,7 @@ describe('workers/repository/config-migration/pr/index', () => { beforeEach(() => { GlobalConfig.reset(); scm.deleteBranch.mockResolvedValue(); + platform.massageMarkdown.mockImplementationOnce((x) => x); }); it('throws when trying to create a new PR', async () => { diff --git a/lib/workers/repository/config-migration/pr/index.ts b/lib/workers/repository/config-migration/pr/index.ts index 53a3ccb57744cd..55b3ad93e22f3e 100644 --- a/lib/workers/repository/config-migration/pr/index.ts +++ b/lib/workers/repository/config-migration/pr/index.ts @@ -5,6 +5,7 @@ import { logger } from '../../../../logger'; import { platform } from '../../../../modules/platform'; import { hashBody } from '../../../../modules/platform/pr-body'; import { scm } from '../../../../modules/platform/scm'; +import { smartTruncate } from '../../../../modules/platform/utils/pr-body'; import { emojify } from '../../../../util/emoji'; import { coerceString } from '../../../../util/string'; import * as template from '../../../../util/template'; @@ -64,6 +65,7 @@ ${ logger.trace({ prBody }, 'prBody'); prBody = platform.massageMarkdown(prBody); + prBody = smartTruncate(prBody, platform.maxBodyLength()); if (existingPr) { logger.debug('Found open migration PR'); diff --git a/lib/workers/repository/dependency-dashboard.ts b/lib/workers/repository/dependency-dashboard.ts index d23e5e70beb298..d7cafc4df6f4ee 100644 --- a/lib/workers/repository/dependency-dashboard.ts +++ b/lib/workers/repository/dependency-dashboard.ts @@ -4,6 +4,7 @@ import type { RenovateConfig } from '../../config/types'; import { logger } from '../../logger'; import type { PackageFile } from '../../modules/manager/types'; import { platform } from '../../modules/platform'; +import { smartTruncate } from '../../modules/platform/utils/pr-body'; import { regEx } from '../../util/regex'; import { coerceString } from '../../util/string'; import * as template from '../../util/template'; @@ -515,7 +516,10 @@ export async function ensureDependencyDashboard( await platform.ensureIssue({ title: config.dependencyDashboardTitle!, reuseTitle, - body: platform.massageMarkdown(issueBody), + body: smartTruncate( + platform.massageMarkdown(issueBody), + platform.maxBodyLength(), + ), labels: config.dependencyDashboardLabels, confidential: config.confidential, }); diff --git a/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap b/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap deleted file mode 100644 index 625b52c907cee7..00000000000000 --- a/lib/workers/repository/onboarding/pr/__snapshots__/index.spec.ts.snap +++ /dev/null @@ -1,218 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`workers/repository/onboarding/pr/index ensureOnboardingPr() creates PR with empty footer and header(onboardingRebaseCheckbox="false") 1`] = ` -" - -Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. - -🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. - - - ---- -### Detected Package Files - - * \`package.json\` (npm) - -### What to Expect - -It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away. - ---- - -❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section. -If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions). - - ---- - - - - -" -`; - -exports[`workers/repository/onboarding/pr/index ensureOnboardingPr() creates PR with empty footer and header(onboardingRebaseCheckbox="true") 1`] = ` -" - -Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. - -🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. - - - ---- -### Detected Package Files - - * \`package.json\` (npm) - -### What to Expect - -It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away. - ---- - -❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section. -If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions). - - ---- - - - [ ] If you want to rebase/retry this PR, click this checkbox. - - ---- - - - - -" -`; - -exports[`workers/repository/onboarding/pr/index ensureOnboardingPr() creates PR with footer and header using templating(onboardingRebaseCheckbox="false") 1`] = ` -"This is a header for platform:github - -Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. - -🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. - - - ---- -### Detected Package Files - - * \`package.json\` (npm) - -### What to Expect - -It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away. - ---- - -❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section. -If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions). - - ---- - -And this is a footer for repository:test baseBranch:some-branch - - -" -`; - -exports[`workers/repository/onboarding/pr/index ensureOnboardingPr() creates PR with footer and header using templating(onboardingRebaseCheckbox="true") 1`] = ` -"This is a header for platform:github - -Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. - -🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. - - - ---- -### Detected Package Files - - * \`package.json\` (npm) - -### What to Expect - -It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away. - ---- - -❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section. -If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions). - - ---- - - - [ ] If you want to rebase/retry this PR, click this checkbox. - - ---- - -And this is a footer for repository:test baseBranch:some-branch - - -" -`; - -exports[`workers/repository/onboarding/pr/index ensureOnboardingPr() creates PR with footer and header with trailing and leading newlines(onboardingRebaseCheckbox="false") 1`] = ` -" - -This should not be the first line of the PR - -Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. - -🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. - - - ---- -### Detected Package Files - - * \`package.json\` (npm) - -### What to Expect - -It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away. - ---- - -❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section. -If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions). - - ---- - -There should be several empty lines at the end of the PR - - - - - -" -`; - -exports[`workers/repository/onboarding/pr/index ensureOnboardingPr() creates PR with footer and header with trailing and leading newlines(onboardingRebaseCheckbox="true") 1`] = ` -" - -This should not be the first line of the PR - -Welcome to [Renovate](https://github.com/renovatebot/renovate)! This is an onboarding PR to help you understand and configure settings before regular Pull Requests begin. - -🚦 To activate Renovate, merge this Pull Request. To disable Renovate, simply close this Pull Request unmerged. - - - ---- -### Detected Package Files - - * \`package.json\` (npm) - -### What to Expect - -It looks like your repository dependencies are already up-to-date and no Pull Requests will be necessary right away. - ---- - -❓ Got questions? Check out Renovate's [Docs](https://docs.renovatebot.com/), particularly the Getting Started section. -If you need any further assistance then you can also [request help here](https://github.com/renovatebot/renovate/discussions). - - ---- - - - [ ] If you want to rebase/retry this PR, click this checkbox. - - ---- - -There should be several empty lines at the end of the PR - - - - - -" -`; diff --git a/lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap b/lib/workers/repository/onboarding/pr/body/__snapshots__/config-description.spec.ts.snap similarity index 75% rename from lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap rename to lib/workers/repository/onboarding/pr/body/__snapshots__/config-description.spec.ts.snap index 1d338c6d487a4d..de58e26a9826ef 100644 --- a/lib/workers/repository/onboarding/pr/__snapshots__/config-description.spec.ts.snap +++ b/lib/workers/repository/onboarding/pr/body/__snapshots__/config-description.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`workers/repository/onboarding/pr/config-description getConfigDesc() contains the onboardingConfigFileName if set 1`] = ` +exports[`workers/repository/onboarding/pr/body/config-description getConfigDesc() contains the onboardingConfigFileName if set 1`] = ` " ### Configuration Summary @@ -15,7 +15,7 @@ Based on the default config's presets, Renovate will: " `; -exports[`workers/repository/onboarding/pr/config-description getConfigDesc() falls back to "renovate.json" if onboardingConfigFileName is not set 1`] = ` +exports[`workers/repository/onboarding/pr/body/config-description getConfigDesc() falls back to "renovate.json" if onboardingConfigFileName is not set 1`] = ` " ### Configuration Summary @@ -30,7 +30,7 @@ Based on the default config's presets, Renovate will: " `; -exports[`workers/repository/onboarding/pr/config-description getConfigDesc() falls back to "renovate.json" if onboardingConfigFileName is not valid 1`] = ` +exports[`workers/repository/onboarding/pr/body/config-description getConfigDesc() falls back to "renovate.json" if onboardingConfigFileName is not valid 1`] = ` " ### Configuration Summary @@ -45,7 +45,7 @@ Based on the default config's presets, Renovate will: " `; -exports[`workers/repository/onboarding/pr/config-description getConfigDesc() returns a full list 1`] = ` +exports[`workers/repository/onboarding/pr/body/config-description getConfigDesc() returns a full list 1`] = ` " ### Configuration Summary @@ -63,7 +63,7 @@ Based on the default config's presets, Renovate will: " `; -exports[`workers/repository/onboarding/pr/config-description getConfigDesc() include retry/refresh checkbox message only if onboardingRebaseCheckbox is true 1`] = ` +exports[`workers/repository/onboarding/pr/body/config-description getConfigDesc() include retry/refresh checkbox message only if onboardingRebaseCheckbox is true 1`] = ` " ### Configuration Summary diff --git a/lib/workers/repository/onboarding/pr/base-branch.spec.ts b/lib/workers/repository/onboarding/pr/body/base-branch.spec.ts similarity index 82% rename from lib/workers/repository/onboarding/pr/base-branch.spec.ts rename to lib/workers/repository/onboarding/pr/body/base-branch.spec.ts index 803ab8ce786724..52fd756795f8be 100644 --- a/lib/workers/repository/onboarding/pr/base-branch.spec.ts +++ b/lib/workers/repository/onboarding/pr/body/base-branch.spec.ts @@ -1,8 +1,8 @@ -import type { RenovateConfig } from '../../../../../test/util'; -import { partial } from '../../../../../test/util'; +import type { RenovateConfig } from '../../../../../../test/util'; +import { partial } from '../../../../../../test/util'; import { getBaseBranchDesc } from './base-branch'; -describe('workers/repository/onboarding/pr/base-branch', () => { +describe('workers/repository/onboarding/pr/body/base-branch', () => { describe('getBaseBranchDesc()', () => { let config: RenovateConfig; diff --git a/lib/workers/repository/onboarding/pr/base-branch.ts b/lib/workers/repository/onboarding/pr/body/base-branch.ts similarity index 87% rename from lib/workers/repository/onboarding/pr/base-branch.ts rename to lib/workers/repository/onboarding/pr/body/base-branch.ts index 0e574e18dc6fc8..448b5d036ade7b 100644 --- a/lib/workers/repository/onboarding/pr/base-branch.ts +++ b/lib/workers/repository/onboarding/pr/body/base-branch.ts @@ -1,4 +1,4 @@ -import type { RenovateConfig } from '../../../../config/types'; +import type { RenovateConfig } from '../../../../../config/types'; export function getBaseBranchDesc(config: RenovateConfig): string { // Describe base branch only if it's configured diff --git a/lib/workers/repository/onboarding/pr/config-description.spec.ts b/lib/workers/repository/onboarding/pr/body/config-description.spec.ts similarity index 92% rename from lib/workers/repository/onboarding/pr/config-description.spec.ts rename to lib/workers/repository/onboarding/pr/body/config-description.spec.ts index 399e5f8a7dd88e..9d38c5ab3cbe8a 100644 --- a/lib/workers/repository/onboarding/pr/config-description.spec.ts +++ b/lib/workers/repository/onboarding/pr/body/config-description.spec.ts @@ -1,9 +1,9 @@ -import type { RenovateConfig } from '../../../../../test/util'; -import { partial } from '../../../../../test/util'; -import type { PackageFile } from '../../../../modules/manager/types'; +import type { RenovateConfig } from '../../../../../../test/util'; +import { partial } from '../../../../../../test/util'; +import type { PackageFile } from '../../../../../modules/manager/types'; import { getConfigDesc } from './config-description'; -describe('workers/repository/onboarding/pr/config-description', () => { +describe('workers/repository/onboarding/pr/body/config-description', () => { describe('getConfigDesc()', () => { let config: RenovateConfig; diff --git a/lib/workers/repository/onboarding/pr/config-description.ts b/lib/workers/repository/onboarding/pr/body/config-description.ts similarity index 86% rename from lib/workers/repository/onboarding/pr/config-description.ts rename to lib/workers/repository/onboarding/pr/body/config-description.ts index e1c092c88e0d79..53b74cdc76ee80 100644 --- a/lib/workers/repository/onboarding/pr/config-description.ts +++ b/lib/workers/repository/onboarding/pr/body/config-description.ts @@ -1,9 +1,9 @@ import is from '@sindresorhus/is'; -import { configFileNames } from '../../../../config/app-strings'; -import type { RenovateConfig } from '../../../../config/types'; -import { logger } from '../../../../logger'; -import type { PackageFile } from '../../../../modules/manager/types'; -import { emojify } from '../../../../util/emoji'; +import { configFileNames } from '../../../../../config/app-strings'; +import type { RenovateConfig } from '../../../../../config/types'; +import { logger } from '../../../../../logger'; +import type { PackageFile } from '../../../../../modules/manager/types'; +import { emojify } from '../../../../../util/emoji'; const defaultConfigFile = configFileNames[0]; diff --git a/lib/workers/repository/onboarding/pr/body/index.spec.ts b/lib/workers/repository/onboarding/pr/body/index.spec.ts new file mode 100644 index 00000000000000..8637c6d0b7ad46 --- /dev/null +++ b/lib/workers/repository/onboarding/pr/body/index.spec.ts @@ -0,0 +1,204 @@ +import type { RenovateConfig } from '../../../../../../test/util'; +import { mocked, platform } from '../../../../../../test/util'; +import { getConfig } from '../../../../../config/defaults'; +import { GlobalConfig } from '../../../../../config/global'; +import { logger } from '../../../../../logger'; +import type { PackageFile } from '../../../../../modules/manager/types'; +import type { BranchConfig } from '../../../../types'; +import * as _baseBranch from './base-branch'; +import * as _configDescription from './config-description'; +import * as _prList from './pr-list'; +import { getPrBody } from '.'; + +jest.mock('./pr-list'); +const prList = mocked(_prList); + +jest.mock('./config-description'); +const configDescription = mocked(_configDescription); + +jest.mock('./base-branch'); +const baseBranch = mocked(_baseBranch); + +describe('workers/repository/onboarding/pr/body/index', () => { + describe('getPrBody', () => { + let config: RenovateConfig; + let packageFiles: Record; + let branches: BranchConfig[]; + + beforeEach(() => { + config = { + ...getConfig(), + errors: [], + warnings: [], + description: [], + prFooter: undefined, + prHeader: undefined, + }; + packageFiles = {}; + branches = []; + + prList.getPrList.mockReturnValueOnce('getPrList'); + configDescription.getConfigDesc.mockReturnValueOnce('getConfigDesc'); + baseBranch.getBaseBranchDesc.mockReturnValueOnce('getBaseBranchDesc'); + platform.massageMarkdown.mockImplementation((x) => x); + GlobalConfig.reset(); + }); + + it('creates body without comments if maxbodylength is long enough', () => { + platform.maxBodyLength.mockReturnValueOnce(Infinity); + const template = 'PrBody'; + + const res = getPrBody(template, packageFiles, config, branches, ''); + + expect(res.body).toBe('PrBody'); + expect(res.comments).toEqual([]); + }); + + it('creates body with prFooter', () => { + platform.maxBodyLength.mockReturnValue(Infinity); + const template = 'PrBody'; + + const res = getPrBody( + template, + packageFiles, + { ...config, prFooter: 'Footer' }, + branches, + '', + ); + + expect(res.body).toContain('Footer'); + }); + + it('creates body with prHeader', () => { + platform.maxBodyLength.mockReturnValue(Infinity); + const template = 'PrBody'; + + const res = getPrBody( + template, + packageFiles, + { ...config, prHeader: 'Header' }, + branches, + '', + ); + + expect(res.body).toStartWith('Header'); + }); + + it('creates body with Pr List in comment', () => { + platform.maxBodyLength.mockReturnValue('PrBody'.length); + platform.massageMarkdown.mockImplementationOnce((x) => x); + platform.massageMarkdown.mockImplementationOnce((_) => 'PrBody'); + const template = 'PrBody{{PRLIST}}'; + + const res = getPrBody(template, packageFiles, config, branches, ''); + + expect(res).toStrictEqual({ + body: 'PrBody', + comments: [{ title: 'PR List', content: 'getPrList' }], + }); + }); + + it('creates body with Pr List & Package Files in comments', () => { + platform.maxBodyLength.mockReturnValue('PrBody'.length); + platform.massageMarkdown.mockImplementationOnce((x) => x); + platform.massageMarkdown.mockImplementationOnce( + (x) => 'PrBody{{PACKAGE FILES}}', + ); + platform.massageMarkdown.mockImplementationOnce((_) => 'PrBody'); + packageFiles = { npm: [{ packageFile: 'package.json', deps: [] }] }; + const template = 'PrBody{{PRLIST}}{{PACKAGE FILES}}'; + + const res = getPrBody(template, packageFiles, config, branches, ''); + + expect(res).toStrictEqual({ + body: 'PrBody', + comments: [ + { title: 'PR List', content: 'getPrList' }, + { + title: 'Package Files', + content: '### Detected Package Files\n\n * `package.json` (npm)\n', + }, + ], + }); + }); + + it('creates & truncates body if body is too long', () => { + platform.maxBodyLength.mockReturnValue(2); + platform.massageMarkdown.mockImplementationOnce((x) => x); + platform.massageMarkdown.mockImplementationOnce( + (x) => 'PrBody{{PACKAGE FILES}}', + ); + platform.massageMarkdown.mockImplementationOnce((_) => 'PrBody'); + packageFiles = { npm: [{ packageFile: 'package.json', deps: [] }] }; + const template = 'PrBody{{PRLIST}}{{PACKAGE FILES}}'; + + const res = getPrBody(template, packageFiles, config, branches, ''); + + expect(res.body).toBe('Pr'); + }); + + it('creates body with empty configDescription if dryRun', () => { + platform.maxBodyLength.mockReturnValueOnce(Infinity); + const template = '{{CONFIG}}\n'; + GlobalConfig.set({ dryRun: 'full' }); + + const res = getPrBody(template, packageFiles, config, branches, ''); + + expect(logger.info).toHaveBeenCalledWith( + 'DRY-RUN: Would check branch renovate/configure', + ); + expect(res.body).toBe(''); + }); + + it('creates body with footer and header using templating', () => { + platform.maxBodyLength.mockReturnValue(Infinity); + const template = ''; + + const res = getPrBody( + template, + packageFiles, + { + ...config, + prFooter: + 'And this is a footer for repository:{{repository}} baseBranch:{{baseBranch}}', + prHeader: 'This is a header for platform:{{platform}}', + baseBranch: 'some-branch', + repository: 'test', + }, + branches, + '', + ); + + expect(res.body).toStartWith('This is a header for platform:github'); + + expect(res.body).toEndWith( + 'And this is a footer for repository:test baseBranch:some-branch\n', + ); + }); + + it('creates & truncates comments if comment is too long', () => { + platform.maxBodyLength.mockReturnValue('PrBody'.length); + platform.maxCommentLength.mockReturnValue(3); + platform.massageMarkdown.mockImplementationOnce((x) => x); + platform.massageMarkdown.mockImplementationOnce( + (x) => 'PrBody{{PACKAGE FILES}}', + ); + platform.massageMarkdown.mockImplementationOnce((_) => 'PrBody'); + packageFiles = { npm: [{ packageFile: 'package.json', deps: [] }] }; + const template = 'PrBody{{PRLIST}}{{PACKAGE FILES}}'; + + const res = getPrBody(template, packageFiles, config, branches, ''); + + expect(res).toStrictEqual({ + body: 'PrBody', + comments: [ + { title: 'PR List', content: 'get' }, + { + title: 'Package Files', + content: '###', + }, + ], + }); + }); + }); +}); diff --git a/lib/workers/repository/onboarding/pr/body/index.ts b/lib/workers/repository/onboarding/pr/body/index.ts new file mode 100644 index 00000000000000..91071dce1a53a7 --- /dev/null +++ b/lib/workers/repository/onboarding/pr/body/index.ts @@ -0,0 +1,146 @@ +import is from '@sindresorhus/is'; +import { GlobalConfig } from '../../../../../config/global'; +import type { RenovateConfig } from '../../../../../config/types'; +import { logger } from '../../../../../logger'; +import type { PackageFile } from '../../../../../modules/manager/types'; +import { platform } from '../../../../../modules/platform'; +import { smartTruncate } from '../../../../../modules/platform/utils/pr-body'; +import * as template from '../../../../../util/template'; +import type { BranchConfig } from '../../../../types'; + +import { + getDepWarningsOnboardingPR, + getErrors, + getWarnings, +} from '../../../errors-warnings'; +import { getBaseBranchDesc } from './base-branch'; +import { getConfigDesc } from './config-description'; +import { getPrList } from './pr-list'; + +interface PrContent { + packageFiles: string; + config: string; + warnings: string; + errors: string; + baseBranch: string; + prList: string; + prHeader: string; + prFooter: string; + onboardingConfigHashComment: string; +} + +interface PrBodyContent { + body: string; + comments: PrComment[]; +} + +interface PrComment { + title: 'PR List' | 'Package Files'; + content: string; +} + +export function getPrBody( + prTemplate: string, + packageFiles: Record | null, + config: RenovateConfig, + branches: BranchConfig[], + onboardingConfigHashComment: string, +): PrBodyContent { + let packageFilesContent = ''; + if (packageFiles && Object.entries(packageFiles).length) { + let files: string[] = []; + for (const [manager, managerFiles] of Object.entries(packageFiles)) { + files = files.concat( + managerFiles.map((file) => ` * \`${file.packageFile}\` (${manager})`), + ); + } + packageFilesContent = + '### Detected Package Files\n\n' + files.join('\n') + '\n'; + } + + let configDesc = ''; + if (GlobalConfig.get('dryRun')) { + // TODO: types (#22198) + logger.info(`DRY-RUN: Would check branch ${config.onboardingBranch!}`); + } else { + configDesc = getConfigDesc(config, packageFiles!); + } + + let prHeader = ''; + if (is.string(config.prHeader)) { + prHeader = template.compile(config.prHeader, config); + } + let prFooter = ''; + if (is.string(config.prFooter)) { + prFooter = template.compile(config.prFooter, config); + } + + const content = { + packageFiles: packageFilesContent, + config: configDesc, + warnings: + getWarnings(config) + getDepWarningsOnboardingPR(packageFiles!, config), + errors: getErrors(config), + baseBranch: getBaseBranchDesc(config), + prList: getPrList(config, branches), + prHeader, + prFooter, + onboardingConfigHashComment, + }; + + const result: PrBodyContent = { + body: createPrBody(prTemplate, content), + comments: [], + }; + if (result.body.length <= platform.maxBodyLength()) { + return result; + } + + if (content.prList) { + result.comments.push({ + title: 'PR List', + content: smartTruncate(content.prList, platform.maxCommentLength()), + }); + content.prList = 'Please see comment below for what to expect'; + + result.body = createPrBody(prTemplate, content); + if (result.body.length <= platform.maxBodyLength()) { + return result; + } + } + + if (content.packageFiles) { + result.comments.push({ + title: 'Package Files', + content: smartTruncate(content.packageFiles, platform.maxCommentLength()), + }); + content.packageFiles = + 'Please see comment below for detected Package Files\n'; + + result.body = createPrBody(prTemplate, content); + if (result.body.length <= platform.maxBodyLength()) { + return result; + } + } + + result.body = smartTruncate(result.body, platform.maxBodyLength()); + return result; +} + +function createPrBody(template: string, content: PrContent): string { + let prBody = template.replace('{{PACKAGE FILES}}\n', content.packageFiles); + prBody = prBody.replace('{{CONFIG}}\n', content.config); + prBody = prBody.replace('{{WARNINGS}}\n', content.warnings); + prBody = prBody.replace('{{ERRORS}}\n', content.errors); + prBody = prBody.replace('{{BASEBRANCH}}\n', content.baseBranch); + prBody = prBody.replace('{{PRLIST}}\n', content.prList); + if (content.prHeader) { + prBody = `${content.prHeader}\n\n${prBody}`; + } + if (content.prFooter) { + prBody = `${prBody}\n---\n\n${content.prFooter}\n`; + } + prBody += content.onboardingConfigHashComment; + prBody = platform.massageMarkdown(prBody); + return prBody; +} diff --git a/lib/workers/repository/onboarding/pr/pr-list.spec.ts b/lib/workers/repository/onboarding/pr/body/pr-list.spec.ts similarity index 93% rename from lib/workers/repository/onboarding/pr/pr-list.spec.ts rename to lib/workers/repository/onboarding/pr/body/pr-list.spec.ts index 6e1637628b170e..676044f4de75e8 100644 --- a/lib/workers/repository/onboarding/pr/pr-list.spec.ts +++ b/lib/workers/repository/onboarding/pr/body/pr-list.spec.ts @@ -1,9 +1,9 @@ -import type { RenovateConfig } from '../../../../../test/util'; -import { partial } from '../../../../../test/util'; -import type { BranchConfig } from '../../../types'; +import type { RenovateConfig } from '../../../../../../test/util'; +import { partial } from '../../../../../../test/util'; +import type { BranchConfig } from '../../../../types'; import { getPrList } from './pr-list'; -describe('workers/repository/onboarding/pr/pr-list', () => { +describe('workers/repository/onboarding/pr/body/pr-list', () => { describe('getPrList()', () => { let config: RenovateConfig; diff --git a/lib/workers/repository/onboarding/pr/pr-list.ts b/lib/workers/repository/onboarding/pr/body/pr-list.ts similarity index 89% rename from lib/workers/repository/onboarding/pr/pr-list.ts rename to lib/workers/repository/onboarding/pr/body/pr-list.ts index 3411b0a37fca46..ba5c49ce3db7c1 100644 --- a/lib/workers/repository/onboarding/pr/pr-list.ts +++ b/lib/workers/repository/onboarding/pr/body/pr-list.ts @@ -1,8 +1,8 @@ -import type { RenovateConfig } from '../../../../config/types'; -import { logger } from '../../../../logger'; -import { emojify } from '../../../../util/emoji'; -import { regEx } from '../../../../util/regex'; -import type { BranchConfig } from '../../../types'; +import type { RenovateConfig } from '../../../../../config/types'; +import { logger } from '../../../../../logger'; +import { emojify } from '../../../../../util/emoji'; +import { regEx } from '../../../../../util/regex'; +import type { BranchConfig } from '../../../../types'; export function getPrList( config: RenovateConfig, diff --git a/lib/workers/repository/onboarding/pr/index.spec.ts b/lib/workers/repository/onboarding/pr/index.spec.ts index ecf18987c0c5c6..5531223bbd0c00 100644 --- a/lib/workers/repository/onboarding/pr/index.spec.ts +++ b/lib/workers/repository/onboarding/pr/index.spec.ts @@ -1,6 +1,6 @@ import type { RequestError, Response } from 'got'; import type { RenovateConfig } from '../../../../../test/util'; -import { partial, platform, scm } from '../../../../../test/util'; +import { mocked, partial, platform, scm } from '../../../../../test/util'; import { getConfig } from '../../../../config/defaults'; import { GlobalConfig } from '../../../../config/global'; import { logger } from '../../../../logger'; @@ -9,10 +9,14 @@ import type { Pr } from '../../../../modules/platform'; import * as memCache from '../../../../util/cache/memory'; import type { BranchConfig } from '../../../types'; import { OnboardingState } from '../common'; +import * as _prBody from './body'; import { ensureOnboardingPr } from '.'; jest.mock('../../../../util/git'); +jest.mock('./body'); +const prBody = mocked(_prBody); + describe('workers/repository/onboarding/pr/index', () => { describe('ensureOnboardingPr()', () => { let config: RenovateConfig; @@ -20,7 +24,7 @@ describe('workers/repository/onboarding/pr/index', () => { let branches: BranchConfig[]; const bodyStruct = { - hash: '6aa71f8cb7b1503b883485c8f5bd564b31923b9c7fa765abe2a7338af40e03b1', + hash: '230d8358dc8e8890b4c58deeb62912ee2f20357ae92a5cc861b98e68fe31acb5', }; beforeEach(() => { @@ -34,12 +38,21 @@ describe('workers/repository/onboarding/pr/index', () => { packageFiles = { npm: [{ packageFile: 'package.json', deps: [] }] }; branches = []; platform.massageMarkdown.mockImplementation((input) => input); - platform.createPr.mockResolvedValueOnce(partial()); + platform.createPr.mockResolvedValueOnce(partial({ number: 3.14 })); + platform.maxBodyLength.mockReturnValueOnce(Infinity); + prBody.getPrBody.mockReturnValue({ + body: 'body', + comments: [ + { content: 'content', title: 'PR List' }, + { content: 'content', title: 'Package Files' }, + ], + }); GlobalConfig.reset(); }); it('returns if onboarded', async () => { config.repoIsOnboarded = true; + await expect( ensureOnboardingPr(config, packageFiles, branches), ).resolves.not.toThrow(); @@ -60,6 +73,7 @@ describe('workers/repository/onboarding/pr/index', () => { config.repoIsOnboarded = false; config.onboardingRebaseCheckbox = onboardingRebaseCheckbox; OnboardingState.prUpdateRequested = prUpdateRequested; + await expect( ensureOnboardingPr(config, packageFiles, branches), ).resolves.not.toThrow(); @@ -70,6 +84,7 @@ describe('workers/repository/onboarding/pr/index', () => { it('creates PR', async () => { await ensureOnboardingPr(config, packageFiles, branches); + expect(platform.createPr).toHaveBeenCalledTimes(1); }); @@ -83,6 +98,7 @@ describe('workers/repository/onboarding/pr/index', () => { packageFiles, branches, ); + expect(platform.createPr).toHaveBeenCalledWith( expect.objectContaining({ prTitle: 'chore: Configure Renovate', @@ -100,6 +116,7 @@ describe('workers/repository/onboarding/pr/index', () => { packageFiles, branches, ); + expect(platform.createPr).toHaveBeenCalledTimes(1); expect(platform.createPr.mock.calls[0][0].labels).toEqual([ 'additional-label', @@ -107,112 +124,20 @@ describe('workers/repository/onboarding/pr/index', () => { ]); }); - it.each` - onboardingRebaseCheckbox - ${false} - ${true} - `( - 'creates PR with empty footer and header' + - '(onboardingRebaseCheckbox="$onboardingRebaseCheckbox")', - async ({ onboardingRebaseCheckbox }) => { - config.onboardingRebaseCheckbox = onboardingRebaseCheckbox; - OnboardingState.prUpdateRequested = true; // case 'false' is tested in "breaks early when onboarding" - await ensureOnboardingPr( - { - ...config, - prHeader: '', - prFooter: '', - }, - packageFiles, - branches, - ); - expect(platform.createPr).toHaveBeenCalledTimes(1); - expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot(); - }, - ); - - it.each` - onboardingRebaseCheckbox - ${false} - ${true} - `( - 'creates PR with footer and header with trailing and leading newlines' + - '(onboardingRebaseCheckbox="$onboardingRebaseCheckbox")', - async ({ onboardingRebaseCheckbox }) => { - config.onboardingRebaseCheckbox = onboardingRebaseCheckbox; - OnboardingState.prUpdateRequested = true; // case 'false' is tested in "breaks early when onboarding" - await ensureOnboardingPr( - { - ...config, - prHeader: '\r\r\nThis should not be the first line of the PR', - prFooter: - 'There should be several empty lines at the end of the PR\r\n\n\n', - }, - packageFiles, - branches, - ); - expect(platform.createPr).toHaveBeenCalledTimes(1); - expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot(); - }, - ); + it('returns if PR does not need updating', async () => { + OnboardingState.prUpdateRequested = true; // case 'false' is tested in "breaks early when onboarding" + platform.getBranchPr.mockResolvedValue( + partial({ + title: 'Configure Renovate', + bodyStruct, + }), + ); - it.each` - onboardingRebaseCheckbox - ${false} - ${true} - `( - 'creates PR with footer and header using templating' + - '(onboardingRebaseCheckbox="$onboardingRebaseCheckbox")', - async ({ onboardingRebaseCheckbox }) => { - config.baseBranch = 'some-branch'; - config.repository = 'test'; - config.onboardingRebaseCheckbox = onboardingRebaseCheckbox; - config.onboardingConfigFileName = undefined; // checks the case when fileName isn't available - OnboardingState.prUpdateRequested = true; // case 'false' is tested in "breaks early when onboarding" - await ensureOnboardingPr( - { - ...config, - prHeader: 'This is a header for platform:{{platform}}', - prFooter: - 'And this is a footer for repository:{{repository}} baseBranch:{{baseBranch}}', - }, - packageFiles, - branches, - ); - expect(platform.createPr).toHaveBeenCalledTimes(1); - expect(platform.createPr.mock.calls[0][0].prBody).toMatch( - /platform:github/, - ); - expect(platform.createPr.mock.calls[0][0].prBody).toMatch( - /repository:test/, - ); - expect(platform.createPr.mock.calls[0][0].prBody).toMatchSnapshot(); - }, - ); + await ensureOnboardingPr(config, packageFiles, branches); - it.each` - onboardingRebaseCheckbox - ${false} - ${true} - `( - 'returns if PR does not need updating' + - '(onboardingRebaseCheckbox="$onboardingRebaseCheckbox")', - async ({ onboardingRebaseCheckbox }) => { - const hash = - '30029ee05ed80b34d2f743afda6e78fe20247a1eedaa9ce6a8070045c229ebfa'; // no rebase checkbox PR hash - config.onboardingRebaseCheckbox = onboardingRebaseCheckbox; - OnboardingState.prUpdateRequested = true; // case 'false' is tested in "breaks early when onboarding" - platform.getBranchPr.mockResolvedValue( - partial({ - title: 'Configure Renovate', - bodyStruct: onboardingRebaseCheckbox ? bodyStruct : { hash }, - }), - ); - await ensureOnboardingPr(config, packageFiles, branches); - expect(platform.createPr).toHaveBeenCalledTimes(0); - expect(platform.updatePr).toHaveBeenCalledTimes(0); - }, - ); + expect(platform.createPr).toHaveBeenCalledTimes(0); + expect(platform.updatePr).toHaveBeenCalledTimes(0); + }); it('ensures comment, when PR is conflicted', async () => { config.baseBranch = 'some-branch'; @@ -223,7 +148,9 @@ describe('workers/repository/onboarding/pr/index', () => { }), ); scm.isBranchConflicted.mockResolvedValueOnce(true); + await ensureOnboardingPr(config, {}, branches); + expect(platform.ensureComment).toHaveBeenCalledTimes(1); expect(platform.createPr).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(0); @@ -237,29 +164,38 @@ describe('workers/repository/onboarding/pr/index', () => { bodyStruct, }), ); + prBody.getPrBody.mockReturnValueOnce({ + body: 'changed Body', + comments: [], + }); + await ensureOnboardingPr(config, {}, branches); + expect(platform.createPr).toHaveBeenCalledTimes(0); expect(platform.updatePr).toHaveBeenCalledTimes(1); }); it('creates PR (no require config)', async () => { config.requireConfig = 'optional'; + await ensureOnboardingPr(config, packageFiles, branches); + expect(platform.createPr).toHaveBeenCalledTimes(1); }); it('creates PR (require config)', async () => { config.requireConfig = 'required'; + await ensureOnboardingPr(config, packageFiles, branches); + expect(platform.createPr).toHaveBeenCalledTimes(1); }); it('dryrun of creates PR', async () => { GlobalConfig.set({ dryRun: 'full' }); + await ensureOnboardingPr(config, packageFiles, branches); - expect(logger.info).toHaveBeenCalledWith( - 'DRY-RUN: Would check branch renovate/configure', - ); + expect(logger.info).toHaveBeenLastCalledWith( 'DRY-RUN: Would create onboarding PR', ); @@ -273,15 +209,102 @@ describe('workers/repository/onboarding/pr/index', () => { bodyStruct, }), ); + prBody.getPrBody.mockReturnValueOnce({ + body: 'changed Body', + comments: [], + }); + await ensureOnboardingPr(config, packageFiles, branches); - expect(logger.info).toHaveBeenCalledWith( - 'DRY-RUN: Would check branch renovate/configure', - ); + expect(logger.info).toHaveBeenLastCalledWith( 'DRY-RUN: Would update onboarding PR', ); }); + it('creates comments if prBody contains comments', async () => { + prBody.getPrBody.mockReturnValueOnce({ + body: 'body', + comments: [ + { content: 'content', title: 'PR List' }, + { content: 'content', title: 'Package Files' }, + ], + }); + + await ensureOnboardingPr(config, packageFiles, branches); + + expect(platform.createPr).toHaveBeenCalledTimes(1); + expect(platform.ensureComment).toHaveBeenCalledWith({ + number: expect.anything(), + topic: 'PR List', + content: 'content', + }); + expect(platform.ensureComment).toHaveBeenCalledWith({ + number: expect.anything(), + topic: 'Package Files', + content: 'content', + }); + expect(platform.ensureCommentRemoval).not.toHaveBeenCalled(); + }); + + it('creates comments if prBody contains comments on existing Pr', async () => { + prBody.getPrBody.mockReturnValueOnce({ + body: 'body', + comments: [ + { content: 'content', title: 'PR List' }, + { content: 'content', title: 'Package Files' }, + ], + }); + platform.getBranchPr.mockResolvedValueOnce( + partial({ + title: 'Configure Renovate', + bodyStruct, + number: 1234, + }), + ); + + await ensureOnboardingPr(config, packageFiles, branches); + + expect(platform.ensureComment).toHaveBeenCalledWith({ + number: expect.anything(), + topic: 'PR List', + content: 'content', + }); + expect(platform.ensureComment).toHaveBeenCalledWith({ + number: expect.anything(), + topic: 'Package Files', + content: 'content', + }); + expect(platform.ensureCommentRemoval).not.toHaveBeenCalled(); + }); + + it('removes comments if prBody does not contains comments', async () => { + platform.getBranchPr.mockResolvedValueOnce( + partial({ + title: 'Configure Renovate', + bodyStruct, + number: 1234, + }), + ); + prBody.getPrBody.mockReturnValue({ + body: 'body', + comments: [], + }); + + await ensureOnboardingPr(config, packageFiles, branches); + + expect(platform.ensureComment).not.toHaveBeenCalled(); + expect(platform.ensureCommentRemoval).toHaveBeenCalledWith({ + number: expect.anything(), + type: 'by-topic', + topic: 'PR List', + }); + expect(platform.ensureCommentRemoval).toHaveBeenCalledWith({ + number: expect.anything(), + type: 'by-topic', + topic: 'Package Files', + }); + }); + describe('ensureOnboardingPr() throws', () => { const response = partial({ statusCode: 422 }); const err = partial({ response }); @@ -294,6 +317,7 @@ describe('workers/repository/onboarding/pr/index', () => { it('throws when trying to create a new PR', async () => { platform.createPr.mockRejectedValueOnce(err); + await expect( ensureOnboardingPr(config, packageFiles, branches), ).toReject(); @@ -305,6 +329,7 @@ describe('workers/repository/onboarding/pr/index', () => { errors: [{ message: 'A pull request already exists' }], }; platform.createPr.mockRejectedValueOnce(err); + await expect( ensureOnboardingPr(config, packageFiles, branches), ).toResolve(); diff --git a/lib/workers/repository/onboarding/pr/index.ts b/lib/workers/repository/onboarding/pr/index.ts index 3150d381324d9a..7a49437da7d225 100644 --- a/lib/workers/repository/onboarding/pr/index.ts +++ b/lib/workers/repository/onboarding/pr/index.ts @@ -1,4 +1,3 @@ -import is from '@sindresorhus/is'; import { GlobalConfig } from '../../../../config/global'; import type { RenovateConfig } from '../../../../config/types'; import { logger } from '../../../../logger'; @@ -10,13 +9,7 @@ import { scm } from '../../../../modules/platform/scm'; import { emojify } from '../../../../util/emoji'; import { getFile } from '../../../../util/git'; import { toSha256 } from '../../../../util/hash'; -import * as template from '../../../../util/template'; import type { BranchConfig } from '../../../types'; -import { - getDepWarningsOnboardingPR, - getErrors, - getWarnings, -} from '../../errors-warnings'; import { getPlatformPrOptions } from '../../update/pr'; import { prepareLabels } from '../../update/pr/labels'; import { addParticipants } from '../../update/pr/participants'; @@ -26,9 +19,7 @@ import { defaultConfigFile, getSemanticCommitPrTitle, } from '../common'; -import { getBaseBranchDesc } from './base-branch'; -import { getConfigDesc } from './config-description'; -import { getPrList } from './pr-list'; +import { getPrBody } from './body'; export async function ensureOnboardingPr( config: RenovateConfig, @@ -105,54 +96,41 @@ If you need any further assistance then you can also [request help here](${ `, ); prTemplate += rebaseCheckBox; - let prBody = prTemplate; - if (packageFiles && Object.entries(packageFiles).length) { - let files: string[] = []; - for (const [manager, managerFiles] of Object.entries(packageFiles)) { - files = files.concat( - managerFiles.map((file) => ` * \`${file.packageFile}\` (${manager})`), - ); - } - prBody = - prBody.replace( - '{{PACKAGE FILES}}', - '### Detected Package Files\n\n' + files.join('\n'), - ) + '\n'; - } else { - prBody = prBody.replace('{{PACKAGE FILES}}\n', ''); - } - let configDesc = ''; - if (GlobalConfig.get('dryRun')) { - // TODO: types (#22198) - logger.info(`DRY-RUN: Would check branch ${config.onboardingBranch!}`); - } else { - configDesc = getConfigDesc(config, packageFiles!); - } - prBody = prBody.replace('{{CONFIG}}\n', configDesc); - prBody = prBody.replace( - '{{WARNINGS}}\n', - getWarnings(config) + getDepWarningsOnboardingPR(packageFiles!, config), + const prBody = getPrBody( + prTemplate, + packageFiles, + config, + branches, + onboardingConfigHashComment, ); - prBody = prBody.replace('{{ERRORS}}\n', getErrors(config)); - prBody = prBody.replace('{{BASEBRANCH}}\n', getBaseBranchDesc(config)); - prBody = prBody.replace('{{PRLIST}}\n', getPrList(config, branches)); - if (is.string(config.prHeader)) { - prBody = `${template.compile(config.prHeader, config)}\n\n${prBody}`; - } - if (is.string(config.prFooter)) { - prBody = `${prBody}\n---\n\n${template.compile(config.prFooter, config)}\n`; - } - - prBody += onboardingConfigHashComment; - - logger.trace('prBody:\n' + prBody); - - prBody = platform.massageMarkdown(prBody); if (existingPr) { logger.debug('Found open onboarding PR'); + let topics: string[] = []; + + if (prBody.comments) { + topics = prBody.comments.map((x) => x.title); + for (const comment of prBody.comments) { + await platform.ensureComment({ + number: existingPr.number, + topic: comment.title, + content: comment.content, + }); + } + } + const topicsToDelete = ['PR List', 'Package Files'].filter( + (x) => !topics.includes(x), + ); + for (const topic of topicsToDelete) { + await platform.ensureCommentRemoval({ + number: existingPr.number, + type: 'by-topic', + topic, + }); + } + // Check if existing PR needs updating - const prBodyHash = hashBody(prBody); + const prBodyHash = hashBody(prBody.body); if (existingPr.bodyStruct?.hash === prBodyHash) { logger.debug(`Pull Request #${existingPr.number} does not need updating`); return; @@ -164,7 +142,7 @@ If you need any further assistance then you can also [request help here](${ await platform.updatePr({ number: existingPr.number, prTitle: existingPr.title, - prBody, + prBody: prBody.body, }); logger.info({ pr: existingPr.number }, 'Onboarding PR updated'); } @@ -185,13 +163,22 @@ If you need any further assistance then you can also [request help here](${ sourceBranch: config.onboardingBranch!, targetBranch: config.defaultBranch!, prTitle, - prBody, + prBody: prBody.body, labels, platformPrOptions: getPlatformPrOptions({ ...config, automerge: false, }), }); + if (pr && prBody.comments) { + for (const comment of prBody.comments) { + await platform.ensureComment({ + number: pr.number, + topic: comment.title, + content: comment.content, + }); + } + } logger.info( { pr: `Pull Request #${pr!.number}` }, 'Onboarding PR created', diff --git a/lib/workers/repository/update/branch/index.ts b/lib/workers/repository/update/branch/index.ts index 4f35e4ad3667e9..47c3faf219e97b 100644 --- a/lib/workers/repository/update/branch/index.ts +++ b/lib/workers/repository/update/branch/index.ts @@ -23,6 +23,7 @@ import { ensureCommentRemoval, } from '../../../../modules/platform/comment'; import { scm } from '../../../../modules/platform/scm'; +import { smartTruncate } from '../../../../modules/platform/utils/pr-body'; import { ExternalHostError } from '../../../../types/errors/external-host-error'; import { getElapsedMs } from '../../../../util/date'; import { emojify } from '../../../../util/emoji'; @@ -871,6 +872,7 @@ export async function processBranch( content += `\`\`\`\n${error.stderr!}\n\`\`\`\n\n`; }); content = platform.massageMarkdown(content); + content = smartTruncate(content, platform.maxBodyLength()); if ( !( config.suppressNotifications!.includes('artifactErrors') || diff --git a/lib/workers/repository/update/pr/body/index.spec.ts b/lib/workers/repository/update/pr/body/index.spec.ts index eb5149a2685a1f..318829586b96e7 100644 --- a/lib/workers/repository/update/pr/body/index.spec.ts +++ b/lib/workers/repository/update/pr/body/index.spec.ts @@ -51,6 +51,7 @@ describe('workers/repository/update/pr/body/index', () => { }); it('handles empty template', () => { + platform.maxBodyLength.mockReturnValueOnce(3.14); const res = getPrBody( { manager: 'some-manager', @@ -67,7 +68,7 @@ describe('workers/repository/update/pr/body/index', () => { }, {}, ); - expect(res).toBeEmptyString(); + expect(res).toStrictEqual({ body: '', comments: [] }); }); it('massages upgrades', () => { @@ -182,8 +183,8 @@ describe('workers/repository/update/pr/body/index', () => { }, {}, ); - expect(res).toContain('PR BODY'); - expect(res).toContain(`\n Some body`, - ); + comments: [], + }); config.labels = ['new_label']; const res = await ensurePr(config); @@ -483,6 +507,50 @@ describe('workers/repository/update/pr/index', () => { 'Pull Request #123 does not need updating', ); }); + + it('creates comments if prBody contains comments on existing Pr', async () => { + platform.getBranchPr.mockResolvedValueOnce(pr); + prBody.getPrBody.mockReturnValueOnce({ + body: 'body', + comments: [ + { content: 'content', title: 'Release Notes' }, + { content: 'content', title: 'Updates' }, + ], + }); + await ensurePr(config); + expect(platform.ensureComment).toHaveBeenCalledWith({ + number: expect.anything(), + topic: 'Release Notes', + content: 'content', + }); + expect(platform.ensureComment).toHaveBeenCalledWith({ + number: expect.anything(), + topic: 'Updates', + content: 'content', + }); + expect(platform.ensureCommentRemoval).not.toHaveBeenCalled(); + }); + + it('removes comments if prBody does not contains comments', async () => { + platform.getBranchPr.mockResolvedValueOnce(pr); + platform.createPr.mockResolvedValueOnce(pr); + prBody.getPrBody.mockReturnValueOnce({ + body: 'body', + comments: [], + }); + await ensurePr(config); + expect(platform.ensureComment).not.toHaveBeenCalled(); + expect(platform.ensureCommentRemoval).toHaveBeenCalledWith({ + number: expect.anything(), + type: 'by-topic', + topic: 'Release Notes', + }); + expect(platform.ensureCommentRemoval).toHaveBeenCalledWith({ + number: expect.anything(), + type: 'by-topic', + topic: 'Updates', + }); + }); }); describe('dry-run', () => { diff --git a/lib/workers/repository/update/pr/index.ts b/lib/workers/repository/update/pr/index.ts index f57b619c02acb1..b214d4aed7c4b1 100644 --- a/lib/workers/repository/update/pr/index.ts +++ b/lib/workers/repository/update/pr/index.ts @@ -21,6 +21,7 @@ import { hashBody, } from '../../../../modules/platform/pr-body'; import { scm } from '../../../../modules/platform/scm'; +import { smartTruncate } from '../../../../modules/platform/utils/pr-body'; import { ExternalHostError } from '../../../../types/errors/external-host-error'; import { getElapsedHours } from '../../../../util/date'; import { stripEmojis } from '../../../../util/emoji'; @@ -356,7 +357,7 @@ export async function ensurePr( const existingPrTitle = stripEmojis(existingPr.title); const existingPrBodyHash = existingPr.bodyStruct?.hash; const newPrTitle = stripEmojis(prTitle); - const newPrBodyHash = hashBody(prBody); + const newPrBodyHash = hashBody(prBody.body); const prInitialLabels = existingPr.bodyStruct?.debugData?.labels; const prCurrentLabels = existingPr.labels; @@ -367,7 +368,28 @@ export async function ensurePr( prCurrentLabels, configuredLabels, ); - + let topics: string[] = []; + + if (prBody.comments) { + topics = prBody.comments.map((x) => x.title); + for (const comment of prBody.comments) { + await platform.ensureComment({ + number: existingPr.number, + topic: comment.title, + content: comment.content, + }); + } + } + const topicsToDelete = ['Release Notes', 'Updates'].filter( + (x) => !topics.includes(x), + ); + for (const topic of topicsToDelete) { + await platform.ensureCommentRemoval({ + number: existingPr.number, + type: 'by-topic', + topic, + }); + } if ( existingPr?.targetBranch === config.baseBranch && existingPrTitle === newPrTitle && @@ -385,7 +407,7 @@ export async function ensurePr( const updatePrConfig: UpdatePrConfig = { number: existingPr.number, prTitle, - prBody, + prBody: prBody.body, platformPrOptions: getPlatformPrOptions(config), }; // PR must need updating @@ -460,7 +482,7 @@ export async function ensurePr( type: 'with-pr', pr: { ...existingPr, - bodyStruct: getPrBodyStruct(prBody), + bodyStruct: getPrBodyStruct(prBody.body), title: prTitle, targetBranch: config.baseBranch, }, @@ -488,12 +510,21 @@ export async function ensurePr( sourceBranch: branchName, targetBranch: config.baseBranch, prTitle, - prBody, + prBody: prBody.body, labels: prepareLabels(config), platformPrOptions: getPlatformPrOptions(config), draftPR: !!config.draftPR, milestone: config.milestone, }); + if (pr && prBody.comments) { + for (const comment of prBody.comments) { + await platform.ensureComment({ + number: pr.number, + topic: comment.title, + content: comment.content, + }); + } + } incLimitedValue('PullRequests'); logger.info({ pr: pr?.number, prTitle }, 'PR created'); @@ -531,6 +562,7 @@ export async function ensurePr( content += '\n___\n * Branch has one or more failed status checks'; } content = platform.massageMarkdown(content); + content = smartTruncate(content, platform.maxBodyLength()); logger.debug('Adding branch automerge failure message to PR'); if (GlobalConfig.get('dryRun')) { logger.info(`DRY-RUN: Would add comment to PR #${pr.number}`);