Skip to content

Commit 5de27f7

Browse files
RahulGautamSinghviceicesecustorrarkinsonedr0p
authored
feat(config): minimumGroupSize (renovatebot#37242)
Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com> Co-authored-by: Rhys Arkins <rhys@arkins.net> Co-authored-by: Devin Buhl <onedr0p@users.noreply.github.com>
1 parent 25f9c1c commit 5de27f7

File tree

11 files changed

+187
-3
lines changed

11 files changed

+187
-3
lines changed

docs/usage/configuration-options.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,6 +2660,30 @@ Renovate will only add a milestone when it _creates_ the PR.
26602660
}
26612661
```
26622662

2663+
## minimumGroupSize
2664+
2665+
If set to to a positive value x then branch creation will be postponed until x or more updates are available in the branch.
2666+
2667+
This applies to both these scenarios:
2668+
2669+
- Grouped updates with more than one dependency updated together, and
2670+
- Branches with multiple updates of the same dependency (e.g. in multiple files)
2671+
2672+
Example:
2673+
2674+
```json title="Create only a grouped update when there are 3 or more node updates"
2675+
{
2676+
"packageRules": [
2677+
{
2678+
"description": "We need to update Node in two places - always wait until both upgrades are available",
2679+
"matchDepNames": ["node"],
2680+
"groupName": "Node.js",
2681+
"minimumGroupSize": 3
2682+
}
2683+
]
2684+
}
2685+
```
2686+
26632687
## minimumReleaseAge
26642688

26652689
This feature used to be called `stabilityDays`.

lib/config/options/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,13 @@ const options: RenovateOptions[] = [
350350
type: 'string',
351351
},
352352
},
353+
{
354+
name: 'minimumGroupSize',
355+
description:
356+
'The minimum number of updates which must be in a group for branches to be created.',
357+
type: 'integer',
358+
default: 1,
359+
},
353360
{
354361
name: 'presetCachePersistence',
355362
description: 'Cache resolved presets in package cache.',

lib/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,7 @@ export interface RenovateConfig
323323
branchTopic?: string;
324324
additionalBranchPrefix?: string;
325325
sharedVariableName?: string;
326+
minimumGroupSize?: number;
326327
}
327328

328329
const CustomDatasourceFormats = [

lib/workers/repository/dependency-dashboard.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,23 @@ describe('workers/repository/dependency-dashboard', () => {
283283
dependencyDashboardRebaseAllOpen: false,
284284
});
285285
});
286+
287+
it('reads dashboard body and group size not met branches', async () => {
288+
const conf: RenovateConfig = {};
289+
conf.prCreation = 'approval';
290+
platform.findIssue.mockResolvedValueOnce({
291+
title: '',
292+
number: 1,
293+
body: `
294+
- [x] <!-- approveGroup-branch=groupedBranch1 -->
295+
- [ ] <!-- approveGroup-branch=groupedBranch2 -->
296+
`,
297+
});
298+
await dependencyDashboard.readDashboardBody(conf);
299+
expect(conf.dependencyDashboardChecks).toMatchObject({
300+
groupedBranch1: 'approveGroup',
301+
});
302+
});
286303
});
287304

288305
describe('ensureDependencyDashboard()', () => {
@@ -685,6 +702,47 @@ describe('workers/repository/dependency-dashboard', () => {
685702
await dryRun(branches, platform, 0, 1);
686703
});
687704

705+
it('checks an issue with group size not met branches', async () => {
706+
const branches: BranchConfig[] = [
707+
{
708+
...mock<BranchConfig>(),
709+
upgrades: [{ ...mock<PrUpgrade>(), depName: 'dep1' }],
710+
result: 'minimum-group-size-not-met',
711+
branchName: 'groupBranch1',
712+
},
713+
];
714+
config.dependencyDashboard = true;
715+
await dependencyDashboard.ensureDependencyDashboard(
716+
config,
717+
branches,
718+
{},
719+
{ result: 'no-migration' },
720+
);
721+
expect(platform.ensureIssueClosing).toHaveBeenCalledTimes(0);
722+
expect(platform.ensureIssue).toHaveBeenCalledTimes(1);
723+
expect(platform.ensureIssue.mock.calls[0][0].title).toBe(
724+
config.dependencyDashboardTitle,
725+
);
726+
expect(platform.ensureIssue.mock.calls[0][0].body.trim()).toBe(
727+
codeBlock`
728+
This issue lists Renovate updates and detected dependencies. Read the [Dependency Dashboard](https://docs.renovatebot.com/key-concepts/dashboard/) docs to learn more.
729+
730+
## Group Size Not Met
731+
732+
The following branches have not met their minimum group size. To create them, click on a checkbox below.
733+
734+
- [ ] <!-- approveGroup-branch=groupBranch1 -->undefined
735+
736+
## Detected dependencies
737+
738+
None detected
739+
`,
740+
);
741+
742+
// same with dry run
743+
await dryRun(branches, platform, 0, 1);
744+
});
745+
688746
it('checks an issue with 3 PR in approval', async () => {
689747
const branches: BranchConfig[] = [
690748
{

lib/workers/repository/dependency-dashboard.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,13 @@ export async function ensureDependencyDashboard(
395395
'Create all pending approval PRs at once',
396396
'🔐',
397397
);
398+
issueBody += getBranchesListMd(
399+
branches,
400+
(branch) => branch.result === 'minimum-group-size-not-met',
401+
'Group Size Not Met',
402+
'The following branches have not met their minimum group size. To create them, click on a checkbox below.',
403+
'approveGroup',
404+
);
398405
issueBody += getBranchesListMd(
399406
branches,
400407
(branch) => branch.result === 'not-scheduled',
@@ -466,6 +473,7 @@ export async function ensureDependencyDashboard(
466473
'error',
467474
'automerged',
468475
'pr-edited',
476+
'minimum-group-size-not-met',
469477
];
470478
const inProgress = branches.filter(
471479
(branch) =>

lib/workers/repository/update/branch/index.spec.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,19 @@ describe('workers/repository/update/branch/index', () => {
165165
});
166166
});
167167

168+
it('skips branch creation if minimumGroupSize is not met', async () => {
169+
scm.branchExists.mockResolvedValue(false);
170+
const res = await branchWorker.processBranch({
171+
...config,
172+
minimumGroupSize: 3,
173+
});
174+
expect(res).toEqual({
175+
branchExists: false,
176+
prNo: undefined,
177+
result: 'minimum-group-size-not-met',
178+
});
179+
});
180+
168181
it('skips branch if not scheduled and not updating out of schedule', async () => {
169182
schedule.isScheduledNow.mockReturnValueOnce(false);
170183
config.updateNotScheduled = false;

lib/workers/repository/update/branch/index.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,8 @@ export async function processBranch(
125125
let config: BranchConfig = { ...branchConfig };
126126
logger.trace({ config }, 'processBranch()');
127127
let branchExists = await scm.branchExists(config.branchName);
128+
const dependencyDashboardCheck =
129+
config.dependencyDashboardChecks?.[config.branchName];
128130
let updatesVerified = false;
129131
if (!branchExists && config.branchPrefix !== config.branchPrefixOld) {
130132
const branchName = config.branchName.replace(
@@ -138,13 +140,26 @@ export async function processBranch(
138140
}
139141
}
140142

143+
if (
144+
!branchExists &&
145+
branchConfig.minimumGroupSize &&
146+
branchConfig.minimumGroupSize > branchConfig.upgrades.length &&
147+
!dependencyDashboardCheck
148+
) {
149+
logger.debug(
150+
`Skipping branch creation as minimumGroupSize: ${branchConfig.minimumGroupSize} is not met`,
151+
);
152+
return {
153+
branchExists: false,
154+
result: 'minimum-group-size-not-met',
155+
};
156+
}
157+
141158
let branchPr = await platform.getBranchPr(
142159
config.branchName,
143160
config.baseBranch,
144161
);
145162
logger.debug(`branchExists=${branchExists}`);
146-
const dependencyDashboardCheck =
147-
config.dependencyDashboardChecks?.[config.branchName];
148163
logger.debug(`dependencyDashboardCheck=${dependencyDashboardCheck!}`);
149164
if (branchPr) {
150165
config.rebaseRequested = await rebaseCheck(config, branchPr);

lib/workers/repository/updates/__snapshots__/generate.spec.ts.snap

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ exports[`workers/repository/updates/generate > generateBranchConfig() > handles
2121
"b",
2222
],
2323
"manager": "some-manager",
24+
"minimumGroupSize": 1,
2425
"newValue": "0.6.0",
2526
"prBodyColumns": [],
2627
"prTitle": "some-title",
@@ -112,6 +113,7 @@ exports[`workers/repository/updates/generate > generateBranchConfig() > handles
112113
"isRange": false,
113114
"labels": [],
114115
"manager": "some-manager",
116+
"minimumGroupSize": 1,
115117
"newValue": "0.6.0",
116118
"prBodyColumns": [],
117119
"prTitle": "some-title",
@@ -194,6 +196,7 @@ exports[`workers/repository/updates/generate > generateBranchConfig() > handles
194196
"isLockFileMaintenance": true,
195197
"labels": [],
196198
"manager": "some-manager",
199+
"minimumGroupSize": 1,
197200
"prBodyColumns": [],
198201
"prTitle": "some-title",
199202
"prettyDepType": "dependency",
@@ -239,6 +242,7 @@ exports[`workers/repository/updates/generate > generateBranchConfig() > handles
239242
"labels": [],
240243
"lockedVersion": "1.0.0",
241244
"manager": "some-manager",
245+
"minimumGroupSize": 1,
242246
"newValue": "^1.0.0",
243247
"newVersion": "1.0.1",
244248
"prBodyColumns": [],

lib/workers/repository/updates/generate.spec.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ describe('workers/repository/updates/generate', () => {
4848
expect(res.groupName).toBeUndefined();
4949
expect(res.releaseTimestamp).toBeDefined();
5050
expect(res.recreateClosed).toBe(false);
51+
expect(res.minimumGroupSize).toBe(1);
5152
});
5253

5354
it('handles lockFileMaintenance', () => {
@@ -75,6 +76,34 @@ describe('workers/repository/updates/generate', () => {
7576
});
7677
});
7778

79+
it('sets minimumGroupSize based on upgrades', () => {
80+
const branch = [
81+
{
82+
manager: 'some-manager',
83+
branchName: 'some-branch',
84+
prTitle: 'some-title',
85+
minimumGroupSize: 1,
86+
},
87+
{
88+
manager: 'some-manager',
89+
branchName: 'some-branch',
90+
prTitle: 'some-title',
91+
minimumGroupSize: 2,
92+
},
93+
{
94+
manager: 'some-manager',
95+
branchName: 'some-branch',
96+
prTitle: 'some-title',
97+
minimumGroupSize: 3,
98+
},
99+
] satisfies BranchUpgradeConfig[];
100+
const res = generateBranchConfig(branch);
101+
expect(res.minimumGroupSize).toBe(3);
102+
expect(logger.logger.debug).toHaveBeenCalledWith(
103+
'Multiple minimumGroupSize values found for this branch, using highest.',
104+
);
105+
});
106+
78107
it('handles lockFileUpdate', () => {
79108
const branch = [
80109
{

lib/workers/repository/updates/generate.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,28 @@ function compilePrTitle(
160160
logger.trace(`prTitle: ` + JSON.stringify(upgrade.prTitle));
161161
}
162162

163+
function getMinimumGroupSize(upgrades: BranchUpgradeConfig[]): number {
164+
let minimumGroupSize = 1;
165+
const groupSizes = new Set<number>();
166+
167+
for (const upg of upgrades) {
168+
if (upg.minimumGroupSize) {
169+
groupSizes.add(upg.minimumGroupSize);
170+
if (minimumGroupSize < upg.minimumGroupSize) {
171+
minimumGroupSize = upg.minimumGroupSize;
172+
}
173+
}
174+
}
175+
176+
if (groupSizes.size > 1) {
177+
logger.debug(
178+
'Multiple minimumGroupSize values found for this branch, using highest.',
179+
);
180+
}
181+
182+
return minimumGroupSize;
183+
}
184+
163185
// Sorted by priority, from low to high
164186
const semanticCommitTypeByPriority = ['chore', 'ci', 'build', 'fix', 'feat'];
165187

@@ -461,6 +483,7 @@ export function generateBranchConfig(
461483
}
462484
}
463485

486+
config.minimumGroupSize = getMinimumGroupSize(config.upgrades);
464487
// Set skipInstalls to false if any upgrade in the branch has it false
465488
config.skipInstalls = config.upgrades.every(
466489
(upgrade) => upgrade.skipInstalls !== false,

0 commit comments

Comments
 (0)