From 605f35c45c817f92cfd8aaecc2eb1513098d99d9 Mon Sep 17 00:00:00 2001 From: Yun Lai <ylai@squareup.com> Date: Thu, 7 Jul 2022 23:30:22 +1000 Subject: [PATCH] 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> --- lib/modules/versioning/api.ts | 2 + lib/modules/versioning/hermit/index.spec.ts | 196 ++++++++++++++++++++ lib/modules/versioning/hermit/index.ts | 136 ++++++++++++++ lib/modules/versioning/hermit/readme.md | 13 ++ lib/modules/versioning/regex/index.ts | 2 +- 5 files changed, 348 insertions(+), 1 deletion(-) create mode 100644 lib/modules/versioning/hermit/index.spec.ts create mode 100644 lib/modules/versioning/hermit/index.ts create mode 100644 lib/modules/versioning/hermit/readme.md diff --git a/lib/modules/versioning/api.ts b/lib/modules/versioning/api.ts index 93ceef3a51..83fb40fa00 100644 --- a/lib/modules/versioning/api.ts +++ b/lib/modules/versioning/api.ts @@ -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); diff --git a/lib/modules/versioning/hermit/index.spec.ts b/lib/modules/versioning/hermit/index.spec.ts new file mode 100644 index 0000000000..5de95b044f --- /dev/null +++ b/lib/modules/versioning/hermit/index.spec.ts @@ -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', + ]); + }); + }); +}); diff --git a/lib/modules/versioning/hermit/index.ts b/lib/modules/versioning/hermit/index.ts new file mode 100644 index 0000000000..ba23ece793 --- /dev/null +++ b/lib/modules/versioning/hermit/index.ts @@ -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; diff --git a/lib/modules/versioning/hermit/readme.md b/lib/modules/versioning/hermit/readme.md new file mode 100644 index 0000000000..44340dfd2f --- /dev/null +++ b/lib/modules/versioning/hermit/readme.md @@ -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. diff --git a/lib/modules/versioning/regex/index.ts b/lib/modules/versioning/regex/index.ts index c06a413a1b..e865fa62d2 100644 --- a/lib/modules/versioning/regex/index.ts +++ b/lib/modules/versioning/regex/index.ts @@ -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(); -- GitLab