Skip to content
26 changes: 26 additions & 0 deletions docs/usage/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -1772,6 +1772,32 @@ Renovate can fetch changelogs when they are hosted on one of these platforms:

If you are running on any platform except `github.com`, you need to [configure a Personal Access Token](./getting-started/running.md#githubcom-token-for-changelogs-and-tools) to allow Renovate to fetch changelogs notes from `github.com`.

You may use [package rules](#packagerules) to override the value of `fetchChangeLogs` for matching depeendencies, with later rules overriding earlier ones.

The following re-enables fetching of changelogs when creating pull-requests for lodash updates.

```json
{
"fetchChangeLogs": "off",
"packageRules": {
"matchSourceUrls": ["https://github.com/lodash/lodash"],
"fetchChangeLogs": "pr"
}
}
```

The following disables fetching of changelogs for any package in aws-sdk-go-v2,
which can be time-consuming due to the repository's large number of tags:

```json
{
"packageRules": {
"matchSourceUrls": ["https://github.com/aws/aws-sdk-go-v2{/**,}"],
"fetchChangeLogs": "off"
}
}
```

<!-- prettier-ignore -->
!!! note
Renovate can only show changelogs from some platforms and some package managers.
Expand Down
6 changes: 5 additions & 1 deletion docs/usage/key-concepts/changelogs.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ For more details, see [Private packages, looking up changelogs](../getting-start

### [`fetchChangeLogs`](../configuration-options.md#fetchchangelogs)

Top-level configuration that controls when to fetch changelogs for addition to pull-request and branch commit-messages.
If you want more finely-grained control over changelog fetching (disabling for specific packages, source repositories, etc),
use [packageRules](../configuration-options.md#packagerules) to override the `fetchChangeLogs` value for matching packages.

Set to `off` if changelog fetching is causing a problem.

Set to `branch` if you have an advanced use case where you're embedding changelogs in the Git commit itself, we don't recommend this due to its potential size.
Expand All @@ -40,7 +44,7 @@ This doesn't help with _fetching_ the changelogs, but if you configure it then R

## Platforms that Renovate can fetch changelogs from

See the list of platforms in the [`fetchChangelogs` config option docs](../configuration-options.md#fetchchangelogs).
See the list of platforms in the [`fetchChangeLogs` config option docs](../configuration-options.md#fetchchangelogs).

### Running Renovate on a non-GitHub platform

Expand Down
1 change: 1 addition & 0 deletions lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3183,6 +3183,7 @@ const options: Readonly<RenovateOptions>[] = [
allowedValues: ['off', 'branch', 'pr'],
default: 'pr',
cli: false,
parents: ['.', 'packageRules'],
},
{
name: 'cloneSubmodules',
Expand Down
13 changes: 13 additions & 0 deletions lib/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,16 @@ type Val = NonNullable<unknown>;
export type Nullish<T extends Val> = T | null | undefined;

export type MaybePromise<T> = T | Promise<T>;

/**
* The Extends utility type ensures that U is a subset of T (typically a union).
* This helps ensure that we get a typescript error should anything ever be removed
* from T but still remain in U.
*
* @example
* type MyType = Extends<AutoMergeType, 'pr' | 'branch'>; // works
*
* @example
* type IsBroken = Extends<AutoMergeType, 'pr' | 'branch' | 'oh-no' >; // This will give a typescript error
*/
export type Extends<T, U extends T> = U;
30 changes: 30 additions & 0 deletions lib/util/package-rules/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,36 @@ describe('util/package-rules/index', () => {
expect(res.packageName).toBe('docker.io/library/node');
});

it('propagates fetchChangeLogs from matching packageRule', async () => {
const config: TestConfig = {
datasource: 'npm',
depName: 'some-dep',
packageRules: [
{
matchDatasources: ['npm'],
fetchChangeLogs: 'off',
},
],
};
const res = await applyPackageRules(config);
expect(res.fetchChangeLogs).toBe('off');
});

it('does not set fetchChangeLogs when packageRule does not match', async () => {
const config: TestConfig = {
datasource: 'npm',
depName: 'some-dep',
packageRules: [
{
matchDatasources: ['pypi'],
fetchChangeLogs: 'off',
},
],
};
const res = await applyPackageRules(config);
expect(res.fetchChangeLogs).toBeUndefined();
});

it('compiles sourceUrl with template helper functions', async () => {
const config: TestConfig = {
datasource: 'terraform-provider',
Expand Down
154 changes: 153 additions & 1 deletion lib/workers/repository/changelog/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { partial } from '~test/util.ts';
import type { FetchChangeLogsOptions } from '../../../config/types.ts';
import type { BranchUpgradeConfig } from '../../types.ts';
import { getChangeLogJSON } from '../update/pr/changelog/index.ts';
import { embedChangelogs } from './index.ts';
import type { SupportedChangelogStages } from './types.ts';

vi.mock('../update/pr/changelog/index.ts');

Expand All @@ -21,7 +23,13 @@ describe('workers/repository/changelog/index', () => {
partial<BranchUpgradeConfig>(),
partial<BranchUpgradeConfig>({ changelogContent: 'testContent' }),
];
await expect(embedChangelogs(branches)).toResolve();
await expect(
embedChangelogs({
upgrades: branches,
stage: 'pr',
fetchChangeLogs: 'pr',
}),
).toResolve();
expect(branches).toEqual([
{ logJSON: null },
{ logJSON: { hasReleaseNotes: true } },
Expand All @@ -42,4 +50,148 @@ describe('workers/repository/changelog/index', () => {
},
]);
});

interface FetchChangeLogsTestCase {
fetchChangeLogs?: FetchChangeLogsOptions;
stage: SupportedChangelogStages;
expectations: () => void;
}

const testCases: FetchChangeLogsTestCase[] = [
{
stage: 'pr',
fetchChangeLogs: undefined, // should default to 'pr'
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(2);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({
groupName: 'fetchChangeLogs is undefined',
}),
);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is pr' }),
);
},
},
{
stage: 'pr',
fetchChangeLogs: 'pr',
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(2);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({
groupName: 'fetchChangeLogs is undefined',
}),
);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is pr' }),
);
},
},
{
stage: 'pr',
fetchChangeLogs: 'branch',
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(1);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is pr' }),
);
},
},
{
stage: 'pr',
fetchChangeLogs: 'off',
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(1);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is pr' }),
);
},
},
{
stage: 'branch',
fetchChangeLogs: undefined, // should default to 'pr'
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(1);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is branch' }),
);
},
},
{
stage: 'branch',
fetchChangeLogs: 'branch',
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(2);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({
groupName: 'fetchChangeLogs is undefined',
}),
);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is branch' }),
);
},
},
{
stage: 'branch',
fetchChangeLogs: 'pr',
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(1);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is branch' }),
);
},
},
{
stage: 'branch',
fetchChangeLogs: 'off',
expectations: () => {
expect(getChangeLogJSON).toHaveBeenCalledTimes(1);
expect(getChangeLogJSON).toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is branch' }),
);
},
},
];

