feat: add versioning for Hermit package manager (#16256)

* feat: add versioning for Hermit package manager

* Update lib/modules/versioning/hermit/index.ts

Co-authored-by: Jamie Magee <jamie.magee@gmail.com>

* Update lib/modules/versioning/hermit/index.ts

Co-authored-by: Jamie Magee <jamie.magee@gmail.com>

* Update index.ts index.spec.ts and readme.md according to PR comments

* fix: fix versioning test double negation and _parseVersion function which is just for testing

* fix: simplify hermit versioning implementation as suggested

* fix: use _compare to simplify versioning implementation

* fix: reword version in hermit versioning and make _isChannel & _getChannel static

* fix: remove duplicated title in test and make _config readonly

Co-authored-by: Jamie Magee <jamie.magee@gmail.com>
Co-authored-by: Michael Kriese <michael.kriese@visualon.de>
This commit is contained in:
Yun Lai 2022-07-07 23:30:22 +10:00 committed by GitHub
parent 5bfa68b8d3
commit 605f35c45c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 348 additions and 1 deletions

View file

@ -8,6 +8,7 @@ import * as git from './git';
import * as gradle from './gradle';
import * as hashicorp from './hashicorp';
import * as helm from './helm';
import * as hermit from './hermit';
import * as hex from './hex';
import * as ivy from './ivy';
import * as loose from './loose';
@ -40,6 +41,7 @@ api.set('git', git.api);
api.set('gradle', gradle.api);
api.set('hashicorp', hashicorp.api);
api.set('helm', helm.api);
api.set('hermit', hermit.api);
api.set('hex', hex.api);
api.set('ivy', ivy.api);
api.set('loose', loose.api);

View file

@ -0,0 +1,196 @@
import { HermitVersioning } from './index';
describe('modules/versioning/hermit/index', () => {
const versioning = new HermitVersioning();
test.each`
version | expected
${'1'} | ${true}
${'1.2'} | ${true}
${'@1'} | ${false}
${'@1.2'} | ${false}
${'@1.2.3'} | ${false}
${'@latest'} | ${false}
${'@stable'} | ${false}
`('isStable("$version") === $expected', ({ version, expected }) => {
expect(versioning.isStable(version)).toBe(expected);
});
test.each`
version | expected
${'1'} | ${true}
${'1rc1'} | ${true}
${'1-foo'} | ${true}
${'1+bar'} | ${true}
${'1.2'} | ${true}
${'1.2-foo'} | ${true}
${'1.2+bar'} | ${true}
${'1.2.3'} | ${true}
${'1.2.3rc1'} | ${true}
${'1.2.3-foo'} | ${true}
${'1.2.3+bar'} | ${true}
${'17.0.1_12'} | ${true}
${'17.0.1_12+m1'} | ${true}
${'17.0.1_12+m1'} | ${true}
${'11.0.11_9-zulu11.48.21'} | ${true}
${'1.2-kotlin.3'} | ${true}
${'@1'} | ${true}
${'@1.2'} | ${true}
${'@1.2.3'} | ${true}
${'@latest'} | ${true}
${'@stable'} | ${true}
`('isValid("$version") === $expected', ({ version, expected }) => {
expect(versioning.isValid(version)).toBe(expected);
});
test.each`
version | major | minor | patch
${'17'} | ${17} | ${0} | ${0}
${'17.2'} | ${17} | ${2} | ${0}
${'17.2.3a1'} | ${17} | ${2} | ${3}
${'17.2.3-foo'} | ${17} | ${2} | ${3}
${'17.2.3+m1'} | ${17} | ${2} | ${3}
${'@17'} | ${17} | ${null} | ${null}
${'@17.2'} | ${17} | ${2} | ${null}
${'@stable'} | ${null} | ${null} | ${null}
`(
'getMajor, getMinor, getPatch for "$version"',
({ version, major, minor, patch }) => {
expect(versioning.getMajor(version)).toBe(major);
expect(versioning.getMinor(version)).toBe(minor);
expect(versioning.getPatch(version)).toBe(patch);
}
);
test.each`
version | other | expected
${'1'} | ${'1.2'} | ${false}
${'@1'} | ${'@1.2'} | ${false}
${'@1.2'} | ${'@1.2'} | ${true}
${'@1.2'} | ${'@1.3'} | ${false}
${'@1.2.3'} | ${'@1.2'} | ${false}
${'@1.2.3_4'} | ${'@1.2.3'} | ${false}
${'@latest'} | ${'@1'} | ${false}
${'@stable'} | ${'@stable'} | ${true}
${'stable'} | ${'stable'} | ${true}
`(
'equals("$version", "$other") === $expected',
({ version, other, expected }) => {
expect(versioning.equals(version, other)).toBe(expected);
}
);
test.each`
version | other | expected
${'@1'} | ${'@1.2'} | ${false}
${'@1.2'} | ${'@1.2'} | ${true}
${'@1.2.3'} | ${'@1.2'} | ${false}
${'@latest'} | ${'@1'} | ${false}
${'@stable'} | ${'@stable'} | ${true}
`(
'matches("$version", "$other") === $expected',
({ version, other, expected }) => {
expect(versioning.matches(version, other)).toBe(expected);
}
);
test.each`
version | other | expected
${'@1'} | ${'@1.2'} | ${true}
${'@1.2'} | ${'@1.2'} | ${false}
${'@1.2'} | ${'@1.3'} | ${false}
${'@1.2.3'} | ${'@1.2'} | ${false}
${'1.2.3'} | ${'@latest'} | ${true}
${'@latest'} | ${'@1'} | ${false}
${'@stable'} | ${'@latest'} | ${true}
${'@latest'} | ${'@stable'} | ${false}
`(
'isGreaterThan("$version", "$other") === $expected',
({ version, other, expected }) => {
expect(versioning.isGreaterThan(version, other)).toBe(expected);
}
);
test.each`
version | other | expected
${'@1'} | ${'@1.2'} | ${false}
${'@1.2'} | ${'@1.2'} | ${false}
${'@1.2.3'} | ${'@1.2'} | ${true}
${'@latest'} | ${'@1'} | ${true}
${'@stable'} | ${'@latest'} | ${false}
${'@latest'} | ${'@stable'} | ${true}
`(
'isLessThanRange("$version", "$other") === $expected',
({ version, other, expected }) => {
expect(versioning.isLessThanRange(version, other)).toBe(expected);
}
);
it('getSatisfyingVersion', () => {
expect(versioning.getSatisfyingVersion(['@1.1.1', '1.2.3'], '1.2.3')).toBe(
'1.2.3'
);
expect(
versioning.getSatisfyingVersion(
['1.1.1', '@2.2.1', '2.2.2', '3.3.3'],
'2.2.2'
)
).toBe('2.2.2');
expect(
versioning.getSatisfyingVersion(
['1.1.1', '@1.3.3', '2.2.2', '3.3.3'],
'1.2.3'
)
).toBeNull();
});
it('minSatisfyingVersion', () => {
expect(versioning.minSatisfyingVersion(['@1.1.1', '1.2.3'], '1.2.3')).toBe(
'1.2.3'
);
expect(
versioning.minSatisfyingVersion(
['1.1.1', '@1.2.3', '2.2.2', '3.3.3'],
'2.2.2'
)
).toBe('2.2.2');
expect(
versioning.minSatisfyingVersion(
['1.1.1', '@1.2.2', '2.2.2', '3.3.3'],
'1.2.3'
)
).toBeNull();
});
describe('sortVersions', () => {
it('sorts versions in an ascending order', () => {
expect(
[
'@1',
'1.1',
'1.2',
'1.2.3',
'1.3',
'@1.2',
'@2',
'2',
'2.1',
'@stable',
'@latest',
].sort((a, b) => versioning.sortVersions(a, b))
).toEqual([
'@latest',
'@stable',
'1.1',
'1.2',
'1.2.3',
'@1.2',
'1.3',
'@1',
'2',
'2.1',
'@2',
]);
});
});
});

View file

@ -0,0 +1,136 @@
import { RegExpVersion, RegExpVersioningApi } from '../regex';
import type { VersioningApiConstructor } from '../types';
export const id = 'hermit';
export const displayName = 'Hermit';
export const urls = [
'https://cashapp.github.io/hermit/packaging/reference/#versions',
];
export const supportsRanges = false;
export class HermitVersioning extends RegExpVersioningApi {
static versionRegex =
'^(?<major>\\d+)(\\.(?<minor>\\d+))?(\\.(?<patch>\\d+))?(_(?<build>\\d+))?([-]?(?<prerelease>[^.+][^+]*))?([+](?<compatibility>[^.-][^+]*))?$';
public constructor() {
super(HermitVersioning.versionRegex);
}
private _isValid(version: string): boolean {
return super._parse(version) !== null;
}
protected override _parse(version: string): RegExpVersion | null {
const parsed = super._parse(version);
if (parsed) {
return parsed;
}
const channelVer = HermitVersioning._getChannel(version);
const groups = this._config?.exec(channelVer)?.groups;
if (!groups) {
return null;
}
const { major, minor, patch, build, prerelease, compatibility } = groups;
const release = [];
if (major) {
release.push(Number.parseInt(major, 10));
}
if (minor) {
release.push(Number.parseInt(minor, 10));
}
if (patch) {
release.push(Number.parseInt(patch, 10));
}
if (build) {
release.push(Number.parseInt(build, 10));
}
return {
release,
prerelease: prerelease,
compatibility: compatibility,
};
}
private static _isChannel(version: string): boolean {
return version.startsWith('@');
}
private static _getChannel(version: string): string {
return version.substring(1);
}
override isStable(version: string): boolean {
if (this._isValid(version)) {
return super.isStable(version);
}
// channel and the rest should be considered unstable version
// as channels are changing values
return false;
}
override isValid(version: string): boolean {
return this._isValid(version) || HermitVersioning._isChannel(version);
}
override isLessThanRange(version: string, range: string): boolean {
return this._compare(version, range) < 0;
}
protected override _compare(version: string, other: string): number {
if (this._isValid(version) && this._isValid(other)) {
return super._compare(version, other);
}
const parsedVersion = this._parse(version);
const parsedOther = this._parse(other);
if (parsedVersion === null || parsedOther === null) {
if (parsedVersion === null && parsedOther === null) {
return version.localeCompare(other);
}
return parsedVersion === null ? -1 : 1;
}
const versionReleases = parsedVersion.release;
const otherReleases = parsedOther.release;
const maxLength =
versionReleases.length > otherReleases.length
? versionReleases.length
: otherReleases.length;
for (let i = 0; i < maxLength; i++) {
const verVal = versionReleases[i];
const otherVal = otherReleases[i];
if (
verVal !== undefined &&
otherVal !== undefined &&
verVal !== otherVal
) {
return verVal - otherVal;
} else if (verVal === undefined) {
return 1;
} else if (otherVal === undefined) {
return -1;
}
}
return 0;
}
override matches(version: string, range: string): boolean {
return this.equals(version, range);
}
}
export const api: VersioningApiConstructor = HermitVersioning;
export default api;

View file

@ -0,0 +1,13 @@
Hermit versioning is a mix of `version` and `channel`.
**Version**
Hermit's package version comes from the packge's original git tag. The version is
an extension to semver, with an extra build number to accomondate package
versions from OpenJDK, which has a value `15.0.1_9`.
**Channel**
[Channel](https://cashapp.github.io/hermit/packaging/reference/#channels) could be hermit generated or user defined.
Channel is considered unstable version and normally won't upgrade.
If you would like to get out of Channel, you could replace the Channel with a given version number and let it managed by Renovate ongoing.

View file

@ -39,7 +39,7 @@ export class RegExpVersioningApi extends GenericVersioningApi<RegExpVersion> {
// RegExp('^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(?<prerelease>[^.-]+)?(-(?<compatibility>.*))?$');
// * matches the versioning approach used by the Bitnami images on DockerHub:
// RegExp('^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(:?-(?<compatibility>.*-r)(?<build>\\d+))?$');
private _config: RegExp | null = null;
protected readonly _config: RegExp;
constructor(_new_config: string | undefined) {
super();