mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-27 07:50:47 +00:00
629740f827
As part of future changes, we're introducing a means to parse
Dockerfiles for any `apk add` commands, so we can propose updates to any
packages using the APK datasource.
Before we do this, we need to correct a couple of bugs in our
implementation of the APK versioning scheme, so we can handle i.e.
apk add curl=~8.12.1
When the `apk` CLI builds a version constraint, it `OR`s one flag per
operator character[0].
For instance, `~` sets both "fuzzy" and "equal".
These operator characters may be in any order and may repeat, so `~`,
`=~` and `~=` are all the same operator, and `>>` and `>` are the same.
To correct our parsing, we should take this into account, rather than
using known patterns.
When we perform a fuzzy match, this is performed over version _tokens_
not the version itself, which means that:
- `~1.6` matches `1.6`, `1.6.0_pre1`, `1.6.0`, `1.6.5` and `1.6.9_p1`
- `~1.6` does not match `1.60`
- `~8.12.1` matches every `8.12.1-rN` i.e. `8.12.1-r1` and `8.12.1-r100`
While doing this, we also fix related issues:
- `~` previously treated this as an npm-style tilde ("same major and
minor, and greater than or equal"),
- admitted fuzzy versions that `apk` rejects - `~2.39.0-r0` should not have
matched `2.39.1-r0`
- rejected fuzzy versions that `apk` accepts - `~2.39.0` should have
matched `2.39.0-r1`
- `=~` and `~=` were not allowed
- `isSingleVersion('=~1.2.3')` was true, so a range looked like a pin
- `isLessThanRange` compared against the raw range, so `_parse` failed on
the operator and it always returned false
- `minSatisfyingVersion` was inherited, so it only ever found an exact
match
- `getNewValue` dropped the operator entirely, rewriting `~8.12.1` to a
bare version
We also set `supportsRanges`, which we'd forgotten to set despite `~`
and `>=` being ranges, and note that we support `pin`, `replace` and
`bump` strategies.
When using `getNewValue`, we now keep the precision the constraint was
written with, as otherwise we'd quietly narrow what the user asked for:
- `~8.12.1` becomes `~8.13.0` (previously `~8.13.0-r0`)
- `~8.12` becomes `~8.13`
- `~8.12.1-r0` becomes `~8.13.0-r2` (with a new revision number)
For `bump`, we raise a lower bound which includes its boundary, as with
`npm` and `pep440`, so:
- `>=8.12.1` becomes `>=8.13.0`
- `>~8.12` becomes `>~8.13`
Otherwise we return `null`, logging which constraint we couldn't handle
in the `bump` case, as `npm` does:
- `>` on its own can't be bumped, as `>8.13.0` would exclude the version
we bump to
- `<` and `<=` are upper bounds, so there's no lower bound to raise
- for `replace`, there's no single obvious new bound for `<`, `<=`, `>`,
`>=`, `>~` or `<~`, and a bound which already admits the new version
doesn't need one
- a prefix constraint whose version doesn't parse, i.e. `~foo`, which
would otherwise throw
`><` constrains a package to an identity hash rather than to a version,
so it's rejected outright.
Additionally, `isLessThanRange` and `matches` weren't agreeing on how to
resolve a fuzzy constraint.
For instance, `_compare` ranks `2.39.0_git20240101` below `2.39.0`, but
`~2.39.0` matches it, this would lead to a rollback of sorts.
Instead, we can ignore the `_git` and `_p` suffixes, which are common in
Alpine and Wolfi packages.
[0]: https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/44dcdfc255c26a4e19a4a27f66a54169cce5ca72/src/version.c
Co-authored-by: Claude Opus 5 <jamie.tanna+claude-code@mend.io>
589 lines
17 KiB
TypeScript
589 lines
17 KiB
TypeScript
import { isTruthy } from '@sindresorhus/is';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import type { RangeStrategy } from '../../../types/versioning.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { GenericVersioningApi } from '../generic.ts';
|
|
import type { NewValueConfig, VersioningApi } from '../types.ts';
|
|
import type { ApkVersion } from './types.ts';
|
|
|
|
export const id = 'apk';
|
|
export const displayName = 'Alpine Package Keeper (APK)';
|
|
export const urls = [
|
|
'[Alpine Linux package policies](https://wiki.alpinelinux.org/wiki/Package_policies)',
|
|
'[Alpine Package Keeper - Package pinning](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper#Package_pinning)',
|
|
];
|
|
export const supportsRanges = true;
|
|
export const supportedRangeStrategies: RangeStrategy[] = [
|
|
'bump',
|
|
'pin',
|
|
'replace',
|
|
];
|
|
|
|
/**
|
|
* APK constraint operators, as the bitmask `apk` builds them into.
|
|
*
|
|
* `apk` ORs one flag per operator character, so the characters may appear in any order and may repeat - `~`, `=~` and `~=` all mean the same thing.
|
|
*
|
|
* Based on `GPL-2.0` code in https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/master/src/version.c
|
|
*/
|
|
const OP_LESS = 1;
|
|
const OP_GREATER = 2;
|
|
const OP_EQUAL = 4;
|
|
const OP_FUZZY = 8;
|
|
|
|
/** A prefix match, e.g. `~1.6` - `apk` treats `~` as fuzzy /and/ equal */
|
|
const OP_PREFIX = OP_FUZZY | OP_EQUAL;
|
|
|
|
/** `><` constrains a package to an identity hash rather than to a version */
|
|
const OP_CHECKSUM = OP_LESS | OP_GREATER;
|
|
|
|
interface ApkConstraint {
|
|
/** the operator as written, so that an update can spell it the same way */
|
|
operator: string;
|
|
mask: number;
|
|
version: string;
|
|
}
|
|
|
|
const constraintRegex = regEx(/^(?<operator>[<>=~]*)(?<version>[^<>=~].*)$/);
|
|
|
|
function operatorMask(operator: string): number | null {
|
|
let mask = 0;
|
|
for (const char of operator) {
|
|
if (char === '<') {
|
|
mask |= OP_LESS;
|
|
} else if (char === '>') {
|
|
mask |= OP_GREATER;
|
|
} else if (char === '=') {
|
|
mask |= OP_EQUAL;
|
|
} else {
|
|
mask |= OP_PREFIX;
|
|
}
|
|
}
|
|
// an absent operator constrains to an exact version, the same as `=`
|
|
if (mask === 0) {
|
|
return OP_EQUAL;
|
|
}
|
|
// an identity hash is not a version, so there is nothing to look up
|
|
return mask === OP_CHECKSUM ? null : mask;
|
|
}
|
|
|
|
/** Splits a constraint such as `~8.12.1` into its operator and version */
|
|
function parseConstraint(input: string): ApkConstraint | null {
|
|
if (!input) {
|
|
return null;
|
|
}
|
|
const groups = constraintRegex.exec(input)?.groups;
|
|
if (!groups) {
|
|
return null;
|
|
}
|
|
const mask = operatorMask(groups.operator);
|
|
if (mask === null) {
|
|
return null;
|
|
}
|
|
return { operator: groups.operator, mask, version: groups.version };
|
|
}
|
|
|
|
// Regex with named capture groups for APK version parsing
|
|
const versionRegex = regEx(
|
|
/^v?(?<major>[0-9]+)(?:\.(?<minor>[0-9]+))?(?:\.(?<patch>[0-9]+))?(?<extra>(?:\.[0-9]+)*)(?<letter>[a-z])?(?:(?<prereleaseType>_alpha|_beta|_pre|_rc)(?<prereleaseNum>[0-9]*))?(?:(?<packageFixType>_cvs|_svn|_git|_hg|_p)(?<packageFixNum>[0-9]*))?(?:-r(?<releaseNum>[0-9]+))?$/,
|
|
);
|
|
|
|
// Regex for splitting version strings into alphanumeric parts
|
|
const alphaNumRegex = regEx(/(?:[a-zA-Z]+)|(?:\d+)/g);
|
|
|
|
const revisionRegex = regEx(/-r\d+$/);
|
|
|
|
/**
|
|
* The kinds of token an APK version is made of, in the order `apk` reads them.
|
|
*
|
|
* A prefix match compares tokens pairwise, so a token's kind has to be part of the comparison - `1.6_rc1` is not a prefix of `1.6.0`, even though both start with `1.6`.
|
|
*/
|
|
const TOKEN_DIGIT = 0;
|
|
const TOKEN_LETTER = 1;
|
|
const TOKEN_PRERELEASE = 2;
|
|
const TOKEN_PACKAGE_FIX = 3;
|
|
const TOKEN_REVISION = 4;
|
|
|
|
interface ApkToken {
|
|
kind: number;
|
|
value: string | number;
|
|
}
|
|
|
|
/** The numeric components of a version, e.g. `[1, 6, 0]` for `1.6.0_pre1` */
|
|
function numericParts(groups: Record<string, string>): number[] {
|
|
const { major, minor, patch, extra } = groups;
|
|
const parts = [parseInt(major, 10)];
|
|
for (const part of [minor, patch]) {
|
|
if (part) {
|
|
parts.push(parseInt(part, 10));
|
|
}
|
|
}
|
|
if (extra) {
|
|
parts.push(...extra.substring(1).split('.').filter(isTruthy).map(Number));
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
function tokenize(groups: Record<string, string>): ApkToken[] {
|
|
const {
|
|
letter,
|
|
prereleaseType,
|
|
prereleaseNum,
|
|
packageFixType,
|
|
packageFixNum,
|
|
releaseNum,
|
|
} = groups;
|
|
|
|
const tokens: ApkToken[] = numericParts(groups).map((value) => ({
|
|
kind: TOKEN_DIGIT,
|
|
value,
|
|
}));
|
|
if (letter) {
|
|
tokens.push({ kind: TOKEN_LETTER, value: letter });
|
|
}
|
|
if (prereleaseType) {
|
|
tokens.push({
|
|
kind: TOKEN_PRERELEASE,
|
|
value: prereleaseType + prereleaseNum,
|
|
});
|
|
}
|
|
if (packageFixType) {
|
|
tokens.push({
|
|
kind: TOKEN_PACKAGE_FIX,
|
|
value: packageFixType + packageFixNum,
|
|
});
|
|
}
|
|
if (releaseNum) {
|
|
tokens.push({ kind: TOKEN_REVISION, value: parseInt(releaseNum, 10) });
|
|
}
|
|
return tokens;
|
|
}
|
|
|
|
/**
|
|
* Whether `constraintVersion` is a token-wise prefix of `version`, which is what `apk` calls a fuzzy match.
|
|
*
|
|
* `~1.6` matches `1.6`, `1.6.0_pre1`, `1.6.0`, `1.6.5` and `1.6.9_p1`, but not `1.60` - the comparison is per token, not on the raw string.
|
|
*/
|
|
function isPrefixMatch(version: string, constraintVersion: string): boolean {
|
|
const versionGroups = versionRegex.exec(version)?.groups;
|
|
const constraintGroups = versionRegex.exec(constraintVersion)?.groups;
|
|
/* v8 ignore next -- `matches` parses both versions before calling this */
|
|
if (!versionGroups || !constraintGroups) {
|
|
return false;
|
|
}
|
|
const versionTokens = tokenize(versionGroups);
|
|
const constraintTokens = tokenize(constraintGroups);
|
|
// a constraint with more tokens than the version cannot be its prefix
|
|
if (constraintTokens.length > versionTokens.length) {
|
|
return false;
|
|
}
|
|
return constraintTokens.every(
|
|
(token, i) =>
|
|
token.kind === versionTokens[i].kind &&
|
|
token.value === versionTokens[i].value,
|
|
);
|
|
}
|
|
|
|
/** Drops the revision when the constraint was written without one */
|
|
function withConstraintRevision(
|
|
newVersion: string,
|
|
constraintVersion: string,
|
|
): string {
|
|
if (revisionRegex.test(constraintVersion)) {
|
|
return newVersion;
|
|
}
|
|
return newVersion.replace(revisionRegex, '');
|
|
}
|
|
|
|
class ApkVersioningApi extends GenericVersioningApi {
|
|
/**
|
|
* Parse APK version format using apko's version parsing patterns
|
|
* Based on `Apache-2.0` code in https://github.com/chainguard-dev/apko/blob/v0.30.35/pkg/apk/apk/version.go
|
|
*/
|
|
protected _parse(version: string): ApkVersion | null {
|
|
const match = versionRegex.exec(version);
|
|
if (!match?.groups) {
|
|
return null;
|
|
}
|
|
|
|
const {
|
|
major,
|
|
minor,
|
|
patch,
|
|
extra,
|
|
letter,
|
|
prereleaseType,
|
|
prereleaseNum,
|
|
packageFixType,
|
|
packageFixNum,
|
|
releaseNum,
|
|
} = match.groups;
|
|
|
|
const packageFixFull = packageFixType ? packageFixType + packageFixNum : '';
|
|
|
|
const minorPatchStr =
|
|
(minor ? `.${minor}` : '') + (patch ? `.${patch}` : '') + extra;
|
|
|
|
// For version comparison, we only include the base version + package fix, not prerelease
|
|
const versionStr = major + minorPatchStr + (letter ?? '') + packageFixFull;
|
|
|
|
let prerelease: string | undefined;
|
|
if (prereleaseType) {
|
|
prerelease = prereleaseType.substring(1) + prereleaseNum;
|
|
}
|
|
|
|
// Extract numeric parts for major/minor/patch/extra
|
|
const release = [parseInt(major, 10)];
|
|
if (minor) {
|
|
release.push(parseInt(minor, 10));
|
|
}
|
|
if (patch) {
|
|
release.push(parseInt(patch, 10));
|
|
}
|
|
// Handle any additional version parts (e.g., 1.2.3.4.5)
|
|
if (extra) {
|
|
const extraParts = extra
|
|
.substring(1)
|
|
.split('.')
|
|
.filter(isTruthy)
|
|
.map(Number);
|
|
release.push(...extraParts);
|
|
}
|
|
|
|
// Extract release string (just the number part)
|
|
const releaseString = releaseNum ?? '';
|
|
|
|
return {
|
|
version: versionStr,
|
|
releaseString,
|
|
release,
|
|
prerelease,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Compare two APK versions according to Alpine Linux rules
|
|
*/
|
|
protected override _compare(version: string, other: string): number {
|
|
const parsed1 = this._parse(version);
|
|
const parsed2 = this._parse(other);
|
|
|
|
if (!(parsed1 && parsed2)) {
|
|
return 1;
|
|
}
|
|
|
|
// Compare version parts (without release number)
|
|
const versionCompare = this._compareVersionParts(
|
|
parsed1.version,
|
|
parsed2.version,
|
|
);
|
|
if (versionCompare !== 0) {
|
|
return versionCompare;
|
|
}
|
|
|
|
// Compare prerelease identifiers
|
|
const prerelease1 = parsed1.prerelease;
|
|
const prerelease2 = parsed2.prerelease;
|
|
|
|
if (prerelease1 || prerelease2) {
|
|
if (!prerelease1) {
|
|
return -1; // Version without prerelease < version with prerelease (APK-specific)
|
|
}
|
|
if (!prerelease2) {
|
|
return 1; // Version with prerelease > version without (APK-specific)
|
|
}
|
|
// Both have prerelease identifiers, compare them
|
|
const prereleaseCompare = prerelease1.localeCompare(prerelease2);
|
|
if (prereleaseCompare !== 0) {
|
|
return prereleaseCompare;
|
|
}
|
|
}
|
|
|
|
// Compare release numbers
|
|
const release1 = parsed1.releaseString || '';
|
|
const release2 = parsed2.releaseString || '';
|
|
|
|
// If one has a release number and the other doesn't, the one with release number is greater
|
|
if (release1 && !release2) {
|
|
return 1;
|
|
}
|
|
if (!release1 && release2) {
|
|
return -1;
|
|
}
|
|
|
|
// If both have release numbers or neither has release numbers, compare them
|
|
const releaseCompare = this._compareVersionParts(
|
|
release1 || '0',
|
|
release2 || '0',
|
|
);
|
|
return releaseCompare;
|
|
}
|
|
|
|
/**
|
|
* Compare version parts using APK's version comparison rules
|
|
*/
|
|
private _compareVersionParts(v1: string, v2: string): number {
|
|
if (v1 === v2) {
|
|
return 0;
|
|
}
|
|
const matchesv1 = v1.match(alphaNumRegex)!;
|
|
const matchesv2 = v2.match(alphaNumRegex)!;
|
|
const matches = Math.min(matchesv1.length, matchesv2.length);
|
|
|
|
for (let i = 0; i < matches; i++) {
|
|
const matchv1 = matchesv1[i];
|
|
const matchv2 = matchesv2[i];
|
|
|
|
if (matchv1 && regEx(/^\d+$/).test(matchv1)) {
|
|
if (!matchv2 || !regEx(/^\d+$/).test(matchv2)) {
|
|
return 1;
|
|
}
|
|
const num1 = parseInt(matchv1, 10);
|
|
const num2 = parseInt(matchv2, 10);
|
|
if (num1 !== num2) {
|
|
return num1 - num2;
|
|
}
|
|
} else if (matchv2 && regEx(/^\d+$/).test(matchv2)) {
|
|
return -1;
|
|
} else if (matchv1 !== matchv2) {
|
|
return matchv1.localeCompare(matchv2);
|
|
}
|
|
}
|
|
|
|
if (matchesv1.length !== matchesv2.length) {
|
|
const maxLength = Math.max(matchesv1.length, matchesv2.length);
|
|
for (let i = matches; i < maxLength; i++) {
|
|
const matchv1 = matchesv1[i];
|
|
const matchv2 = matchesv2[i];
|
|
|
|
if (matchv1 && regEx(/^\d+$/).test(matchv1)) {
|
|
return 1;
|
|
}
|
|
if (matchv2 && regEx(/^\d+$/).test(matchv2)) {
|
|
return -1;
|
|
}
|
|
if (matchv1) {
|
|
return -1;
|
|
}
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
override isValid(input: string): boolean {
|
|
const constraint = parseConstraint(input);
|
|
return !!constraint && this._parse(constraint.version) !== null;
|
|
}
|
|
|
|
/** A constraint is not itself a version, however valid it is */
|
|
override isVersion(input: string): boolean {
|
|
const constraint = parseConstraint(input);
|
|
return (
|
|
!!constraint &&
|
|
constraint.operator === '' &&
|
|
this._parse(constraint.version) !== null
|
|
);
|
|
}
|
|
|
|
override isSingleVersion(input: string): boolean {
|
|
const constraint = parseConstraint(input);
|
|
return (
|
|
!!constraint &&
|
|
constraint.mask === OP_EQUAL &&
|
|
this._parse(constraint.version) !== null
|
|
);
|
|
}
|
|
|
|
override isStable(input: string): boolean {
|
|
const constraint = parseConstraint(input);
|
|
if (!constraint) {
|
|
return false;
|
|
}
|
|
const parsed = this._parse(constraint.version);
|
|
// Consider versions without prerelease identifiers as stable
|
|
return !!parsed && !parsed.prerelease;
|
|
}
|
|
|
|
/**
|
|
* Whether `version` satisfies the constraint, following `apk_version_match`: compare the two, fuzzily when the operator asks for it, then check the result against the operator's mask.
|
|
*/
|
|
override matches(version: string, range: string): boolean {
|
|
const constraint = parseConstraint(range);
|
|
if (
|
|
!constraint ||
|
|
!this._parse(version) ||
|
|
!this._parse(constraint.version)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
if (
|
|
(constraint.mask & OP_FUZZY) !== 0 &&
|
|
isPrefixMatch(version, constraint.version)
|
|
) {
|
|
return (constraint.mask & OP_EQUAL) !== 0;
|
|
}
|
|
|
|
const compared = this._compare(version, constraint.version);
|
|
if (compared < 0) {
|
|
return (constraint.mask & OP_LESS) !== 0;
|
|
}
|
|
if (compared > 0) {
|
|
return (constraint.mask & OP_GREATER) !== 0;
|
|
}
|
|
return (constraint.mask & OP_EQUAL) !== 0;
|
|
}
|
|
|
|
override isLessThanRange(version: string, range: string): boolean {
|
|
const constraint = parseConstraint(range);
|
|
if (
|
|
!constraint ||
|
|
!this._parse(version) ||
|
|
!this._parse(constraint.version)
|
|
) {
|
|
return false;
|
|
}
|
|
// a range with no lower bound has nothing below it
|
|
if ((constraint.mask & OP_LESS) !== 0) {
|
|
return false;
|
|
}
|
|
// a fuzzy match satisfies the constraint, so it cannot be below it, even though `_compare` ranks `2.39.0_git20240101` below `2.39.0`
|
|
if (
|
|
(constraint.mask & OP_FUZZY) !== 0 &&
|
|
isPrefixMatch(version, constraint.version)
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
const compared = this._compare(version, constraint.version);
|
|
if (compared !== 0) {
|
|
return compared < 0;
|
|
}
|
|
// equal to the boundary, so below the range only when it is excluded
|
|
return (constraint.mask & OP_EQUAL) === 0;
|
|
}
|
|
|
|
override getSatisfyingVersion(
|
|
versions: string[],
|
|
range: string,
|
|
): string | null {
|
|
const satisfying = versions.filter((version) =>
|
|
this.matches(version, range),
|
|
);
|
|
if (!satisfying.length) {
|
|
return null;
|
|
}
|
|
return satisfying.sort((a, b) => this.sortVersions(b, a))[0];
|
|
}
|
|
|
|
override minSatisfyingVersion(
|
|
versions: string[],
|
|
range: string,
|
|
): string | null {
|
|
const satisfying = versions.filter((version) =>
|
|
this.matches(version, range),
|
|
);
|
|
if (!satisfying.length) {
|
|
return null;
|
|
}
|
|
return satisfying.sort((a, b) => this.sortVersions(a, b))[0];
|
|
}
|
|
|
|
override getMajor(input: string): number | null {
|
|
const constraint = parseConstraint(input);
|
|
return constraint
|
|
? (this._parse(constraint.version)?.release[0] ?? null)
|
|
: null;
|
|
}
|
|
|
|
override getMinor(input: string): number | null {
|
|
const constraint = parseConstraint(input);
|
|
return constraint
|
|
? (this._parse(constraint.version)?.release[1] ?? null)
|
|
: null;
|
|
}
|
|
|
|
override getPatch(input: string): number | null {
|
|
const constraint = parseConstraint(input);
|
|
return constraint
|
|
? (this._parse(constraint.version)?.release[2] ?? null)
|
|
: null;
|
|
}
|
|
|
|
override getNewValue({
|
|
currentValue,
|
|
rangeStrategy,
|
|
newVersion,
|
|
}: NewValueConfig): string | null {
|
|
const constraint = parseConstraint(currentValue);
|
|
if (!constraint) {
|
|
return null;
|
|
}
|
|
|
|
const { operator, mask, version } = constraint;
|
|
|
|
// a constraint we cannot parse the version of is one we cannot rewrite
|
|
const parsed = this._parse(version);
|
|
if (!parsed) {
|
|
return null;
|
|
}
|
|
|
|
if (rangeStrategy === 'pin') {
|
|
return newVersion;
|
|
}
|
|
|
|
// an exact constraint identifies one version, so only the revision is written to the precision the user chose
|
|
if (mask === OP_EQUAL) {
|
|
return `${operator}${withConstraintRevision(newVersion, version)}`;
|
|
}
|
|
|
|
// `bump` raises a lower bound to the new version, which only makes sense when the bound includes its boundary - bumping `>8.12.1` to `>8.13.0` would exclude the very version it was bumped to
|
|
if (
|
|
rangeStrategy === 'bump' &&
|
|
(mask & OP_GREATER) !== 0 &&
|
|
(mask & OP_EQUAL) !== 0
|
|
) {
|
|
// the whole point of the bump is to raise the bound, so unlike a prefix constraint we write `newVersion` in full rather than to the precision it was written with
|
|
return `${operator}${newVersion}`;
|
|
}
|
|
|
|
// a prefix constraint keeps the precision it was written with, so `~8.12.1` becomes `~8.13.0` rather than `~8.13.0-r0`, and stays revision-agnostic
|
|
if (mask === OP_PREFIX) {
|
|
const currentParts = parsed.release;
|
|
const newParts = this._parse(newVersion)?.release;
|
|
/* v8 ignore next -- newVersion comes from the datasource, so it parses */
|
|
if (!newParts) {
|
|
return null;
|
|
}
|
|
if (currentParts.length < newParts.length) {
|
|
return `${operator}${newParts.slice(0, currentParts.length).join('.')}`;
|
|
}
|
|
return `${operator}${withConstraintRevision(newVersion, version)}`;
|
|
}
|
|
|
|
// `>` cannot be bumped, and an upper bound is not a bound to bump
|
|
if (rangeStrategy === 'bump') {
|
|
logger.debug(
|
|
`Unsupported range type for rangeStrategy=bump: ${currentValue}`,
|
|
);
|
|
}
|
|
|
|
// there is no single obvious replacement for `>`, `>=`, `<`, `<=`, `>~` or `<~`, and a bound which already admits the new version needs no change
|
|
return null;
|
|
}
|
|
|
|
// Override to provide clean version for PR titles and display
|
|
override sortVersions(a: string, b: string): number {
|
|
// Strip = prefix if present for comparison
|
|
const cleanA = a.replace(regEx(/^=/), '');
|
|
const cleanB = b.replace(regEx(/^=/), '');
|
|
return super.sortVersions(cleanA, cleanB);
|
|
}
|
|
}
|
|
|
|
export const api: VersioningApi = new ApkVersioningApi();
|
|
|
|
export default api;
|