From 6100dcdbba38abe571836c72de9ac9ddee381a8f Mon Sep 17 00:00:00 2001 From: Stefano Arlandini Date: Wed, 12 Feb 2025 00:35:33 +0100 Subject: [PATCH 1/3] feat(manager/composer): support updates with minimal changes --- lib/modules/manager/composer/artifacts.ts | 3 +- lib/modules/manager/composer/utils.spec.ts | 51 +++++++++++++++++++ lib/modules/manager/composer/utils.ts | 14 +++++ lib/modules/versioning/composer/index.spec.ts | 21 ++++++++ lib/modules/versioning/composer/index.ts | 10 ++++ lib/modules/versioning/npm/index.spec.ts | 20 ++++++++ lib/modules/versioning/npm/index.ts | 2 + lib/modules/versioning/types.ts | 5 ++ 8 files changed, 125 insertions(+), 1 deletion(-) diff --git a/lib/modules/manager/composer/artifacts.ts b/lib/modules/manager/composer/artifacts.ts index 23dba464cd7c2d..370606e6878393 100644 --- a/lib/modules/manager/composer/artifacts.ts +++ b/lib/modules/manager/composer/artifacts.ts @@ -33,6 +33,7 @@ import type { AuthJson } from './types'; import { extractConstraints, getComposerArguments, + getComposerUpdateArguments, getPhpConstraint, isArtifactAuthEnabled, requireComposerDependencyInstallation, @@ -191,7 +192,7 @@ export async function updateArtifacts({ .join(' ') ).trim() + ' --with-dependencies'; } - args += getComposerArguments(config, composerToolConstraint); + args += getComposerUpdateArguments(config, composerToolConstraint) logger.trace({ cmd, args }, 'composer command'); commands.push(`${cmd} ${args}`); diff --git a/lib/modules/manager/composer/utils.spec.ts b/lib/modules/manager/composer/utils.spec.ts index 22fb07fd2981e3..b0f3cbe9dd5d9a 100644 --- a/lib/modules/manager/composer/utils.spec.ts +++ b/lib/modules/manager/composer/utils.spec.ts @@ -5,6 +5,7 @@ import { Lockfile, PackageFile } from './schema'; import { extractConstraints, getComposerArguments, + getComposerUpdateArguments, requireComposerDependencyInstallation, } from './utils'; @@ -277,6 +278,56 @@ describe('modules/manager/composer/utils', () => { }); }); + describe('getComposerUpdateArguments', () => { + it('does not request an update with minimal changes with 2.6.0', () => { + expect( + getComposerUpdateArguments({}, { toolName: 'composer', constraint: '2.6.0' }), + ).toBe( + ' --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins', + ); + }); + + it('requests an update with minimal changes with 2.7.0', () => { + expect( + getComposerUpdateArguments({}, { toolName: 'composer', constraint: '2.7.0' }), + ).toBe( + " --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins --minimal-changes", + ); + }); + + it('requests an update with minimal changes with 2.8.0', () => { + expect( + getComposerUpdateArguments({}, { toolName: 'composer', constraint: '2.8.0' }), + ).toBe( + " --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins --minimal-changes", + ); + }); + + it('requests an update with minimal changes with ^2.6', () => { + expect( + getComposerUpdateArguments({}, { toolName: 'composer', constraint: '^2.6' }), + ).toBe( + " --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins --minimal-changes", + ); + }); + + it('requests an update with minimal changes with ^2.7', () => { + expect( + getComposerUpdateArguments({}, { toolName: 'composer', constraint: '^2.7' }), + ).toBe( + " --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins --minimal-changes", + ); + }); + + it('requests an update with minimal changes with ^2.8', () => { + expect( + getComposerUpdateArguments({}, { toolName: 'composer', constraint: '^2.8' }), + ).toBe( + " --no-ansi --no-interaction --no-scripts --no-autoloader --no-plugins --minimal-changes", + ); + }); + }); + describe('requireComposerDependencyInstallation', () => { it('returns true when symfony/flex has been installed', () => { const lockfile = Lockfile.parse({ diff --git a/lib/modules/manager/composer/utils.ts b/lib/modules/manager/composer/utils.ts index 89328f611ef24d..2e7546e95c7616 100644 --- a/lib/modules/manager/composer/utils.ts +++ b/lib/modules/manager/composer/utils.ts @@ -46,6 +46,20 @@ export function getComposerArguments( return args; } +export function getComposerUpdateArguments( + config: UpdateArtifactsConfig, + toolConstraint: ToolConstraint, +): string { + let args = getComposerArguments(config, toolConstraint); + + // TODO: toolConstraint.constraint can be null or undefined? (#22198) + if (api.intersects!(toolConstraint.constraint!, '^2.7')) { + args += ' --minimal-changes'; + } + + return args; +} + export function getPhpConstraint( constraints: Record, ): string | null { diff --git a/lib/modules/versioning/composer/index.spec.ts b/lib/modules/versioning/composer/index.spec.ts index 56feb523462a64..6423888fe3c9f8 100644 --- a/lib/modules/versioning/composer/index.spec.ts +++ b/lib/modules/versioning/composer/index.spec.ts @@ -174,6 +174,27 @@ describe('modules/versioning/composer/index', () => { expect(semver.subset!(a, b)).toBe(expected); }); + it.each` + a | b | expected + ${'1.0.0'} | ${'1.0.0'} | ${true} + ${'1.0.0'} | ${'>=1.0.0'} | ${true} + ${'1.1.0'} | ${'^1.0.0'} | ${true} + ${'>=1.0.0'} | ${'>=1.0.0'} | ${true} + ${'~1.0.0'} | ${'~1.0.0'} | ${true} + ${'^1.0.0'} | ${'^1.0.0'} | ${true} + ${'>=1.0.0'} | ${'>=1.1.0'} | ${true} + ${'~1.0.0'} | ${'~1.1.0'} | ${false} + ${'^1.0.0'} | ${'^1.1.0'} | ${true} + ${'>=1.0.0'} | ${'<1.0.0'} | ${false} + ${'~1.0.0'} | ${'~0.9.0'} | ${false} + ${'^1.0.0'} | ${'^0.9.0'} | ${false} + ${'^1.1.0 || ^2.0.0'} | ${'^1.0.0 || ^2.0.0'} | ${true} + ${'^1.0.0 || ^2.0.0'} | ${'^1.1.0 || ^2.0.0'} | ${true} + ${'^7.0.0'} | ${'<8.0-DEV'} | ${true} + `('intersects("$a", "$b") === $expected', ({ a, b, expected }) => { + expect(semver.intersects!(a, b)).toBe(expected); + }); + it.each` currentValue | rangeStrategy | currentVersion | newVersion | expected ${'~1.0'} | ${'pin'} | ${'1.0'} | ${'V1.1'} | ${'V1.1'} diff --git a/lib/modules/versioning/composer/index.ts b/lib/modules/versioning/composer/index.ts index d875bfa13ea5d5..a397ef39ad9033 100644 --- a/lib/modules/versioning/composer/index.ts +++ b/lib/modules/versioning/composer/index.ts @@ -234,6 +234,15 @@ function subset(subRange: string, superRange: string): boolean | undefined { } } +function intersects(subRange: string, superRange: string): boolean { + try { + return npm.intersects!(composer2npm(subRange), composer2npm(superRange)); + } catch (err) { + logger.trace({ err }, 'composer.intersects error'); + return false; + } +} + function getNewValue({ currentValue, rangeStrategy, @@ -393,5 +402,6 @@ export const api: VersioningApi = { getNewValue, sortVersions, subset, + intersects, }; export default api; diff --git a/lib/modules/versioning/npm/index.spec.ts b/lib/modules/versioning/npm/index.spec.ts index ee55880a6020e5..e1c3b1d32bea3c 100644 --- a/lib/modules/versioning/npm/index.spec.ts +++ b/lib/modules/versioning/npm/index.spec.ts @@ -75,6 +75,26 @@ describe('modules/versioning/npm/index', () => { expect(semver.subset!(a, b)).toBe(expected); }); + it.each` + a | b | expected + ${'1.0.0'} | ${'1.0.0'} | ${true} + ${'1.0.0'} | ${'>=1.0.0'} | ${true} + ${'1.1.0'} | ${'^1.0.0'} | ${true} + ${'>=1.0.0'} | ${'>=1.0.0'} | ${true} + ${'~1.0.0'} | ${'~1.0.0'} | ${true} + ${'^1.0.0'} | ${'^1.0.0'} | ${true} + ${'>=1.0.0'} | ${'>=1.1.0'} | ${true} + ${'~1.0.0'} | ${'~1.1.0'} | ${false} + ${'^1.0.0'} | ${'^1.1.0'} | ${true} + ${'>=1.0.0'} | ${'<1.0.0'} | ${false} + ${'~1.0.0'} | ${'~0.9.0'} | ${false} + ${'^1.0.0'} | ${'^0.9.0'} | ${false} + ${'^1.1.0 || ^2.0.0'} | ${'^1.0.0 || ^2.0.0'} | ${true} + ${'^1.0.0 || ^2.0.0'} | ${'^1.1.0 || ^2.0.0'} | ${true} + `('intersects("$a", "$b") === $expected', ({ a, b, expected }) => { + expect(semver.intersects!(a, b)).toBe(expected); + }); + it.each` currentValue | rangeStrategy | currentVersion | newVersion | expected ${'=1.0.0'} | ${'bump'} | ${'1.0.0'} | ${'1.1.0'} | ${'=1.1.0'} diff --git a/lib/modules/versioning/npm/index.ts b/lib/modules/versioning/npm/index.ts index 6ece07d64fbf30..8b6147e0944423 100644 --- a/lib/modules/versioning/npm/index.ts +++ b/lib/modules/versioning/npm/index.ts @@ -34,6 +34,7 @@ const { gt: isGreaterThan, eq: equals, subset, + intersects, } = semver; // If this is left as an alias, inputs like "17.04.0" throw errors @@ -65,6 +66,7 @@ export const api: VersioningApi = { minSatisfyingVersion, sortVersions, subset, + intersects, }; export default api; diff --git a/lib/modules/versioning/types.ts b/lib/modules/versioning/types.ts index f1e66ac15b7b30..1f8fe71ee69eb7 100644 --- a/lib/modules/versioning/types.ts +++ b/lib/modules/versioning/types.ts @@ -125,6 +125,11 @@ export interface VersioningApi { */ subset?(subRange: string, superRange: string): boolean | undefined; + /** + * Checks whether subRange intersects superRange. + */ + intersects?(subRange: string, superRange: string): boolean; + /** * Return whether unstable-to-unstable upgrades within the same major version are allowed. */ From 19a623fbcf5efe342cb4eaf805f4ac1095d85f40 Mon Sep 17 00:00:00 2001 From: Stefano Arlandini Date: Fri, 14 Feb 2025 18:16:41 +0100 Subject: [PATCH 2/3] fix: handle null or undefined tool constraint --- lib/modules/manager/composer/utils.ts | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/modules/manager/composer/utils.ts b/lib/modules/manager/composer/utils.ts index 2e7546e95c7616..a67f0161378fa9 100644 --- a/lib/modules/manager/composer/utils.ts +++ b/lib/modules/manager/composer/utils.ts @@ -8,6 +8,7 @@ import { coerceNumber } from '../../../util/number'; import { api, id as composerVersioningId } from '../../versioning/composer'; import type { UpdateArtifactsConfig } from '../types'; import type { Lockfile, PackageFile } from './schema'; +import is from '@sindresorhus/is'; export { composerVersioningId }; @@ -21,12 +22,16 @@ export function getComposerArguments( if (config.composerIgnorePlatformReqs) { if (config.composerIgnorePlatformReqs.length === 0) { - // TODO: toolConstraint.constraint can be null or undefined? (#22198) - const major = api.getMajor(toolConstraint.constraint!); - const minor = api.getMinor(toolConstraint.constraint!); - args += api.matches(`${major}.${minor}`, '^2.2') - ? " --ignore-platform-req='ext-*' --ignore-platform-req='lib-*'" - : ' --ignore-platform-reqs'; + if (is.string(toolConstraint.constraint)) { + const major = api.getMajor(toolConstraint.constraint); + const minor = api.getMinor(toolConstraint.constraint); + + args += api.matches(`${major}.${minor}`, '^2.2') + ? " --ignore-platform-req='ext-*' --ignore-platform-req='lib-*'" + : ' --ignore-platform-reqs'; + } else { + args += ' --ignore-platform-reqs'; + } } else { config.composerIgnorePlatformReqs.forEach((req) => { args += ' --ignore-platform-req ' + quote(req); @@ -52,9 +57,11 @@ export function getComposerUpdateArguments( ): string { let args = getComposerArguments(config, toolConstraint); - // TODO: toolConstraint.constraint can be null or undefined? (#22198) - if (api.intersects!(toolConstraint.constraint!, '^2.7')) { - args += ' --minimal-changes'; + if ( + is.string(toolConstraint.constraint) && + api.intersects!(toolConstraint.constraint, '^2.7') + ) { + args += ' --minimal-changes'; } return args; From 6b327c30924423495df1c095dcf0ee4ed3a90dbb Mon Sep 17 00:00:00 2001 From: Stefano Arlandini Date: Sun, 16 Feb 2025 18:45:48 +0100 Subject: [PATCH 3/3] chore: revert changes to `getComposerArguments()` function --- lib/modules/manager/composer/utils.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/modules/manager/composer/utils.ts b/lib/modules/manager/composer/utils.ts index a67f0161378fa9..38fe4870851550 100644 --- a/lib/modules/manager/composer/utils.ts +++ b/lib/modules/manager/composer/utils.ts @@ -22,16 +22,12 @@ export function getComposerArguments( if (config.composerIgnorePlatformReqs) { if (config.composerIgnorePlatformReqs.length === 0) { - if (is.string(toolConstraint.constraint)) { - const major = api.getMajor(toolConstraint.constraint); - const minor = api.getMinor(toolConstraint.constraint); - - args += api.matches(`${major}.${minor}`, '^2.2') - ? " --ignore-platform-req='ext-*' --ignore-platform-req='lib-*'" - : ' --ignore-platform-reqs'; - } else { - args += ' --ignore-platform-reqs'; - } + // TODO: toolConstraint.constraint can be null or undefined? (#22198) + const major = api.getMajor(toolConstraint.constraint!); + const minor = api.getMinor(toolConstraint.constraint!); + args += api.matches(`${major}.${minor}`, '^2.2') + ? " --ignore-platform-req='ext-*' --ignore-platform-req='lib-*'" + : ' --ignore-platform-reqs'; } else { config.composerIgnorePlatformReqs.forEach((req) => { args += ' --ignore-platform-req ' + quote(req);