test.each(testCases)(
'stage is "$stage", top-level fetchChangeLogs is "$fetchChangeLogs"',
async ({ fetchChangeLogs, stage, expectations }) => {
vi.mocked(getChangeLogJSON).mockResolvedValue({
hasReleaseNotes: true,
});
const branches = [
partial<BranchUpgradeConfig>({
groupName: 'fetchChangeLogs is undefined',
}),
partial<BranchUpgradeConfig>({
groupName: 'fetchChangeLogs is pr',
fetchChangeLogs: 'pr',
}),
partial<BranchUpgradeConfig>({
groupName: 'fetchChangeLogs is branch',
fetchChangeLogs: 'branch',
}),
partial<BranchUpgradeConfig>({
groupName: 'fetchChangeLogs is off',
fetchChangeLogs: 'off',
}),
];
await expect(
embedChangelogs({
upgrades: branches,
stage,
fetchChangeLogs,
}),
).toResolve();

expectations();

// When fetchChangeLogs is explicitly set to 'off', no changelogs should be fetched.
expect(getChangeLogJSON).not.toHaveBeenCalledWith(
expect.objectContaining({ groupName: 'fetchChangeLogs is off' }),
);
},
);
});
32 changes: 28 additions & 4 deletions lib/workers/repository/changelog/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { FetchChangeLogsOptions } from '../../../config/types.ts';
import * as p from '../../../util/promises.ts';
import type { BranchUpgradeConfig } from '../../types.ts';
import { getChangeLogJSON } from '../update/pr/changelog/index.ts';
import type { EmbedChangelogsOptions } from './types.ts';

