diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md index 29df075503d324..76215bf4576cb8 100644 --- a/docs/usage/configuration-options.md +++ b/docs/usage/configuration-options.md @@ -3773,6 +3773,15 @@ Configure this to `true` if you wish to get one PR for every separate major vers e.g. if you are on webpack@v1 currently then default behavior is a PR for upgrading to webpack@v3 and not for webpack@v2. If this setting is true then you would get one PR for webpack@v2 and one for webpack@v3. +## separateMultipleMinor + +Enable this for dependencies when it is important to split updates into separate PRs per minor release stream (e.g. `python`). + +For example, if you are on `python@v3.9.0` currently, then by default Renovate creates a PR to upgrade you to the latest version such as `python@v3.12.x`. +By default, Renovate skips versions in between, like `python@v3.10.x`. + +But if you set `separateMultipleMinor=true` then you get separate PRs for each minor stream, like `python@3.9.x`, `python@v3.10.x` and `python@v3.11.x`, etc. + ## skipInstalls By default, Renovate will use the most efficient approach to updating package files and lock files, which in most cases skips the need to perform a full module install by the bot. diff --git a/lib/config/options/index.ts b/lib/config/options/index.ts index c8e743b6c16ec8..91d41e1070d4a2 100644 --- a/lib/config/options/index.ts +++ b/lib/config/options/index.ts @@ -1620,6 +1620,15 @@ const options: RenovateOptions[] = [ type: 'boolean', default: false, }, + { + name: 'separateMultipleMinor', + description: + 'If set to `true`, Renovate creates separate PRs for each `minor` stream.', + stage: 'package', + type: 'boolean', + default: false, + experimental: true, + }, { name: 'separateMinorPatch', description: diff --git a/lib/config/presets/internal/default.ts b/lib/config/presets/internal/default.ts index 68895dc50d603e..c60cbefefe6ebb 100644 --- a/lib/config/presets/internal/default.ts +++ b/lib/config/presets/internal/default.ts @@ -574,6 +574,11 @@ export const presets: Record = { separateMajorMinor: true, separateMultipleMajor: true, }, + separateMultipleMinorReleases: { + description: + 'Separate each `minor` version of dependencies into individual branches/PRs.', + separateMultipleMinor: true, + }, separatePatchReleases: { description: 'Separate `patch` and `minor` releases of dependencies into separate PRs.', diff --git a/lib/config/validation.ts b/lib/config/validation.ts index 976a91b028644b..7807c9efb49d65 100644 --- a/lib/config/validation.ts +++ b/lib/config/validation.ts @@ -464,6 +464,7 @@ export async function validateConfig( 'separateMajorMinor', 'separateMinorPatch', 'separateMultipleMajor', + 'separateMultipleMinor', 'versioning', ]; if (is.nonEmptyArray(resolvedRule.matchUpdateTypes)) { diff --git a/lib/workers/repository/process/lookup/bucket.ts b/lib/workers/repository/process/lookup/bucket.ts index 4cc4295673f891..b54b70ab99d8ea 100644 --- a/lib/workers/repository/process/lookup/bucket.ts +++ b/lib/workers/repository/process/lookup/bucket.ts @@ -3,6 +3,7 @@ import type { VersioningApi } from '../../../../modules/versioning/types'; export interface BucketConfig { separateMajorMinor?: boolean; separateMultipleMajor?: boolean; + separateMultipleMinor?: boolean; separateMinorPatch?: boolean; } @@ -12,8 +13,12 @@ export function getBucket( newVersion: string, versioning: VersioningApi, ): string | null { - const { separateMajorMinor, separateMultipleMajor, separateMinorPatch } = - config; + const { + separateMajorMinor, + separateMultipleMajor, + separateMultipleMinor, + separateMinorPatch, + } = config; if (!separateMajorMinor) { return 'latest'; } @@ -46,11 +51,9 @@ export function getBucket( // Check the minor update type first if (fromMinor !== toMinor) { - /* future option if (separateMultipleMinor) { return `v${toMajor}.${toMinor}`; } - */ if (separateMinorPatch) { return 'minor'; diff --git a/lib/workers/repository/process/lookup/index.spec.ts b/lib/workers/repository/process/lookup/index.spec.ts index e56be9918897ad..6116f2edfaa508 100644 --- a/lib/workers/repository/process/lookup/index.spec.ts +++ b/lib/workers/repository/process/lookup/index.spec.ts @@ -2785,6 +2785,21 @@ describe('workers/repository/process/lookup/index', () => { ]); }); + it('should upgrade to 16 minors', async () => { + config.currentValue = '1.0.0'; + config.separateMultipleMinor = true; + config.packageName = 'webpack'; + config.datasource = NpmDatasource.id; + httpMock + .scope('https://registry.npmjs.org') + .get('/webpack') + .reply(200, webpackJson); + const { updates } = await Result.wrap( + lookup.lookupUpdates(config), + ).unwrapOrThrow(); + expect(updates).toHaveLength(16); + }); + it('does not jump major unstable', async () => { config.currentValue = '^4.4.0-canary.3'; config.rangeStrategy = 'replace'; diff --git a/lib/workers/repository/process/lookup/types.ts b/lib/workers/repository/process/lookup/types.ts index 5524de60ad4113..ed944ea253d5e9 100644 --- a/lib/workers/repository/process/lookup/types.ts +++ b/lib/workers/repository/process/lookup/types.ts @@ -41,6 +41,7 @@ export interface LookupUpdateConfig isVulnerabilityAlert?: boolean; separateMajorMinor?: boolean; separateMultipleMajor?: boolean; + separateMultipleMinor?: boolean; datasource: string; packageName: string; minimumConfidence?: MergeConfidence | undefined; diff --git a/lib/workers/repository/process/lookup/update-type.ts b/lib/workers/repository/process/lookup/update-type.ts index a47375b4c64e77..9db72f6a56746e 100644 --- a/lib/workers/repository/process/lookup/update-type.ts +++ b/lib/workers/repository/process/lookup/update-type.ts @@ -4,6 +4,7 @@ import type * as allVersioning from '../../../../modules/versioning'; export interface UpdateTypeConfig { separateMajorMinor?: boolean; separateMultipleMajor?: boolean; + separateMultipleMinor?: boolean; separateMinorPatch?: boolean; } diff --git a/lib/workers/repository/updates/branch-name.spec.ts b/lib/workers/repository/updates/branch-name.spec.ts index 62da7332c99dd1..297d7012bcd4af 100644 --- a/lib/workers/repository/updates/branch-name.spec.ts +++ b/lib/workers/repository/updates/branch-name.spec.ts @@ -59,6 +59,43 @@ describe('workers/repository/updates/branch-name', () => { expect(upgrade.branchName).toBe('major-2-some-group-slug-grouptopic'); }); + it('separates minor with groups', () => { + const upgrade: RenovateConfig = { + groupName: 'some group name', + groupSlug: 'some group slug', + updateType: 'minor', + separateMultipleMinor: true, + newMinor: 1, + newMajor: 2, + group: { + branchName: '{{groupSlug}}-{{branchTopic}}', + branchTopic: 'grouptopic', + }, + }; + generateBranchName(upgrade); + expect(upgrade.branchName).toBe('minor-2.1-some-group-slug-grouptopic'); + }); + + it('separates minor when separateMultipleMinor=true', () => { + const upgrade: RenovateConfig = { + branchName: + '{{{branchPrefix}}}{{{additionalBranchPrefix}}}{{{branchTopic}}}', + branchPrefix: 'renovate/', + additionalBranchPrefix: '', + depNameSanitized: 'lodash', + newMajor: 4, + separateMinorPatch: true, + isPatch: true, + newMinor: 17, + branchTopic: + '{{{depNameSanitized}}}-{{{newMajor}}}{{#if separateMinorPatch}}{{#if isPatch}}.{{{newMinor}}}{{/if}}{{/if}}.x{{#if isLockfileUpdate}}-lockfile{{/if}}', + depName: 'dep', + group: {}, + }; + generateBranchName(upgrade); + expect(upgrade.branchName).toBe('renovate/lodash-4.17.x'); + }); + it('uses single major with groups', () => { const upgrade: RenovateConfig = { groupName: 'some group name', diff --git a/lib/workers/repository/updates/branch-name.ts b/lib/workers/repository/updates/branch-name.ts index 7200b12ecfee49..bc42966fd0ded7 100644 --- a/lib/workers/repository/updates/branch-name.ts +++ b/lib/workers/repository/updates/branch-name.ts @@ -47,6 +47,8 @@ function cleanBranchName( export function generateBranchName(update: RenovateConfig): void { // Check whether to use a group name + const newMajor = String(update.newMajor); + const newMinor = String(update.newMinor); if (update.groupName) { update.groupName = template.compile(update.groupName, update); logger.trace('Using group branchName template'); @@ -64,12 +66,14 @@ export function generateBranchName(update: RenovateConfig): void { }); if (update.updateType === 'major' && update.separateMajorMinor) { if (update.separateMultipleMajor) { - const newMajor = String(update.newMajor); update.groupSlug = `major-${newMajor}-${update.groupSlug}`; } else { update.groupSlug = `major-${update.groupSlug}`; } } + if (update.updateType === 'minor' && update.separateMultipleMinor) { + update.groupSlug = `minor-${newMajor}.${newMinor}-${update.groupSlug}`; + } if (update.updateType === 'patch' && update.separateMinorPatch) { update.groupSlug = `patch-${update.groupSlug}`; } @@ -116,7 +120,9 @@ export function generateBranchName(update: RenovateConfig): void { update.branchName = template.compile(update.branchName, update); update.branchName = template.compile(update.branchName, update); } - + if (update.updateType === 'minor' && update.separateMultipleMinor) { + update.branchName = update.branchName.replace('.x', `.${newMinor}.x`); + } update.branchName = cleanBranchName( update.branchName, update.branchNameStrict,