Add support for pep440 version identifiers (#353)

Fixes: #264
This commit is contained in:
Kevin Stillhammer 2025-03-30 18:00:56 +02:00 committed by GitHub
parent 2d49baf2b6
commit 794ea9455c
WARNING! Although there is a key with this ID in the database it does not verify this commit! This commit is SUSPICIOUS.
GPG key ID: B5690EEEBB952194
6 changed files with 860 additions and 6 deletions

View file

@ -82,6 +82,23 @@ jobs:
env:
UV_VERSION: ${{ steps.setup-uv.outputs.uv-version }}
test-pep440-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install version 0.4.30
id: setup-uv
uses: ./
with:
version: ">=0.4.25,<0.5"
- name: Correct version gets installed
run: |
if [ "$UV_VERSION" != "0.4.30" ]; then
exit 1
fi
env:
UV_VERSION: ${{ steps.setup-uv.outputs.uv-version }}
test-pyproject-file-version:
runs-on: ubuntu-latest
steps:
@ -475,6 +492,7 @@ jobs:
- test-default-version
- test-specific-version
- test-semver-range
- test-pep440-version
- test-pyproject-file-version
- test-malformed-pyproject-file-fallback
- test-uv-file-version

View file

@ -66,6 +66,7 @@ For an example workflow, see
### Install a version by supplying a semver range
You can specify a [semver range](https://github.com/npm/node-semver?tab=readme-ov-file#ranges)
or [pep440 identifier](https://peps.python.org/pep-0440/#version-specifiers)
to install the latest version that satisfies the range.
```yaml
@ -82,6 +83,13 @@ to install the latest version that satisfies the range.
version: "0.4.x"
```
```yaml
- name: Install a pep440-specifier-satisfying version of uv
uses: astral-sh/setup-uv@v5
with:
version: ">=0.4.25,<0.5"
```
### Install a required-version
You can specify a [required-version](https://docs.astral.sh/uv/reference/settings/#required-version)

795
dist/setup/index.js generated vendored
View file

@ -76630,6 +76630,783 @@ class ReflectionTypeCheck {
exports.ReflectionTypeCheck = ReflectionTypeCheck;
/***/ }),
/***/ 63297:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const { valid, clean, explain, parse } = __nccwpck_require__(99961);
const { lt, le, eq, ne, ge, gt, compare, rcompare } = __nccwpck_require__(99469);
const {
filter,
maxSatisfying,
minSatisfying,
RANGE_PATTERN,
satisfies,
validRange,
} = __nccwpck_require__(23185);
const { major, minor, patch, inc } = __nccwpck_require__(6829);
module.exports = {
// version
valid,
clean,
explain,
parse,
// operator
lt,
le,
lte: le,
eq,
ne,
neq: ne,
ge,
gte: ge,
gt,
compare,
rcompare,
// range
filter,
maxSatisfying,
minSatisfying,
RANGE_PATTERN,
satisfies,
validRange,
// semantic
major,
minor,
patch,
inc,
};
/***/ }),
/***/ 99469:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const { parse } = __nccwpck_require__(99961);
module.exports = {
compare,
rcompare,
lt,
le,
eq,
ne,
ge,
gt,
'<': lt,
'<=': le,
'==': eq,
'!=': ne,
'>=': ge,
'>': gt,
'===': arbitrary,
};
function lt(version, other) {
return compare(version, other) < 0;
}
function le(version, other) {
return compare(version, other) <= 0;
}
function eq(version, other) {
return compare(version, other) === 0;
}
function ne(version, other) {
return compare(version, other) !== 0;
}
function ge(version, other) {
return compare(version, other) >= 0;
}
function gt(version, other) {
return compare(version, other) > 0;
}
function arbitrary(version, other) {
return version.toLowerCase() === other.toLowerCase();
}
function compare(version, other) {
const parsedVersion = parse(version);
const parsedOther = parse(other);
const keyVersion = calculateKey(parsedVersion);
const keyOther = calculateKey(parsedOther);
return pyCompare(keyVersion, keyOther);
}
function rcompare(version, other) {
return -compare(version, other);
}
// this logic is buitin in python, but we need to port it to js
// see https://stackoverflow.com/a/5292332/1438522
function pyCompare(elemIn, otherIn) {
let elem = elemIn;
let other = otherIn;
if (elem === other) {
return 0;
}
if (Array.isArray(elem) !== Array.isArray(other)) {
elem = Array.isArray(elem) ? elem : [elem];
other = Array.isArray(other) ? other : [other];
}
if (Array.isArray(elem)) {
const len = Math.min(elem.length, other.length);
for (let i = 0; i < len; i += 1) {
const res = pyCompare(elem[i], other[i]);
if (res !== 0) {
return res;
}
}
return elem.length - other.length;
}
if (elem === -Infinity || other === Infinity) {
return -1;
}
if (elem === Infinity || other === -Infinity) {
return 1;
}
return elem < other ? -1 : 1;
}
function calculateKey(input) {
const { epoch } = input;
let { release, pre, post, local, dev } = input;
// When we compare a release version, we want to compare it with all of the
// trailing zeros removed. So we'll use a reverse the list, drop all the now
// leading zeros until we come to something non zero, then take the rest
// re-reverse it back into the correct order and make it a tuple and use
// that for our sorting key.
release = release.concat();
release.reverse();
while (release.length && release[0] === 0) {
release.shift();
}
release.reverse();
// We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
// We'll do this by abusing the pre segment, but we _only_ want to do this
// if there is !a pre or a post segment. If we have one of those then
// the normal sorting rules will handle this case correctly.
if (!pre && !post && dev) pre = -Infinity;
// Versions without a pre-release (except as noted above) should sort after
// those with one.
else if (!pre) pre = Infinity;
// Versions without a post segment should sort before those with one.
if (!post) post = -Infinity;
// Versions without a development segment should sort after those with one.
if (!dev) dev = Infinity;
if (!local) {
// Versions without a local segment should sort before those with one.
local = -Infinity;
} else {
// Versions with a local segment need that segment parsed to implement
// the sorting rules in PEP440.
// - Alpha numeric segments sort before numeric segments
// - Alpha numeric segments sort lexicographically
// - Numeric segments sort numerically
// - Shorter versions sort before longer versions when the prefixes
// match exactly
local = local.map((i) =>
Number.isNaN(Number(i)) ? [-Infinity, i] : [Number(i), ''],
);
}
return [epoch, release, pre, post, dev, local];
}
/***/ }),
/***/ 6829:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
const { explain, parse, stringify } = __nccwpck_require__(99961);
// those notation are borrowed from semver
module.exports = {
major,
minor,
patch,
inc,
};
function major(input) {
const version = explain(input);
if (!version) {
throw new TypeError('Invalid Version: ' + input);
}
return version.release[0];
}
function minor(input) {
const version = explain(input);
if (!version) {
throw new TypeError('Invalid Version: ' + input);
}
if (version.release.length < 2) {
return 0;
}
return version.release[1];
}
function patch(input) {
const version = explain(input);
if (!version) {
throw new TypeError('Invalid Version: ' + input);
}
if (version.release.length < 3) {
return 0;
}
return version.release[2];
}
function inc(input, release, preReleaseIdentifier) {
let identifier = preReleaseIdentifier || `a`;
const version = parse(input);
if (!version) {
return null;
}
if (
!['a', 'b', 'c', 'rc', 'alpha', 'beta', 'pre', 'preview'].includes(
identifier,
)
) {
return null;
}
switch (release) {
case 'premajor':
{
const [majorVersion] = version.release;
version.release.fill(0);
version.release[0] = majorVersion + 1;
}
version.pre = [identifier, 0];
delete version.post;
delete version.dev;
delete version.local;
break;
case 'preminor':
{
const [majorVersion, minorVersion = 0] = version.release;
version.release.fill(0);
version.release[0] = majorVersion;
version.release[1] = minorVersion + 1;
}
version.pre = [identifier, 0];
delete version.post;
delete version.dev;
delete version.local;
break;
case 'prepatch':
{
const [majorVersion, minorVersion = 0, patchVersion = 0] =
version.release;
version.release.fill(0);
version.release[0] = majorVersion;
version.release[1] = minorVersion;
version.release[2] = patchVersion + 1;
}
version.pre = [identifier, 0];
delete version.post;
delete version.dev;
delete version.local;
break;
case 'prerelease':
if (version.pre === null) {
const [majorVersion, minorVersion = 0, patchVersion = 0] =
version.release;
version.release.fill(0);
version.release[0] = majorVersion;
version.release[1] = minorVersion;
version.release[2] = patchVersion + 1;
version.pre = [identifier, 0];
} else {
if (preReleaseIdentifier === undefined && version.pre !== null) {
[identifier] = version.pre;
}
const [letter, number] = version.pre;
if (letter === identifier) {
version.pre = [letter, number + 1];
} else {
version.pre = [identifier, 0];
}
}
delete version.post;
delete version.dev;
delete version.local;
break;
case 'major':
if (
version.release.slice(1).some((value) => value !== 0) ||
version.pre === null
) {
const [majorVersion] = version.release;
version.release.fill(0);
version.release[0] = majorVersion + 1;
}
delete version.pre;
delete version.post;
delete version.dev;
delete version.local;
break;
case 'minor':
if (
version.release.slice(2).some((value) => value !== 0) ||
version.pre === null
) {
const [majorVersion, minorVersion = 0] = version.release;
version.release.fill(0);
version.release[0] = majorVersion;
version.release[1] = minorVersion + 1;
}
delete version.pre;
delete version.post;
delete version.dev;
delete version.local;
break;
case 'patch':
if (
version.release.slice(3).some((value) => value !== 0) ||
version.pre === null
) {
const [majorVersion, minorVersion = 0, patchVersion = 0] =
version.release;
version.release.fill(0);
version.release[0] = majorVersion;
version.release[1] = minorVersion;
version.release[2] = patchVersion + 1;
}
delete version.pre;
delete version.post;
delete version.dev;
delete version.local;
break;
default:
return null;
}
return stringify(version);
}
/***/ }),
/***/ 23185:
/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => {
// This file is dual licensed under the terms of the Apache License, Version
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
// for complete details.
const { VERSION_PATTERN, explain: explainVersion } = __nccwpck_require__(99961);
const Operator = __nccwpck_require__(99469);
const RANGE_PATTERN = [
'(?<operator>(===|~=|==|!=|<=|>=|<|>))',
'\\s*',
'(',
/* */ '(?<version>(?:' + VERSION_PATTERN.replace(/\?<\w+>/g, '?:') + '))',
/* */ '(?<prefix>\\.\\*)?',
/* */ '|',
/* */ '(?<legacy>[^,;\\s)]+)',
')',
].join('');
module.exports = {
RANGE_PATTERN,
parse,
satisfies,
filter,
validRange,
maxSatisfying,
minSatisfying,
};
const isEqualityOperator = (op) => ['==', '!=', '==='].includes(op);
const rangeRegex = new RegExp('^' + RANGE_PATTERN + '$', 'i');
function parse(ranges) {
if (!ranges.trim()) {
return [];
}
const specifiers = ranges
.split(',')
.map((range) => rangeRegex.exec(range.trim()) || {})
.map(({ groups }) => {
if (!groups) {
return null;
}
let { ...spec } = groups;
const { operator, version, prefix, legacy } = groups;
if (version) {
spec = { ...spec, ...explainVersion(version) };
if (operator === '~=') {
if (spec.release.length < 2) {
return null;
}
}
if (!isEqualityOperator(operator) && spec.local) {
return null;
}
if (prefix) {
if (!isEqualityOperator(operator) || spec.dev || spec.local) {
return null;
}
}
}
if (legacy && operator !== '===') {
return null;
}
return spec;
});
if (specifiers.filter(Boolean).length !== specifiers.length) {
return null;
}
return specifiers;
}
function filter(versions, specifier, options = {}) {
const filtered = pick(versions, specifier, options);
if (filtered.length === 0 && options.prereleases === undefined) {
return pick(versions, specifier, { prereleases: true });
}
return filtered;
}
function maxSatisfying(versions, range, options) {
const found = filter(versions, range, options).sort(Operator.compare);
return found.length === 0 ? null : found[found.length - 1];
}
function minSatisfying(versions, range, options) {
const found = filter(versions, range, options).sort(Operator.compare);
return found.length === 0 ? null : found[0];
}
function pick(versions, specifier, options) {
const parsed = parse(specifier);
if (!parsed) {
return [];
}
return versions.filter((version) => {
const explained = explainVersion(version);
if (!parsed.length) {
return explained && !(explained.is_prerelease && !options.prereleases);
}
return parsed.reduce((pass, spec) => {
if (!pass) {
return false;
}
return contains({ ...spec, ...options }, { version, explained });
}, true);
});
}
function satisfies(version, specifier, options = {}) {
const filtered = pick([version], specifier, options);
return filtered.length === 1;
}
function arrayStartsWith(array, prefix) {
if (prefix.length > array.length) {
return false;
}
for (let i = 0; i < prefix.length; i += 1) {
if (prefix[i] !== array[i]) {
return false;
}
}
return true;
}
function contains(specifier, input) {
const { explained } = input;
let { version } = input;
const { ...spec } = specifier;
if (spec.prereleases === undefined) {
spec.prereleases = spec.is_prerelease;
}
if (explained && explained.is_prerelease && !spec.prereleases) {
return false;
}
if (spec.operator === '~=') {
let compatiblePrefix = spec.release.slice(0, -1).concat('*').join('.');
if (spec.epoch) {
compatiblePrefix = spec.epoch + '!' + compatiblePrefix;
}
return satisfies(version, `>=${spec.version}, ==${compatiblePrefix}`);
}
if (spec.prefix) {
const isMatching =
explained.epoch === spec.epoch &&
arrayStartsWith(explained.release, spec.release);
const isEquality = spec.operator !== '!=';
return isEquality ? isMatching : !isMatching;
}
if (explained)
if (explained.local && spec.version) {
version = explained.public;
spec.version = explainVersion(spec.version).public;
}
if (spec.operator === '<' || spec.operator === '>') {
// simplified version of https://www.python.org/dev/peps/pep-0440/#exclusive-ordered-comparison
if (Operator.eq(spec.release.join('.'), explained.release.join('.'))) {
return false;
}
}
const op = Operator[spec.operator];
return op(version, spec.version || spec.legacy);
}
function validRange(specifier) {
return Boolean(parse(specifier));
}
/***/ }),
/***/ 99961:
/***/ ((module) => {
const VERSION_PATTERN = [
'v?',
'(?:',
/* */ '(?:(?<epoch>[0-9]+)!)?', // epoch
/* */ '(?<release>[0-9]+(?:\\.[0-9]+)*)', // release segment
/* */ '(?<pre>', // pre-release
/* */ '[-_\\.]?',
/* */ '(?<pre_l>(a|b|c|rc|alpha|beta|pre|preview))',
/* */ '[-_\\.]?',
/* */ '(?<pre_n>[0-9]+)?',
/* */ ')?',
/* */ '(?<post>', // post release
/* */ '(?:-(?<post_n1>[0-9]+))',
/* */ '|',
/* */ '(?:',
/* */ '[-_\\.]?',
/* */ '(?<post_l>post|rev|r)',
/* */ '[-_\\.]?',
/* */ '(?<post_n2>[0-9]+)?',
/* */ ')',
/* */ ')?',
/* */ '(?<dev>', // dev release
/* */ '[-_\\.]?',
/* */ '(?<dev_l>dev)',
/* */ '[-_\\.]?',
/* */ '(?<dev_n>[0-9]+)?',
/* */ ')?',
')',
'(?:\\+(?<local>[a-z0-9]+(?:[-_\\.][a-z0-9]+)*))?', // local version
].join('');
module.exports = {
VERSION_PATTERN,
valid,
clean,
explain,
parse,
stringify,
};
const validRegex = new RegExp('^' + VERSION_PATTERN + '$', 'i');
function valid(version) {
return validRegex.test(version) ? version : null;
}
const cleanRegex = new RegExp('^\\s*' + VERSION_PATTERN + '\\s*$', 'i');
function clean(version) {
return stringify(parse(version, cleanRegex));
}
function parse(version, regex) {
// Validate the version and parse it into pieces
const { groups } = (regex || validRegex).exec(version) || {};
if (!groups) {
return null;
}
// Store the parsed out pieces of the version
const parsed = {
epoch: Number(groups.epoch ? groups.epoch : 0),
release: groups.release.split('.').map(Number),
pre: normalize_letter_version(groups.pre_l, groups.pre_n),
post: normalize_letter_version(
groups.post_l,
groups.post_n1 || groups.post_n2,
),
dev: normalize_letter_version(groups.dev_l, groups.dev_n),
local: parse_local_version(groups.local),
};
return parsed;
}
function stringify(parsed) {
if (!parsed) {
return null;
}
const { epoch, release, pre, post, dev, local } = parsed;
const parts = [];
// Epoch
if (epoch !== 0) {
parts.push(`${epoch}!`);
}
// Release segment
parts.push(release.join('.'));
// Pre-release
if (pre) {
parts.push(pre.join(''));
}
// Post-release
if (post) {
parts.push('.' + post.join(''));
}
// Development release
if (dev) {
parts.push('.' + dev.join(''));
}
// Local version segment
if (local) {
parts.push(`+${local}`);
}
return parts.join('');
}
function normalize_letter_version(letterIn, numberIn) {
let letter = letterIn;
let number = numberIn;
if (letter) {
// We consider there to be an implicit 0 in a pre-release if there is
// not a numeral associated with it.
if (!number) {
number = 0;
}
// We normalize any letters to their lower case form
letter = letter.toLowerCase();
// We consider some words to be alternate spellings of other words and
// in those cases we want to normalize the spellings to our preferred
// spelling.
if (letter === 'alpha') {
letter = 'a';
} else if (letter === 'beta') {
letter = 'b';
} else if (['c', 'pre', 'preview'].includes(letter)) {
letter = 'rc';
} else if (['rev', 'r'].includes(letter)) {
letter = 'post';
}
return [letter, Number(number)];
}
if (!letter && number) {
// We assume if we are given a number, but we are not given a letter
// then this is using the implicit post release syntax (e.g. 1.0-1)
letter = 'post';
return [letter, Number(number)];
}
return null;
}
function parse_local_version(local) {
/*
Takes a string like abc.1.twelve and turns it into("abc", 1, "twelve").
*/
if (local) {
return local
.split(/[._-]/)
.map((part) =>
Number.isNaN(Number(part)) ? part.toLowerCase() : Number(part),
);
}
return null;
}
function explain(version) {
const parsed = parse(version);
if (!parsed) {
return parsed;
}
const { epoch, release, pre, post, dev, local } = parsed;
let base_version = '';
if (epoch !== 0) {
base_version += epoch + '!';
}
base_version += release.join('.');
const is_prerelease = Boolean(dev || pre);
const is_devrelease = Boolean(dev);
const is_postrelease = Boolean(post);
// return
return {
epoch,
release,
pre,
post: post ? post[1] : post,
dev: dev ? dev[1] : dev,
local: local ? local.join('.') : local,
public: stringify(parsed).split('+', 1)[0],
base_version,
is_prerelease,
is_devrelease,
is_postrelease,
};
}
/***/ }),
/***/ 31324:
@ -123171,6 +123948,7 @@ exports.resolveVersion = resolveVersion;
const core = __importStar(__nccwpck_require__(37484));
const tc = __importStar(__nccwpck_require__(33472));
const path = __importStar(__nccwpck_require__(76760));
const pep440 = __importStar(__nccwpck_require__(63297));
const node_fs_1 = __nccwpck_require__(73024);
const constants_1 = __nccwpck_require__(56156);
const checksum_1 = __nccwpck_require__(95391);
@ -123222,8 +124000,8 @@ async function resolveVersion(versionInput, githubToken) {
}
const availableVersions = await getAvailableVersions(githubToken);
core.debug(`Available versions: ${availableVersions}`);
const resolvedVersion = tc.evaluateVersions(availableVersions, version);
if (resolvedVersion === "") {
const resolvedVersion = maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`);
}
return resolvedVersion;
@ -123281,6 +124059,19 @@ async function getLatestRelease(octokit) {
});
return latestRelease;
}
function maxSatisfying(versions, version) {
const maxSemver = tc.evaluateVersions(versions, version);
if (maxSemver !== "") {
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
return maxSemver;
}
const maxPep440 = pep440.maxSatisfying(versions, version);
if (maxPep440 !== null) {
core.debug(`Found a version that satisfies the pep440 specifier: ${maxPep440}`);
return maxPep440;
}
return undefined;
}
/***/ }),

16
package-lock.json generated
View file

@ -18,6 +18,7 @@
"@octokit/core": "^6.1.4",
"@octokit/plugin-paginate-rest": "^11.4.3",
"@octokit/plugin-rest-endpoint-methods": "^13.3.1",
"@renovatebot/pep440": "^4.1.0",
"smol-toml": "^1.3.1",
"undici": "^7.5.0"
},
@ -1705,6 +1706,16 @@
"@protobuf-ts/runtime": "^2.9.4"
}
},
"node_modules/@renovatebot/pep440": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-4.1.0.tgz",
"integrity": "sha512-mo2RxnOSp78Njt1HmgMwjl6FapP4OyIS8HypJlymCvN7AIV2Xf5PmZfl/E3O1WWZ6IjKrfsEAaPWFMi8tnkq3g==",
"license": "Apache-2.0",
"engines": {
"node": "^20.9.0 || ^22.11.0",
"pnpm": "^9.0.0"
}
},
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@ -5933,6 +5944,11 @@
"@protobuf-ts/runtime": "^2.9.4"
}
},
"@renovatebot/pep440": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@renovatebot/pep440/-/pep440-4.1.0.tgz",
"integrity": "sha512-mo2RxnOSp78Njt1HmgMwjl6FapP4OyIS8HypJlymCvN7AIV2Xf5PmZfl/E3O1WWZ6IjKrfsEAaPWFMi8tnkq3g=="
},
"@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",

View file

@ -32,8 +32,9 @@
"@octokit/core": "^6.1.4",
"@octokit/plugin-paginate-rest": "^11.4.3",
"@octokit/plugin-rest-endpoint-methods": "^13.3.1",
"undici": "^7.5.0",
"smol-toml": "^1.3.1"
"@renovatebot/pep440": "^4.1.0",
"smol-toml": "^1.3.1",
"undici": "^7.5.0"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",

View file

@ -1,6 +1,7 @@
import * as core from "@actions/core";
import * as tc from "@actions/tool-cache";
import * as path from "node:path";
import * as pep440 from "@renovatebot/pep440";
import { promises as fs } from "node:fs";
import { OWNER, REPO, TOOL_CACHE_NAME } from "../utils/constants";
import type { Architecture, Platform } from "../utils/platforms";
@ -85,8 +86,8 @@ export async function resolveVersion(
}
const availableVersions = await getAvailableVersions(githubToken);
core.debug(`Available versions: ${availableVersions}`);
const resolvedVersion = tc.evaluateVersions(availableVersions, version);
if (resolvedVersion === "") {
const resolvedVersion = maxSatisfying(availableVersions, version);
if (resolvedVersion === undefined) {
throw new Error(`No version found for ${version}`);
}
return resolvedVersion;
@ -154,3 +155,22 @@ async function getLatestRelease(octokit: InstanceType<typeof Octokit>) {
});
return latestRelease;
}
function maxSatisfying(
versions: string[],
version: string,
): string | undefined {
const maxSemver = tc.evaluateVersions(versions, version);
if (maxSemver !== "") {
core.debug(`Found a version that satisfies the semver range: ${maxSemver}`);
return maxSemver;
}
const maxPep440 = pep440.maxSatisfying(versions, version);
if (maxPep440 !== null) {
core.debug(
`Found a version that satisfies the pep440 specifier: ${maxPep440}`,
);
return maxPep440;
}
return undefined;
}