Skip to content

Commit

Permalink
feat(versioning): same major (#28418)
Browse files Browse the repository at this point in the history
Co-authored-by: Sebastian Poxhofer <secustor@users.noreply.github.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com>
Co-authored-by: Rhys Arkins <rhys@arkins.net>
  • Loading branch information
5 people committed Jun 26, 2024
1 parent ab39248 commit e6a29bb
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 0 deletions.
2 changes: 2 additions & 0 deletions lib/modules/versioning/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand Down
70 changes: 70 additions & 0 deletions lib/modules/versioning/same-major/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
72 changes: 72 additions & 0 deletions lib/modules/versioning/same-major/index.ts
Original file line number Diff line number Diff line change
@@ -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 <X+1'
* If the input is already a range, it returns the input.
*/
function massageVersion(input: string): string {
// istanbul ignore if: same-major versioning should not be used with ranges as it defeats the purpose
if (!semverCoerced.isSingleVersion(input)) {
logger.warn(
{ version: input },
'Same major versioning expects a single version but got a range. Please switch to a different versioning as this may lead to unexpected behaviour.',
);
return input;
}

// we are sure to get a major because of the isSingleVersion check
const major = semverCoerced.getMajor(input)!;
return `>=${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;
27 changes: 27 additions & 0 deletions lib/modules/versioning/same-major/readme.md
Original file line number Diff line number Diff line change
@@ -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.
26 changes: 26 additions & 0 deletions lib/util/package-rules/current-version.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});

0 comments on commit e6a29bb

Please sign in to comment.