feat(release-notes)!: support configurable fetching stage (#22781)

Changes fetchReleaseNotes from boolean to enum, with values off, branch, pr.

Closes #20476

BREAKING CHANGE: Release notes won't be fetched early for commitBody insertion unless explicitly configured with fetchReleaseNotes=branch
This commit is contained in:
RahulGautamSingh 2023-06-18 13:58:25 +05:45 committed by Rhys Arkins
parent aa14b777c0
commit c2d3ca856f
14 changed files with 66 additions and 114 deletions

View file

@ -931,7 +931,14 @@ A similar one could strip leading `v` prefixes:
## fetchReleaseNotes ## fetchReleaseNotes
Set this to `false` if you want to disable release notes fetching. Use this config option to configure release notes fetching.
The available options are:
- `off` - disable release notes fetching
- `branch` - fetch release notes while creating/updating branch
- `pr`(default) - fetches release notes while creating/updating pull-request
It is not recommended to set fetchReleaseNotes=branch unless you are embedding release notes in commit information, because it results in a performance decrease.
Renovate can fetch release notes when they are hosted on one of these platforms: Renovate can fetch release notes when they are hosted on one of these platforms:

View file

@ -0,0 +1,22 @@
import { FetchReleaseNotesMigration } from './fetch-release-notes-migration';
describe('config/migrations/custom/fetch-release-notes-migration', () => {
it('migrates', () => {
expect(FetchReleaseNotesMigration).toMigrate(
{
fetchReleaseNotes: false as never,
},
{
fetchReleaseNotes: 'off',
}
);
expect(FetchReleaseNotesMigration).toMigrate(
{
fetchReleaseNotes: true as never,
},
{
fetchReleaseNotes: 'pr',
}
);
});
});

View file

@ -0,0 +1,12 @@
import is from '@sindresorhus/is';
import { AbstractMigration } from '../base/abstract-migration';
export class FetchReleaseNotesMigration extends AbstractMigration {
override readonly propertyName = 'fetchReleaseNotes';
override run(value: unknown): void {
if (is.boolean(value)) {
this.rewrite(value ? 'pr' : 'off');
}
}
}

View file

@ -20,6 +20,7 @@ import { DepTypesMigration } from './custom/dep-types-migration';
import { DryRunMigration } from './custom/dry-run-migration'; import { DryRunMigration } from './custom/dry-run-migration';
import { EnabledManagersMigration } from './custom/enabled-managers-migration'; import { EnabledManagersMigration } from './custom/enabled-managers-migration';
import { ExtendsMigration } from './custom/extends-migration'; import { ExtendsMigration } from './custom/extends-migration';
import { FetchReleaseNotesMigration } from './custom/fetch-release-notes-migration';
import { GoModTidyMigration } from './custom/go-mod-tidy-migration'; import { GoModTidyMigration } from './custom/go-mod-tidy-migration';
import { HostRulesMigration } from './custom/host-rules-migration'; import { HostRulesMigration } from './custom/host-rules-migration';
import { IgnoreNodeModulesMigration } from './custom/ignore-node-modules-migration'; import { IgnoreNodeModulesMigration } from './custom/ignore-node-modules-migration';
@ -148,6 +149,7 @@ export class MigrationsService {
DatasourceMigration, DatasourceMigration,
RecreateClosedMigration, RecreateClosedMigration,
StabilityDaysMigration, StabilityDaysMigration,
FetchReleaseNotesMigration,
]; ];
static run(originalConfig: RenovateConfig): RenovateConfig { static run(originalConfig: RenovateConfig): RenovateConfig {

View file

@ -2567,9 +2567,10 @@ const options: RenovateOptions[] = [
}, },
{ {
name: 'fetchReleaseNotes', name: 'fetchReleaseNotes',
description: 'Controls if release notes are fetched.', description: 'Controls if and when release notes are fetched.',
type: 'boolean', type: 'string',
default: true, allowedValues: ['off', 'branch', 'pr'],
default: 'pr',
cli: false, cli: false,
env: false, env: false,
}, },

View file

@ -261,7 +261,7 @@ export interface RenovateConfig
vulnerabilitySeverity?: string; vulnerabilitySeverity?: string;
regexManagers?: RegExManager[]; regexManagers?: RegExManager[];
fetchReleaseNotes?: boolean; fetchReleaseNotes?: FetchReleaseNotesOptions;
secrets?: Record<string, string>; secrets?: Record<string, string>;
constraints?: Record<string, string>; constraints?: Record<string, string>;
@ -302,6 +302,8 @@ export type UpdateType =
| 'bump' | 'bump'
| 'replacement'; | 'replacement';
export type FetchReleaseNotesOptions = 'off' | 'branch' | 'pr';
export type MatchStringsStrategy = 'any' | 'recursive' | 'combination'; export type MatchStringsStrategy = 'any' | 'recursive' | 'combination';
export type MergeStrategy = export type MergeStrategy =

View file

@ -1,7 +1,7 @@
import { mockedFunction, partial } from '../../../../test/util'; import { mockedFunction, partial } from '../../../../test/util';
import type { BranchUpgradeConfig } from '../../types'; import type { BranchUpgradeConfig } from '../../types';
import { getChangeLogJSON } from '../update/pr/changelog'; import { getChangeLogJSON } from '../update/pr/changelog';
import { embedChangelogs, needsChangelogs } from '.'; import { embedChangelogs } from '.';
jest.mock('../update/pr/changelog'); jest.mock('../update/pr/changelog');
@ -27,23 +27,4 @@ describe('workers/repository/changelog/index', () => {
{ logJSON: null }, { logJSON: null },
]); ]);
}); });
it('needsChangelogs', () => {
expect(needsChangelogs(partial<BranchUpgradeConfig>())).toBeFalse();
expect(
needsChangelogs(
partial<BranchUpgradeConfig>({
commitBody: '{{#if logJSON.hasReleaseNotes}}has changelog{{/if}}',
})
)
).toBeFalse();
expect(
needsChangelogs(
partial<BranchUpgradeConfig>({
commitBody: '{{#if logJSON.hasReleaseNotes}}has changelog{{/if}}',
}),
['commitBody']
)
).toBeTrue();
});
}); });