export async function embedChangelog(
upgrade: BranchUpgradeConfig,
Expand Down Expand Up @@ -43,8 +45,30 @@ export async function embedChangelog(
}
}

export async function embedChangelogs(
branches: BranchUpgradeConfig[],
): Promise<void> {
await p.map(branches, embedChangelog, { concurrency: 10 });
// Merges the top-level fetchChangeLogs value with the upgrade's fetchChangeLogs value (prioritizing the latter, if defined).
function resolveFetchChangeLogs(
fetchChangeLogs?: FetchChangeLogsOptions,
upgradeFetchChangeLogs?: FetchChangeLogsOptions,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think this is always set on upgrades object 🤔

please create another reproduction:

  1. set fetchChangeLogs=off globally
  2. set fetchChangeLogsPr for a single dep
  3. group deps to single PR
  4. show us the link 😁

): FetchChangeLogsOptions {
// Default to 'pr' if top-level fetchChangeLogs is undefined.
let fetchChangeLogsResolved: FetchChangeLogsOptions = fetchChangeLogs ?? 'pr';
if (upgradeFetchChangeLogs) {
// Override using the upgrade's fetchChangeLogs value.
fetchChangeLogsResolved = upgradeFetchChangeLogs;
}
return fetchChangeLogsResolved;
}

export async function embedChangelogs({
upgrades,
stage,
fetchChangeLogs,
}: EmbedChangelogsOptions): Promise<void> {
// Filter down to branch upgrades that match the stage and fetchChangeLogs configuration.
const filteredUpgrades = upgrades.filter(
(upgrade) =>
resolveFetchChangeLogs(fetchChangeLogs, upgrade.fetchChangeLogs) ===
stage,
);
await p.map(filteredUpgrades, embedChangelog, { concurrency: 10 });
}
14 changes: 14 additions & 0 deletions lib/workers/repository/changelog/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { FetchChangeLogsOptions } from '../../../config/types.ts';
import type { Extends } from '../../../types/index.ts';
import type { BranchUpgradeConfig } from '../../types.ts';

export type SupportedChangelogStages = Extends<
FetchChangeLogsOptions,
'branch' | 'pr'
>;

export interface EmbedChangelogsOptions {
upgrades: BranchUpgradeConfig[];
stage: SupportedChangelogStages;
fetchChangeLogs?: FetchChangeLogsOptions;
}
22 changes: 22 additions & 0 deletions lib/workers/repository/update/branch/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
BranchUpgradeConfig,
CacheFingerprintMatchResult,
} from '../../../types.ts';
import * as _changelog from '../../changelog/index.ts';
import * as _prAutomerge from '../pr/automerge.ts';
import type { ResultWithPr } from '../pr/index.ts';
import * as _prWorker from '../pr/index.ts';
Expand Down Expand Up @@ -79,6 +80,7 @@ const exec = vi.mocked(_exec);
const sanitize = vi.mocked(_sanitize);
const limits = vi.mocked(_limits);
const repoCache = vi.mocked(_repoCache);
const changelog = vi.mocked(_changelog);

const adminConfig: RepoGlobalConfig = { localDir: '', cacheDir: '' };

Expand Down Expand Up @@ -1223,6 +1225,26 @@ describe('workers/repository/update/branch/index', () => {
});
});

it('fetches changelogs for the "branch" stage', async () => {
getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce(
partial<PackageFilesResult>({
updatedPackageFiles: [partial<FileChange>()],
}),
);
npmPostExtract.getAdditionalFiles.mockResolvedValueOnce({
artifactErrors: [],
updatedArtifacts: [partial<FileChange>()],
});

await branchWorker.processBranch({ ...config });

expect(changelog.embedChangelogs).toHaveBeenCalledExactlyOnceWith({
upgrades: config.upgrades,
stage: 'branch',
fetchChangeLogs: config.fetchChangeLogs,
});
});

it('ensures PR and tries automerge', async () => {
getUpdated.getUpdatedPackageFiles.mockResolvedValueOnce(
partial<PackageFilesResult>({
Expand Down
Loading
Loading