From e6a29bbff47656c13180bf3e9c5bd61eb5e21c37 Mon Sep 17 00:00:00 2001 From: RahulGautamSingh Date: Wed, 26 Jun 2024 12:53:39 +0530 Subject: [PATCH] feat(versioning): same major (#28418) Co-authored-by: Sebastian Poxhofer Co-authored-by: Michael Kriese Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Rhys Arkins --- lib/modules/versioning/api.ts | 2 + .../versioning/same-major/index.spec.ts | 70 ++++++++++++++++++ lib/modules/versioning/same-major/index.ts | 72 +++++++++++++++++++ lib/modules/versioning/same-major/readme.md | 27 +++++++ .../package-rules/current-version.spec.ts | 26 +++++++ 5 files changed, 197 insertions(+) create mode 100644 lib/modules/versioning/same-major/index.spec.ts create mode 100644 lib/modules/versioning/same-major/index.ts create mode 100644 lib/modules/versioning/same-major/readme.md diff --git a/lib/modules/versioning/api.ts b/lib/modules/versioning/api.ts index 2fb4afb5d9479d..764529bf40c69f 100644 --- a/lib/modules/versioning/api.ts +++ b/lib/modules/versioning/api.ts @@ -32,6 +32,7 @@ import * as regex from './regex'; import * as rez from './rez'; import * as rpm from './rpm'; import * as ruby from './ruby'; +import * as sameMajor from './same-major'; import * as semver from './semver'; import * as semverCoerced from './semver-coerced'; import * as swift from './swift'; @@ -76,6 +77,7 @@ api.set(regex.id, regex.api); api.set(rez.id, rez.api); api.set(rpm.id, rpm.api); api.set(ruby.id, ruby.api); +api.set(sameMajor.id, sameMajor.api); api.set(semver.id, semver.api); api.set(semverCoerced.id, semverCoerced.api); api.set(swift.id, swift.api); diff --git a/lib/modules/versioning/same-major/index.spec.ts b/lib/modules/versioning/same-major/index.spec.ts new file mode 100644 index 00000000000000..b84791e5d33eab --- /dev/null +++ b/lib/modules/versioning/same-major/index.spec.ts @@ -0,0 +1,70 @@ +import sameMajor from '.'; + +describe('modules/versioning/same-major/index', () => { + describe('.isGreaterThan(version, other)', () => { + it('should return true', () => { + expect(sameMajor.isGreaterThan('4.0.0', '3.0.0')).toBeTrue(); // greater + }); + + it('should return false', () => { + expect(sameMajor.isGreaterThan('2.0.2', '3.1.0')).toBeFalse(); // less + expect(sameMajor.isGreaterThan('3.1.0', '3.0.0')).toBeFalse(); // same major -> equal + expect(sameMajor.isGreaterThan('3.0.0', '3.0.0')).toBeFalse(); // equal + expect(sameMajor.isGreaterThan('a', '3.0.0')).toBeFalse(); // invalid versions + }); + }); + + describe('.matches(version, range)', () => { + it('should return true when version has same major', () => { + expect(sameMajor.matches('1.0.1', '1.0.0')).toBeTrue(); + expect(sameMajor.matches('1.0.0', '1.0.0')).toBeTrue(); + }); + + it('should return false when version has different major', () => { + expect(sameMajor.matches('2.0.1', '1.0.0')).toBeFalse(); + }); + + it('should return false when version is out of range', () => { + expect(sameMajor.matches('1.2.3', '1.2.4')).toBeFalse(); + expect(sameMajor.matches('2.0.0', '1.2.4')).toBeFalse(); + expect(sameMajor.matches('3.2.4', '1.2.4')).toBeFalse(); + }); + + it('should return false when version is invalid', () => { + expect(sameMajor.matches('1.0.0', 'xxx')).toBeFalse(); + }); + }); + + describe('.getSatisfyingVersion(versions, range)', () => { + it('should return max satisfying version in range', () => { + expect( + sameMajor.getSatisfyingVersion( + ['1.0.0', '1.0.4', '1.3.0', '2.0.0'], + '1.0.3', + ), + ).toBe('1.3.0'); + }); + }); + + describe('.minSatisfyingVersion(versions, range)', () => { + it('should return min satisfying version in range', () => { + expect( + sameMajor.minSatisfyingVersion( + ['1.0.0', '1.0.4', '1.3.0', '2.0.0'], + '1.0.3', + ), + ).toBe('1.0.4'); + }); + }); + + describe('.isLessThanRange(version, range)', () => { + it('should return true', () => { + expect(sameMajor.isLessThanRange?.('2.0.2', '3.0.0')).toBeTrue(); + }); + + it('should return false', () => { + expect(sameMajor.isLessThanRange?.('4.0.0', '3.0.0')).toBeFalse(); + expect(sameMajor.isLessThanRange?.('3.1.0', '3.0.0')).toBeFalse(); + }); + }); +}); diff --git a/lib/modules/versioning/same-major/index.ts b/lib/modules/versioning/same-major/index.ts new file mode 100644 index 00000000000000..95d287f6b3d5cd --- /dev/null +++ b/lib/modules/versioning/same-major/index.ts @@ -0,0 +1,72 @@ +import { logger } from '../../../logger'; +import { api as semverCoerced } from '../semver-coerced'; +import type { VersioningApi } from '../types'; + +export const id = 'same-major'; +export const displayName = 'Same Major Versioning'; +export const urls = []; +export const supportsRanges = false; + +/** + * + * Converts input to range if it's a version. eg. X.Y.Z -> '>=X.Y.Z =${input} <${major + 1}`; +} + +// for same major versioning one version is greater than the other if its major is greater +function isGreaterThan(version: string, other: string): boolean { + const versionMajor = semverCoerced.getMajor(version)!; + const otherMajor = semverCoerced.getMajor(other)!; + + if (!versionMajor || !otherMajor) { + return false; + } + + return versionMajor > otherMajor; +} + +function matches(version: string, range: string): boolean { + return semverCoerced.matches(version, massageVersion(range)); +} + +function getSatisfyingVersion( + versions: string[], + range: string, +): string | null { + return semverCoerced.getSatisfyingVersion(versions, massageVersion(range)); +} + +function minSatisfyingVersion( + versions: string[], + range: string, +): string | null { + return semverCoerced.minSatisfyingVersion(versions, massageVersion(range)); +} + +function isLessThanRange(version: string, range: string): boolean { + return semverCoerced.isLessThanRange!(version, massageVersion(range)); +} + +export const api: VersioningApi = { + ...semverCoerced, + matches, + getSatisfyingVersion, + minSatisfyingVersion, + isLessThanRange, + isGreaterThan, +}; +export default api; diff --git a/lib/modules/versioning/same-major/readme.md b/lib/modules/versioning/same-major/readme.md new file mode 100644 index 00000000000000..5a95fe4634d917 --- /dev/null +++ b/lib/modules/versioning/same-major/readme.md @@ -0,0 +1,27 @@ +The 'Same Major' versioning is designed to handle the case where a version needs to treated as a "greate than or equal to" constraint. +Specifically, the case where the version say, `X.Y.Z` signifies a range of compatibility from greater than or equal to `X.Y.Z` to less than `X+1`. + +This process uses Semver-Coerced versioning beneath the surface, single versions (e.g., `X.Y.Z`) are converted to a range like `X+1` and then passed to the corresponding semver-coerced method. + +This method is handy when managing dependencies like dotnet-sdk's rollForward settings. +Let's say a project uses dotnet-sdk version `3.1.0`. +It needs to be compatible with any version in the `3.x.x` range but _not_ with versions in the next major version, like `4.x.x`. + +For example: + +```json +{ + "sdk": { + "version": "6.0.300", + "rollForward": "major" + } +} +``` + +The roll-forward policy to use when selecting an SDK version, either as a fallback when a specific SDK version is missing or as a directive to use a higher version. In this case with `major` it means that select the latest version with the same major. +ie. `>= 6.0.300 < 7.0.0` + +For such cases, the users would not want Renovate to create an update PR for any version within the range `>= 6.0.300 < 7.0.0` as it would not change the behaviour on their end, since it is handled by the manager already. + +Note: +You should create a discussion before using this versioning as this is an experimental support and might have some edge cases unhandled. diff --git a/lib/util/package-rules/current-version.spec.ts b/lib/util/package-rules/current-version.spec.ts index 07a4ba1dfc9229..c4fce08fb497f6 100644 --- a/lib/util/package-rules/current-version.spec.ts +++ b/lib/util/package-rules/current-version.spec.ts @@ -116,5 +116,31 @@ describe('util/package-rules/current-version', () => { ); expect(result).toBeFalse(); }); + + it('return true for same-major verisioning if version lies in expected range', () => { + const result = matcher.matches( + { + versioning: 'same-major', + currentValue: '6.0.300', + }, + { + matchCurrentVersion: '6.0.400', + }, + ); + expect(result).toBeTrue(); + }); + + it('return false for same-major verisioning if version lies outside of expected range', () => { + const result = matcher.matches( + { + versioning: 'same-major', + currentValue: '6.0.300', + }, + { + matchCurrentVersion: '6.0.100', + }, + ); + expect(result).toBeFalse(); + }); }); });