diff --git a/lib/versioning/loose/generic.ts b/lib/versioning/loose/generic.ts index f916c684ebc0c519efd5a4b708745986345c5e67..55646f2b704d5267efa7c07f3ef0461674f550fc 100644 --- a/lib/versioning/loose/generic.ts +++ b/lib/versioning/loose/generic.ts @@ -1,3 +1,4 @@ +import is from '@sindresorhus/is'; import type { NewValueConfig, VersioningApi } from '../types'; export interface GenericVersion { @@ -133,7 +134,51 @@ export abstract class GenericVersioningApi< : null; } - protected abstract _compare(version: string, other: string): number; + protected _compare(version: string, other: string): number { + const left = this._parse(version); + const right = this._parse(other); + + // istanbul ignore if + if (!(left && right)) { + return 1; + } + + // support variable length compare + const length = Math.max(left.release.length, right.release.length); + for (let i = 0; i < length; i += 1) { + // 2.1 and 2.1.0 are equivalent + const part1 = left.release[i] ?? 0; + const part2 = right.release[i] ?? 0; + if (part1 !== part2) { + return part1 - part2; + } + } + + if ( + is.nonEmptyString(left.prerelease) && + is.nonEmptyString(right.prerelease) + ) { + const pre = left.prerelease.localeCompare(right.prerelease); + + if (pre !== 0) { + return pre; + } + } else if (is.nonEmptyString(left.prerelease)) { + return -1; + } else if (is.nonEmptyString(right.prerelease)) { + return 1; + } + + return this._compareOther(left, right); + } + + /* + * virtual + */ + // eslint-disable-next-line class-methods-use-this + protected _compareOther(_left: T, _right: T): number { + return 0; + } protected abstract _parse(version: string): T | null; diff --git a/lib/versioning/regex/index.spec.ts b/lib/versioning/regex/index.spec.ts index 8e5c93a97c7dde0797509606ed0d4d93308c2728..09c57ac1d3360a6be5f43811f2944d378f641583 100644 --- a/lib/versioning/regex/index.spec.ts +++ b/lib/versioning/regex/index.spec.ts @@ -396,4 +396,33 @@ describe('regex', () => { expect(regex.matches('1.2.4a1-foo', '1.2.3a1-bar')).toBe(false); }); }); + + describe('Supported 4th number as build', () => { + it('supports Bitnami docker versioning', () => { + const re = get( + 'regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(:?-(?<compatibility>.*-r)(?<build>\\d+))?$' + ); + + expect(re.isValid('12.7.0-debian-10-r69')).toBe(true); + expect(re.isValid('12.7.0-debian-10-r100')).toBe(true); + + expect( + re.isCompatible('12.7.0-debian-10-r69', '12.7.0-debian-10-r100') + ).toBe(true); + + expect( + re.isGreaterThan('12.7.0-debian-10-r69', '12.7.0-debian-10-r100') + ).toBe(false); + expect( + re.isGreaterThan('12.7.0-debian-10-r169', '12.7.0-debian-10-r100') + ).toBe(true); + + expect(re.matches('12.7.0-debian-9-r69', '12.7.0-debian-10-r69')).toBe( + true + ); + expect(re.matches('12.7.0-debian-9-r69', '12.7.0-debian-10-r68')).toBe( + true + ); + }); + }); }); diff --git a/lib/versioning/regex/index.ts b/lib/versioning/regex/index.ts index 9257a41d0a09851338a7d3f68dc5dcdfa67fc3f9..43252b36ae101f758e5987e50f08091c47edbb8a 100644 --- a/lib/versioning/regex/index.ts +++ b/lib/versioning/regex/index.ts @@ -1,4 +1,5 @@ -import { compare, ltr, maxSatisfying, minSatisfying, satisfies } from 'semver'; +import is from '@sindresorhus/is'; +import { ltr, maxSatisfying, minSatisfying, satisfies } from 'semver'; import { CONFIG_VALIDATION } from '../../constants/error-messages'; import { regEx } from '../../util/regex'; import { GenericVersion, GenericVersioningApi } from '../loose/generic'; @@ -10,8 +11,6 @@ export const urls = []; export const supportsRanges = false; export interface RegExpVersion extends GenericVersion { - /** prereleases are treated in the standard semver manner, if present */ - prerelease: string; /** * compatibility, if present, are treated as a compatibility layer: we will * never try to update to a version with a different compatibility. @@ -22,7 +21,7 @@ export interface RegExpVersion extends GenericVersion { // convenience method for passing a Version object into any semver.* method. function asSemver(version: RegExpVersion): string { let vstring = `${version.release[0]}.${version.release[1]}.${version.release[2]}`; - if (typeof version.prerelease !== 'undefined') { + if (is.nonEmptyString(version.prerelease)) { vstring += `-${version.prerelease}`; } return vstring; @@ -38,6 +37,8 @@ export class RegExpVersioningApi extends GenericVersioningApi<RegExpVersion> { // RegExp('^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(-(?<compatibility>.*))?$') // * matches the versioning approach used by the Python images on DockerHub: // 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; constructor(new_config: string) { @@ -66,13 +67,6 @@ export class RegExpVersioningApi extends GenericVersioningApi<RegExpVersion> { this._config = regEx(new_config); } - protected _compare(version: string, other: string): number { - return compare( - asSemver(this._parse(version)), - asSemver(this._parse(other)) - ); - } - // convenience method for passing a string into a Version given current config. protected _parse(version: string): RegExpVersion | null { const match = this._config.exec(version); @@ -81,12 +75,18 @@ export class RegExpVersioningApi extends GenericVersioningApi<RegExpVersion> { } const groups = match.groups; + const release = [ + typeof groups.major === 'undefined' ? 0 : Number(groups.major), + typeof groups.minor === 'undefined' ? 0 : Number(groups.minor), + typeof groups.patch === 'undefined' ? 0 : Number(groups.patch), + ]; + + if (groups.build) { + release.push(Number(groups.build)); + } + return { - release: [ - typeof groups.major === 'undefined' ? 0 : Number(groups.major), - typeof groups.minor === 'undefined' ? 0 : Number(groups.minor), - typeof groups.patch === 'undefined' ? 0 : Number(groups.patch), - ], + release, prerelease: groups.prerelease, compatibility: groups.compatibility, }; @@ -98,10 +98,6 @@ export class RegExpVersioningApi extends GenericVersioningApi<RegExpVersion> { ); } - isStable(version: string): boolean { - return typeof this._parse(version).prerelease === 'undefined'; - } - isLessThanRange(version: string, range: string): boolean { return ltr(asSemver(this._parse(version)), asSemver(this._parse(range))); } diff --git a/lib/versioning/regex/readme.md b/lib/versioning/regex/readme.md index 6ec07e07c42d286570304d4c6b2b4ce037e2f350..92122b42eab1f5f24b374f2ba68b348bf9e85104 100644 --- a/lib/versioning/regex/readme.md +++ b/lib/versioning/regex/readme.md @@ -34,3 +34,17 @@ Here is another example, this time for handling `python` Docker images, which us ] } ``` + +Here is another example, this time for handling Bitnami Docker images, which use build indicators as well as version suffixes for compatibility: + +```json +{ + "packageRules": [ + { + "matchDatasources": ["docker"], + "matchPackagePrefixes": ["bitnami/"], + "versioning": "regex:^(?<major>\\d+)\\.(?<minor>\\d+)\\.(?<patch>\\d+)(:?-(?<compatibility>.*-r)(?<build>\\d+))?$" + } + ] +} +```