Files
renovate/lib/modules/versioning/apk/readme.md
Jamie Tanna 629740f827 fix(versioning/apk): correctly implement APK's version constraint operators (#45692)
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>
2026-09-08 17:54:56 +00:00

50 lines
3.0 KiB
Markdown

Alpine Package Keeper (APK) versioning is used for packages served from [APK repositories](https://wiki.alpinelinux.org/wiki/Repositories), such as the official Alpine Linux repositories or the [Wolfi APK package repository](https://packages.wolfi.dev/os)
This implementation follows the [Alpine Linux Package versions](https://wiki.alpinelinux.org/wiki/Package_policies#Package_versions) for version formatting.
Versions are similar to other Linux distributions, e.g. 3.2.1-r0
- The first segment follows semantic versioning
- Alpha, release candidates (\_rc2), etc are prefixed with an underscore, not a hyphen.
- Subsequent package versions are -r0, -r1, and so on; the number is from $pkgver.
- Subsequent package fixes are \_p0, \_p1 (typically seen if not using major.minor.patch, e.g. 6.5_p20250503-r0)
### Version Format Examples
- `2.39.0-r0` - Standard version with release number
- `2.39.0_rc1-r0` - Release candidate (pre-release)
- `6.5_p20250503-r0` - Package fix with date-based patch
### Pre-release Handling
- `_rc` patterns (e.g., `_rc1`, `_rc2`) are treated as pre-release identifiers
- `_p` patterns (e.g., `_p20250503`) are treated as part of the version number
### Version constraints
Constraints follow [`apk-world(5)`](https://gitlab.alpinelinux.org/alpine/apk-tools/-/blob/master/doc/apk-world.5.scd), which builds the operator from its characters, so the characters may be given in any order and may repeat.
This means `~`, `=~` and `~=` all mean the same thing.
| Operator | Meaning |
| -------- | ---------------------------- |
| _(none)_ | exact version |
| `=` | exact version |
| `<` | less than |
| `<=` | less than or equal |
| `>` | greater than |
| `>=` | greater than or equal |
| `~` | prefix match |
| `>~` | greater than or prefix match |
| `<~` | less than or prefix match |
A prefix match compares token by token rather than by string, so `~1.6` matches `1.6`, `1.6.0_pre1`, `1.6.0`, `1.6.5` and `1.6.9_p1`, but not `1.60`.
Because a revision is a token of its own, `~8.12.1` matches every `8.12.1-rN`, which is how Wolfi and Chainguard images commonly pin their packages.
Renovate keeps the precision a prefix constraint was written with, so `~8.12.1` becomes `~8.13.0` rather than `~8.13.0-r0`, and `~8.12` becomes `~8.13`.
With `rangeStrategy=bump`, Renovate raises a lower bound which includes its boundary to the new version, so `>=8.12.1` becomes `>=8.13.0-r0` and `>~8.12` becomes `>~8.13.0-r0`.
A bare `>` is left alone, as `>8.13.0-r0` would exclude the very version it was bumped to, and `<` and `<=` are upper bounds so there is nothing to raise.
Otherwise Renovate does not propose a replacement for the `<`, `<=`, `>`, `>=`, `>~` and `<~` operators, as there is no single obvious new bound for them.
The `><` operator constrains a package to an identity hash rather than to a version, so it is not a version constraint Renovate can resolve.