View file

@ -1,8 +1,4 @@
import * as p from '../../../util/promises'; import * as p from '../../../util/promises';
import {
containsTemplates,
exposedConfigOptions,
} from '../../../util/template';
import type { BranchUpgradeConfig } from '../../types'; import type { BranchUpgradeConfig } from '../../types';
import { getChangeLogJSON } from '../update/pr/changelog'; import { getChangeLogJSON } from '../update/pr/changelog';
@ -21,17 +17,3 @@ export async function embedChangelogs(
): Promise<void> { ): Promise<void> {
await p.map(branches, embedChangelog, { concurrency: 10 }); await p.map(branches, embedChangelog, { concurrency: 10 });
} }
export function needsChangelogs(
upgrade: BranchUpgradeConfig,
fields = exposedConfigOptions.filter((o) => o !== 'commitBody')
): boolean {
// commitBody is now compiled when commit is done
for (const field of fields) {
// fields set by `getChangeLogJSON`
if (containsTemplates(upgrade[field], ['logJSON', 'releases'])) {
return true;
}
}
return false;
}

View file

@ -2,7 +2,6 @@ import {
fs, fs,
git, git,
mocked, mocked,
mockedFunction,
partial, partial,
platform, platform,
scm, scm,
@ -34,7 +33,6 @@ import * as _mergeConfidence from '../../../../util/merge-confidence';
import * as _sanitize from '../../../../util/sanitize'; import * as _sanitize from '../../../../util/sanitize';
import * as _limits from '../../../global/limits'; import * as _limits from '../../../global/limits';
import type { BranchConfig, BranchUpgradeConfig } from '../../../types'; import type { BranchConfig, BranchUpgradeConfig } from '../../../types';
import { needsChangelogs } from '../../changelog';
import type { ResultWithPr } from '../pr'; import type { ResultWithPr } from '../pr';
import * as _prWorker from '../pr'; import * as _prWorker from '../pr';
import * as _prAutomerge from '../pr/automerge'; import * as _prAutomerge from '../pr/automerge';
@ -816,9 +814,8 @@ describe('workers/repository/update/branch/index', () => {
ignoreTests: true, ignoreTests: true,
prCreation: 'not-pending', prCreation: 'not-pending',
commitBody: '[skip-ci]', commitBody: '[skip-ci]',
fetchReleaseNotes: true, fetchReleaseNotes: 'branch',
} satisfies BranchConfig; } satisfies BranchConfig;
mockedFunction(needsChangelogs).mockReturnValueOnce(true);
scm.getBranchCommit.mockResolvedValue('123test'); //TODO:not needed? scm.getBranchCommit.mockResolvedValue('123test'); //TODO:not needed?
expect(await branchWorker.processBranch(inconfig)).toEqual({ expect(await branchWorker.processBranch(inconfig)).toEqual({
branchExists: true, branchExists: true,

View file

@ -34,7 +34,7 @@ import { toMs } from '../../../../util/pretty-time';
import * as template from '../../../../util/template'; import * as template from '../../../../util/template';
import { isLimitReached } from '../../../global/limits'; import { isLimitReached } from '../../../global/limits';
import type { BranchConfig, BranchResult, PrBlockedBy } from '../../../types'; import type { BranchConfig, BranchResult, PrBlockedBy } from '../../../types';
import { embedChangelog, needsChangelogs } from '../../changelog'; import { embedChangelogs } from '../../changelog';
import { ensurePr } from '../pr'; import { ensurePr } from '../pr';
import { checkAutoMerge } from '../pr/automerge'; import { checkAutoMerge } from '../pr/automerge';
import { setArtifactErrorStatus } from './artifacts'; import { setArtifactErrorStatus } from './artifacts';
@ -482,6 +482,10 @@ export async function processBranch(
} else { } else {
logger.debug('No updated lock files in branch'); logger.debug('No updated lock files in branch');
} }
if (config.fetchReleaseNotes === 'branch') {
await embedChangelogs(config.upgrades);
}
const postUpgradeCommandResults = await executePostUpgradeCommands( const postUpgradeCommandResults = await executePostUpgradeCommands(
config config
); );
@ -540,14 +544,6 @@ export async function processBranch(
// compile commit message with body, which maybe needs changelogs // compile commit message with body, which maybe needs changelogs
if (config.commitBody) { if (config.commitBody) {
if (
config.fetchReleaseNotes &&
needsChangelogs(config, ['commitBody'])
) {
// we only need first upgrade, the others are only needed on PR update
// we add it to first, so PR fetch can skip fetching for that update
await embedChangelog(config.upgrades[0]);
}
// changelog is on first upgrade // changelog is on first upgrade
config.commitMessage = `${config.commitMessage!}\n\n${template.compile( config.commitMessage = `${config.commitMessage!}\n\n${template.compile(
config.commitBody, config.commitBody,

View file

@ -102,7 +102,7 @@ describe('workers/repository/update/pr/index', () => {
platform.createPr.mockResolvedValueOnce(pr); platform.createPr.mockResolvedValueOnce(pr);
limits.isLimitReached.mockReturnValueOnce(true); limits.isLimitReached.mockReturnValueOnce(true);
config.fetchReleaseNotes = true; config.fetchReleaseNotes = 'pr';
const res = await ensurePr(config); const res = await ensurePr(config);
@ -871,13 +871,13 @@ describe('workers/repository/update/pr/index', () => {
bodyFingerprint: fingerprint( bodyFingerprint: fingerprint(
generatePrBodyFingerprintConfig({ generatePrBodyFingerprintConfig({
...config, ...config,
fetchReleaseNotes: true, fetchReleaseNotes: 'pr',
}) })
), ),
lastEdited: new Date('2020-01-20T00:00:00Z').toISOString(), lastEdited: new Date('2020-01-20T00:00:00Z').toISOString(),
}; };
prCache.getPrCache.mockReturnValueOnce(cachedPr); prCache.getPrCache.mockReturnValueOnce(cachedPr);
const res = await ensurePr({ ...config, fetchReleaseNotes: true }); const res = await ensurePr({ ...config, fetchReleaseNotes: 'pr' });
expect(res).toEqual({ expect(res).toEqual({
type: 'with-pr', type: 'with-pr',
pr: existingPr, pr: existingPr,
@ -904,13 +904,13 @@ describe('workers/repository/update/pr/index', () => {
bodyFingerprint: fingerprint( bodyFingerprint: fingerprint(
generatePrBodyFingerprintConfig({ generatePrBodyFingerprintConfig({
...config, ...config,
fetchReleaseNotes: true, fetchReleaseNotes: 'pr',
}) })
), ),
lastEdited: new Date('2020-01-20T00:00:00Z').toISOString(), lastEdited: new Date('2020-01-20T00:00:00Z').toISOString(),
}; };
prCache.getPrCache.mockReturnValueOnce(cachedPr); prCache.getPrCache.mockReturnValueOnce(cachedPr);
const res = await ensurePr({ ...config, fetchReleaseNotes: true }); const res = await ensurePr({ ...config, fetchReleaseNotes: 'pr' });
expect(res).toEqual({ expect(res).toEqual({
type: 'with-pr', type: 'with-pr',
pr: { pr: {

View file

@ -234,7 +234,7 @@ export async function ensurePr(
}`; }`;
} }
if (config.fetchReleaseNotes) { if (config.fetchReleaseNotes === 'pr') {
// fetch changelogs when not already done; // fetch changelogs when not already done;
await embedChangelogs(upgrades); await embedChangelogs(upgrades);
} }

View file

@ -1,4 +1,4 @@
import { RenovateConfig, mocked, mockedFunction } from '../../../../test/util'; import { RenovateConfig, mocked } from '../../../../test/util';
import { getConfig } from '../../../config/defaults'; import { getConfig } from '../../../config/defaults';
import * as _changelog from '../changelog'; import * as _changelog from '../changelog';
import { branchifyUpgrades } from './branchify'; import { branchifyUpgrades } from './branchify';
@ -124,7 +124,7 @@ describe('workers/repository/updates/branchify', () => {
}); });
it('no fetch changelogs', async () => { it('no fetch changelogs', async () => {
config.fetchReleaseNotes = false; config.fetchReleaseNotes = 'off';
flattenUpdates.mockResolvedValueOnce([ flattenUpdates.mockResolvedValueOnce([
{ {
depName: 'foo', depName: 'foo',
@ -153,38 +153,5 @@ describe('workers/repository/updates/branchify', () => {
expect(embedChangelogs).not.toHaveBeenCalled(); expect(embedChangelogs).not.toHaveBeenCalled();
expect(Object.keys(res.branches)).toHaveLength(2); expect(Object.keys(res.branches)).toHaveLength(2);
}); });
it('fetch changelogs if required', async () => {
config.fetchReleaseNotes = true;
config.repoIsOnboarded = true;
mockedFunction(_changelog.needsChangelogs).mockReturnValueOnce(true);
flattenUpdates.mockResolvedValueOnce([
{
depName: 'foo',
branchName: 'foo',
prTitle: 'some-title',
version: '1.1.0',
groupName: 'My Group',
group: { branchName: 'renovate/{{groupSlug}}' },
},
{
depName: 'foo',
branchName: 'foo',
prTitle: 'some-title',
version: '2.0.0',
},
{
depName: 'bar',
branchName: 'bar-{{version}}',
prTitle: 'some-title',
version: '1.1.0',
groupName: 'My Group',
group: { branchName: 'renovate/my-group' },
},
]);
const res = await branchifyUpgrades(config, {});
expect(embedChangelogs).toHaveBeenCalledOnce();
expect(Object.keys(res.branches)).toHaveLength(2);
});
}); });
}); });

View file

@ -3,7 +3,6 @@ import type { Merge } from 'type-fest';
import type { RenovateConfig, ValidationMessage } from '../../../config/types'; import type { RenovateConfig, ValidationMessage } from '../../../config/types';
import { addMeta, logger, removeMeta } from '../../../logger'; import { addMeta, logger, removeMeta } from '../../../logger';
import type { BranchConfig, BranchUpgradeConfig } from '../../types'; import type { BranchConfig, BranchUpgradeConfig } from '../../types';
import { embedChangelogs, needsChangelogs } from '../changelog';
import { flattenUpdates } from './flatten'; import { flattenUpdates } from './flatten';
import { generateBranchConfig } from './generate'; import { generateBranchConfig } from './generate';
@ -72,22 +71,6 @@ export async function branchifyUpgrades(
}) })
.reverse(); .reverse();
if (config.fetchReleaseNotes && config.repoIsOnboarded) {
const branches = branchUpgrades[branchName].filter((upg) =>
needsChangelogs(upg)
);
if (branches.length) {
logger.warn(
{
branches: branches.map((b) => b.branchName),
docs: 'https://docs.renovatebot.com/templates/',
},
'Fetching changelogs early is deprecated. Remove `logJSON` and `releases` from config templates. They are only allowed in `commitBody` template. See template docs for allowed templates'
);
await embedChangelogs(branches);
}
}
const branch = generateBranchConfig(branchUpgrades[branchName]); const branch = generateBranchConfig(branchUpgrades[branchName]);
branch.branchName = branchName; branch.branchName = branchName;
branch.packageFiles = packageFiles; branch.packageFiles = packageFiles;