Read Python version from .tool-versions (#996)

## Summary
- read the Python version from an explicitly selected `.tool-versions`
file
- preserve `python-version` and existing `UV_PYTHON` precedence
- add parser, input, and workflow coverage and update documentation and
bundled action artifacts

## Validation
- `npm run all`
- `actionlint .github/workflows/test.yml`
- `uvx zizmor .github/workflows/test.yml`

Closes #983

Refs: pi-session 019ff01a-544c-79f3-8f73-a00132af39f5
This commit is contained in:
Kevin Stillhammer
2026-08-11 14:26:03 +02:00
committed by GitHub
parent 8ed89c5114
commit 46f427bd47
12 changed files with 400 additions and 49 deletions
+26
View File
@@ -234,6 +234,31 @@ jobs:
exit 1
fi
test-tool-versions-python-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install versions from .tool-versions
id: setup-uv
uses: ./
with:
version-file: "__tests__/fixtures/.tool-versions"
- name: Verify Python version from .tool-versions
run: |
if [ "$UV_PYTHON" != "3.13.1t" ]; then
echo "Wrong UV_PYTHON: $UV_PYTHON"
exit 1
fi
if [ "$PYTHON_VERSION" != "3.13.1t" ]; then
echo "Wrong python-version output: $PYTHON_VERSION"
exit 1
fi
shell: bash
env:
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
test-malformed-pyproject-file-fallback:
runs-on: ubuntu-latest
steps:
@@ -1126,6 +1151,7 @@ jobs:
- test-from-working-directory-version
- test-malformed-pyproject-file-fallback
- test-version-file-version
- test-tool-versions-python-version
- test-checksum
- test-with-explicit-token
- test-uvx
+21 -3
View File
@@ -47,13 +47,13 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# The version of uv to install, e.g., "0.5.0", "latest", or "latest-known" (default: searches for version in config files, then latest)
version: ""
# Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock (default: searches uv.toml then pyproject.toml)
# Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock. A selected .tool-versions file can also provide the Python version (default: searches uv.toml then pyproject.toml)
version-file: ""
# Resolution strategy when resolving version ranges: 'highest' or 'lowest'
resolution-strategy: "highest"
# The version of Python to set UV_PYTHON to
# The version of Python to set UV_PYTHON to (overrides the Python version from .tool-versions)
python-version: ""
# Use uv venv to activate a venv ready to be used by later steps
@@ -144,7 +144,25 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
You can use the input `python-version` to set the environment variable `UV_PYTHON` for the rest of your workflow
This will override any python version specifications in `pyproject.toml` and `.python-version`
This will override any python version specifications in `pyproject.toml`, `.python-version`, and
an explicitly selected `.tool-versions` file.
When `version-file` points to `.tool-versions`, its `python` entry is used if neither
`python-version` nor `UV_PYTHON` is set:
```text
uv 0.12.3
python 3.13
```
```yaml
- uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
with:
version-file: ".tool-versions"
```
Only a single Python version is supported. Multiple fallback versions and the asdf `ref:`, `path:`,
and `system` forms are ignored with a warning.
```yaml
- name: Install the latest version of uv and set the python version to 3.13t
+1
View File
@@ -1 +1,2 @@
uv 0.5.15
python 3.13.1t
+49
View File
@@ -18,6 +18,7 @@ const ORIGINAL_HOME = process.env.HOME;
const ORIGINAL_RUNNER_ENVIRONMENT = process.env.RUNNER_ENVIRONMENT;
const ORIGINAL_RUNNER_TEMP = process.env.RUNNER_TEMP;
const ORIGINAL_UV_CACHE_DIR = process.env.UV_CACHE_DIR;
const ORIGINAL_UV_PYTHON = process.env.UV_PYTHON;
const ORIGINAL_UV_PYTHON_INSTALL_DIR = process.env.UV_PYTHON_INSTALL_DIR;
const mockDebug = jest.fn();
@@ -60,6 +61,7 @@ function resetEnvironment(): void {
delete process.env.RUNNER_ENVIRONMENT;
delete process.env.RUNNER_TEMP;
delete process.env.UV_CACHE_DIR;
delete process.env.UV_PYTHON;
delete process.env.UV_PYTHON_INSTALL_DIR;
}
@@ -74,6 +76,7 @@ function restoreEnvironment(): void {
process.env.RUNNER_ENVIRONMENT = ORIGINAL_RUNNER_ENVIRONMENT;
process.env.RUNNER_TEMP = ORIGINAL_RUNNER_TEMP;
process.env.UV_CACHE_DIR = ORIGINAL_UV_CACHE_DIR;
process.env.UV_PYTHON = ORIGINAL_UV_PYTHON;
process.env.UV_PYTHON_INSTALL_DIR = ORIGINAL_UV_PYTHON_INSTALL_DIR;
}
@@ -100,6 +103,52 @@ describe("loadInputs", () => {
expect(inputs.resolutionStrategy).toBe("highest");
});
it("uses the Python version from an explicitly selected .tool-versions file", () => {
mockInputs["working-directory"] = createTempProject({
".tool-versions": "uv 0.12.3\npython 3.13.1t\n",
});
mockInputs["version-file"] = ".tool-versions";
const inputs = loadInputs();
expect(inputs.pythonVersion).toBe("3.13.1t");
});
it("prefers the python-version input over .tool-versions", () => {
mockInputs["working-directory"] = createTempProject({
".tool-versions": "uv 0.12.3\npython 3.13\n",
});
mockInputs["version-file"] = ".tool-versions";
mockInputs["python-version"] = "3.12";
const inputs = loadInputs();
expect(inputs.pythonVersion).toBe("3.12");
});
it("preserves UV_PYTHON instead of overriding it from .tool-versions", () => {
mockInputs["working-directory"] = createTempProject({
".tool-versions": "uv 0.12.3\npython 3.13\n",
});
mockInputs["version-file"] = ".tool-versions";
process.env.UV_PYTHON = "3.11";
const inputs = loadInputs();
expect(inputs.pythonVersion).toBe("");
expect(process.env.UV_PYTHON).toBe("3.11");
});
it("does not discover .tool-versions from the working directory", () => {
mockInputs["working-directory"] = createTempProject({
".tool-versions": "uv 0.12.3\npython 3.13\n",
});
const inputs = loadInputs();
expect(inputs.pythonVersion).toBe("");
});
it.each(["pull_request_target", "workflow_run", "release"])(
"disables automatic caching for the %s event",
(eventName) => {
+72 -2
View File
@@ -21,6 +21,11 @@ async function getVersionFromToolVersions(filePath: string) {
return getUvVersionFromToolVersions(filePath);
}
async function getPythonVersionFromToolVersions(filePath: string) {
const module = await import("../../src/version/tool-versions-file");
return module.getPythonVersionFromToolVersions(filePath);
}
describe("getUvVersionFromToolVersions", () => {
beforeEach(() => {
jest.resetModules();
@@ -61,8 +66,8 @@ describe("getUvVersionFromToolVersions", () => {
expect(result).toBe("0.3.0");
});
it("should skip commented lines", async () => {
const fileContent = "# uv 0.1.0\npython 3.11.0\nuv 0.2.0";
it("should skip comments", async () => {
const fileContent = "# uv 0.1.0\npython 3.11.0\nuv 0.2.0 # inline comment";
mockReadFileSync.mockReturnValue(fileContent);
const result = await getVersionFromToolVersions(".tool-versions");
@@ -121,3 +126,68 @@ describe("getUvVersionFromToolVersions", () => {
);
});
});
describe("getPythonVersionFromToolVersions", () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
});
it("should return version for a valid Python entry", async () => {
mockReadFileSync.mockReturnValue(
"nodejs 24.0.0\r\npython v3.13.1t # use free-threaded Python\r\nuv 0.12.3",
);
const result = await getPythonVersionFromToolVersions(".tool-versions");
expect(result).toBe("3.13.1t");
});
it("should return the first matching Python version", async () => {
mockReadFileSync.mockReturnValue("python 3.12\npython 3.13");
const result = await getPythonVersionFromToolVersions(".tool-versions");
expect(result).toBe("3.12");
});
it("should return undefined when no Python entry is found", async () => {
mockReadFileSync.mockReturnValue("uv 0.12.3\nnodejs 24.0.0");
const result = await getPythonVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined();
});
it("should warn and return undefined for multiple Python versions", async () => {
mockReadFileSync.mockReturnValue("python 3.13 3.12 system");
const result = await getPythonVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined();
expect(mockWarning).toHaveBeenCalledWith(
"Multiple Python versions in .tool-versions are not supported. The Python entry will be ignored.",
);
});
it.each(["ref:main", "path:~/src/python", "system"])(
"should warn and return undefined for %s",
async (version) => {
mockReadFileSync.mockReturnValue(`python ${version}`);
const result = await getPythonVersionFromToolVersions(".tool-versions");
expect(result).toBeUndefined();
expect(mockWarning).toHaveBeenCalledWith(
`The Python version ${version} in .tool-versions is not supported. The Python entry will be ignored.`,
);
},
);
it("should return undefined for non-.tool-versions files", async () => {
const result = await getPythonVersionFromToolVersions(".python-version");
expect(result).toBeUndefined();
expect(mockReadFileSync).not.toHaveBeenCalled();
});
});
+2 -2
View File
@@ -7,10 +7,10 @@ inputs:
description: "The version of uv to install, e.g., `0.5.0`, `latest`, or `latest-known`. Defaults to the version in pyproject.toml or `latest`."
default: ""
version-file:
description: "Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock. Defaults to searching for uv.toml and if not found pyproject.toml."
description: "Path to a file containing the version of uv to install, e.g., uv.toml, pyproject.toml, .tool-versions, requirements.txt or uv.lock. A selected .tool-versions file can also provide the Python version. Defaults to searching for uv.toml and if not found pyproject.toml."
default: ""
python-version:
description: "The version of Python to set UV_PYTHON to"
description: "The version of Python to set UV_PYTHON to. Overrides the Python version from .tool-versions."
required: false
activate-environment:
description: "Use uv venv to activate a venv ready to be used by later steps. "
Generated Vendored
+71 -7
View File
@@ -26998,7 +26998,7 @@ __export(save_cache_exports, {
run: () => run
});
module.exports = __toCommonJS(save_cache_exports);
var fs7 = __toESM(require("node:fs"), 1);
var fs9 = __toESM(require("node:fs"), 1);
// node_modules/@actions/core/lib/command.js
var os = __toESM(require("os"), 1);
@@ -61845,10 +61845,54 @@ var STATE_UV_PATH = "uv-path";
var STATE_UV_VERSION = "uv-version";
// src/utils/inputs.ts
var import_node_fs4 = __toESM(require("node:fs"), 1);
var import_node_path = __toESM(require("node:path"), 1);
// src/utils/config-file.ts
// src/version/tool-versions-file.ts
var import_node_fs2 = __toESM(require("node:fs"), 1);
function getPythonVersionFromToolVersions(filePath) {
const versions = getToolVersions(filePath, "python");
if (versions === void 0 || versions.length === 0) {
return void 0;
}
if (versions.length > 1) {
warning(
"Multiple Python versions in .tool-versions are not supported. The Python entry will be ignored."
);
return void 0;
}
const version3 = stripVersionPrefix(versions[0]);
if (version3 === "system" || version3.startsWith("ref:") || version3.startsWith("path:")) {
warning(
`The Python version ${versions[0]} in .tool-versions is not supported. The Python entry will be ignored.`
);
return void 0;
}
return version3;
}
function getToolVersions(filePath, toolName) {
if (!filePath.endsWith(".tool-versions")) {
return void 0;
}
const fileContents = import_node_fs2.default.readFileSync(filePath, "utf8");
for (const line of fileContents.split("\n")) {
const content = line.split("#", 1)[0].trim();
if (content === "") {
continue;
}
const [tool, ...versions] = content.split(/\s+/);
if (tool === toolName) {
return versions;
}
}
return void 0;
}
function stripVersionPrefix(version3) {
return version3.startsWith("v") ? version3.slice(1) : version3;
}
// src/utils/config-file.ts
var import_node_fs3 = __toESM(require("node:fs"), 1);
// node_modules/smol-toml/dist/date.js
var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[T ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|[-+]\d{2}:\d{2})?$/i;
@@ -62532,10 +62576,10 @@ function parse3(toml, { maxDepth = 1e3, integersAsBigInt } = {}) {
// src/utils/config-file.ts
function getConfigValueFromTomlFile(filePath, key) {
if (!import_node_fs2.default.existsSync(filePath) || !filePath.endsWith(".toml")) {
if (!import_node_fs3.default.existsSync(filePath) || !filePath.endsWith(".toml")) {
return void 0;
}
const fileContent = import_node_fs2.default.readFileSync(filePath, "utf-8");
const fileContent = import_node_fs3.default.readFileSync(filePath, "utf-8");
return getConfigValueFromTomlContent(filePath, fileContent, key);
}
function getConfigValueFromTomlContent(filePath, fileContent, key) {
@@ -62555,7 +62599,7 @@ function loadInputs() {
const workingDirectory = getInput("working-directory");
const version3 = getInput("version");
const versionFile = getVersionFile(workingDirectory);
const pythonVersion = getInput("python-version");
const pythonVersion = getPythonVersion(versionFile);
const activateEnvironment = getBooleanInput("activate-environment");
const noProject = getBooleanInput("no-project");
const venvPath = getVenvPath(workingDirectory, activateEnvironment);
@@ -62621,6 +62665,26 @@ function getVersionFile(workingDirectory) {
}
return versionFileInput;
}
function getPythonVersion(versionFile) {
const pythonVersionInput = getInput("python-version");
if (pythonVersionInput !== "") {
return pythonVersionInput;
}
if (process.env.UV_PYTHON !== void 0 && process.env.UV_PYTHON !== "") {
return "";
}
if (versionFile === "" || !import_node_fs4.default.existsSync(versionFile)) {
return "";
}
try {
return getPythonVersionFromToolVersions(versionFile) ?? "";
} catch (err) {
warning2(
`Error while parsing Python version from ${versionFile}: ${err.message}`
);
return "";
}
}
function getVenvPath(workingDirectory, activateEnvironment) {
const venvPathInput = getInput("venv-path");
if (venvPathInput !== "") {
@@ -62864,7 +62928,7 @@ async function saveCache3(inputs) {
await pruneCache();
}
const actualCachePath = getUvCachePath(inputs);
if (!fs7.existsSync(actualCachePath)) {
if (!fs9.existsSync(actualCachePath)) {
if (inputs.ignoreNothingToCache) {
info2(
"No cacheable uv cache paths were found. Ignoring because ignore-nothing-to-cache is enabled."
@@ -62884,7 +62948,7 @@ async function saveCache3(inputs) {
}
}
if (inputs.cachePython) {
if (!fs7.existsSync(inputs.pythonDir)) {
if (!fs9.existsSync(inputs.pythonDir)) {
warning2(
`Python cache path ${inputs.pythonDir} does not exist on disk. Skipping Python cache save because no managed Python installation was found. If you want uv to install managed Python instead of using a system interpreter, set UV_PYTHON_PREFERENCE=only-managed.`
);
Generated Vendored
+69 -18
View File
@@ -56054,7 +56054,7 @@ var require_semver5 = __commonJS({
});
// src/setup-uv.ts
var import_node_fs8 = __toESM(require("node:fs"), 1);
var import_node_fs9 = __toESM(require("node:fs"), 1);
var path16 = __toESM(require("node:path"), 1);
// node_modules/@actions/core/lib/command.js
@@ -98258,29 +98258,59 @@ function getUvVersionFromDependency(dependency) {
// src/version/tool-versions-file.ts
var import_node_fs4 = __toESM(require("node:fs"), 1);
function getUvVersionFromToolVersions(filePath) {
const versions = getToolVersions(filePath, "uv");
if (versions === void 0 || versions.length !== 1) {
return void 0;
}
const version3 = stripVersionPrefix(versions[0]);
if (version3.startsWith("ref")) {
warning(
"The ref syntax of .tool-versions is not supported. Please use a released version instead."
);
return void 0;
}
return version3;
}
function getPythonVersionFromToolVersions(filePath) {
const versions = getToolVersions(filePath, "python");
if (versions === void 0 || versions.length === 0) {
return void 0;
}
if (versions.length > 1) {
warning(
"Multiple Python versions in .tool-versions are not supported. The Python entry will be ignored."
);
return void 0;
}
const version3 = stripVersionPrefix(versions[0]);
if (version3 === "system" || version3.startsWith("ref:") || version3.startsWith("path:")) {
warning(
`The Python version ${versions[0]} in .tool-versions is not supported. The Python entry will be ignored.`
);
return void 0;
}
return version3;
}
function getToolVersions(filePath, toolName) {
if (!filePath.endsWith(".tool-versions")) {
return void 0;
}
const fileContents = import_node_fs4.default.readFileSync(filePath, "utf8");
const lines = fileContents.split("\n");
for (const line of lines) {
if (line.trim().startsWith("#")) {
for (const line of fileContents.split("\n")) {
const content = line.split("#", 1)[0].trim();
if (content === "") {
continue;
}
const match2 = line.match(/^\s*uv\s*v?\s*(?<version>[^\s]+)\s*$/);
if (match2) {
const matchedVersion = match2.groups?.version.trim();
if (matchedVersion?.startsWith("ref")) {
warning(
"The ref syntax of .tool-versions is not supported. Please use a released version instead."
);
return void 0;
}
return matchedVersion;
const [tool, ...versions] = content.split(/\s+/);
if (tool === toolName) {
return versions;
}
}
return void 0;
}
function stripVersionPrefix(version3) {
return version3.startsWith("v") ? version3.slice(1) : version3;
}
// src/version/uv-lock-file.ts
var import_node_fs5 = __toESM(require("node:fs"), 1);
@@ -98734,12 +98764,13 @@ function getExtension(platform2) {
}
// src/utils/inputs.ts
var import_node_fs8 = __toESM(require("node:fs"), 1);
var import_node_path = __toESM(require("node:path"), 1);
function loadInputs() {
const workingDirectory = getInput("working-directory");
const version3 = getInput("version");
const versionFile = getVersionFile(workingDirectory);
const pythonVersion = getInput("python-version");
const pythonVersion = getPythonVersion(versionFile);
const activateEnvironment2 = getBooleanInput("activate-environment");
const noProject = getBooleanInput("no-project");
const venvPath = getVenvPath(workingDirectory, activateEnvironment2);
@@ -98805,6 +98836,26 @@ function getVersionFile(workingDirectory) {
}
return versionFileInput;
}
function getPythonVersion(versionFile) {
const pythonVersionInput = getInput("python-version");
if (pythonVersionInput !== "") {
return pythonVersionInput;
}
if (process.env.UV_PYTHON !== void 0 && process.env.UV_PYTHON !== "") {
return "";
}
if (versionFile === "" || !import_node_fs8.default.existsSync(versionFile)) {
return "";
}
try {
return getPythonVersionFromToolVersions(versionFile) ?? "";
} catch (err) {
warning2(
`Error while parsing Python version from ${versionFile}: ${err.message}`
);
return "";
}
}
function getVenvPath(workingDirectory, activateEnvironment2) {
const venvPathInput = getInput("venv-path");
if (venvPathInput !== "") {
@@ -99018,7 +99069,7 @@ process.on("uncaughtException", (error2) => {
process.on("unhandledRejection", (reason) => {
failUnexpectedly("Unhandled promise rejection", reason);
});
async function getPythonVersion(inputs) {
async function getPythonVersion2(inputs) {
if (inputs.pythonVersion !== "") {
return inputs.pythonVersion;
}
@@ -99068,7 +99119,7 @@ async function run() {
setOutput("uv-version", setupResult.version);
saveState(STATE_UV_VERSION, setupResult.version);
info2(`Successfully installed uv version ${setupResult.version}`);
const detectedPythonVersion = await getPythonVersion(inputs);
const detectedPythonVersion = await getPythonVersion2(inputs);
setOutput("python-version", detectedPythonVersion);
if (inputs.enableCache) {
await restoreCache2(inputs, detectedPythonVersion);
@@ -99080,7 +99131,7 @@ async function run() {
}
}
function detectEmptyWorkdir(inputs) {
if (import_node_fs8.default.readdirSync(inputs.workingDirectory).length === 0) {
if (import_node_fs9.default.readdirSync(inputs.workingDirectory).length === 0) {
if (inputs.ignoreEmptyWorkdir) {
info2(
"Empty workdir detected. Ignoring because ignore-empty-workdir is enabled"
+4 -1
View File
@@ -85,7 +85,10 @@ You can use the `version-file` input to specify a file that contains the version
This can either be a `pyproject.toml` or `uv.toml` file which defines a `required-version` or
uv defined as a dependency in `pyproject.toml` or `requirements.txt`.
[asdf](https://asdf-vm.com/) `.tool-versions` is also supported, but without the `ref` syntax.
[asdf](https://asdf-vm.com/) `.tool-versions` is also supported for selecting uv. If neither
`python-version` nor `UV_PYTHON` is set, the `python` entry from the selected file is also exported
as `UV_PYTHON`. Only a single Python version is supported; multiple fallback versions and the asdf
`ref:`, `path:`, and `system` forms are ignored with a warning.
```yaml
- name: Install uv based on the version defined in pyproject.toml
+2 -1
View File
@@ -131,7 +131,7 @@ If you want to ignore this, set the `ignore-empty-workdir` input to `true`.
This action sets several environment variables that influence uv's behavior and can be used by subsequent steps:
- `UV_PYTHON`: Set when `python-version` input is specified. Controls which Python version uv uses.
- `UV_PYTHON`: Set when `python-version` is specified or the selected `.tool-versions` file contains a supported `python` entry. Controls which Python version uv uses.
- `UV_CACHE_DIR`: Set when caching is enabled (unless already configured in uv config files). Controls where uv stores its cache.
- `UV_TOOL_DIR`: Set when `tool-dir` input is specified. Controls where uv installs tool environments.
- `UV_TOOL_BIN_DIR`: Set when `tool-bin-dir` input is specified. Controls where uv installs tool binaries.
@@ -142,6 +142,7 @@ This action sets several environment variables that influence uv's behavior and
- `UV_NO_MODIFY_PATH`: If set, prevents the action from modifying PATH. Cannot be used with `activate-environment`.
- `UV_CACHE_DIR`: If already set, the action will respect it instead of setting its own cache directory.
- `UV_PYTHON`: If already set and `python-version` is not specified, the action will respect it instead of using the `python` entry from `.tool-versions`.
```yaml
- name: Example using environment variables
+25 -1
View File
@@ -1,5 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import * as core from "@actions/core";
import { getPythonVersionFromToolVersions } from "../version/tool-versions-file";
import { getConfigValueFromTomlFile } from "./config-file";
import * as log from "./logging";
@@ -51,7 +53,7 @@ export function loadInputs(): SetupInputs {
const workingDirectory = core.getInput("working-directory");
const version = core.getInput("version");
const versionFile = getVersionFile(workingDirectory);
const pythonVersion = core.getInput("python-version");
const pythonVersion = getPythonVersion(versionFile);
const activateEnvironment = core.getBooleanInput("activate-environment");
const noProject = core.getBooleanInput("no-project");
const venvPath = getVenvPath(workingDirectory, activateEnvironment);
@@ -122,6 +124,28 @@ function getVersionFile(workingDirectory: string): string {
return versionFileInput;
}
function getPythonVersion(versionFile: string): string {
const pythonVersionInput = core.getInput("python-version");
if (pythonVersionInput !== "") {
return pythonVersionInput;
}
if (process.env.UV_PYTHON !== undefined && process.env.UV_PYTHON !== "") {
return "";
}
if (versionFile === "" || !fs.existsSync(versionFile)) {
return "";
}
try {
return getPythonVersionFromToolVersions(versionFile) ?? "";
} catch (err) {
log.warning(
`Error while parsing Python version from ${versionFile}: ${(err as Error).message}`,
);
return "";
}
}
function getVenvPath(
workingDirectory: string,
activateEnvironment: boolean,
+58 -14
View File
@@ -4,28 +4,72 @@ import * as core from "@actions/core";
export function getUvVersionFromToolVersions(
filePath: string,
): string | undefined {
const versions = getToolVersions(filePath, "uv");
if (versions === undefined || versions.length !== 1) {
return undefined;
}
const version = stripVersionPrefix(versions[0]);
if (version.startsWith("ref")) {
core.warning(
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
);
return undefined;
}
return version;
}
export function getPythonVersionFromToolVersions(
filePath: string,
): string | undefined {
const versions = getToolVersions(filePath, "python");
if (versions === undefined || versions.length === 0) {
return undefined;
}
if (versions.length > 1) {
core.warning(
"Multiple Python versions in .tool-versions are not supported. The Python entry will be ignored.",
);
return undefined;
}
const version = stripVersionPrefix(versions[0]);
if (
version === "system" ||
version.startsWith("ref:") ||
version.startsWith("path:")
) {
core.warning(
`The Python version ${versions[0]} in .tool-versions is not supported. The Python entry will be ignored.`,
);
return undefined;
}
return version;
}
function getToolVersions(
filePath: string,
toolName: string,
): string[] | undefined {
if (!filePath.endsWith(".tool-versions")) {
return undefined;
}
const fileContents = fs.readFileSync(filePath, "utf8");
const lines = fileContents.split("\n");
for (const line of lines) {
// Skip commented lines
if (line.trim().startsWith("#")) {
for (const line of fileContents.split("\n")) {
const content = line.split("#", 1)[0].trim();
if (content === "") {
continue;
}
const match = line.match(/^\s*uv\s*v?\s*(?<version>[^\s]+)\s*$/);
if (match) {
const matchedVersion = match.groups?.version.trim();
if (matchedVersion?.startsWith("ref")) {
core.warning(
"The ref syntax of .tool-versions is not supported. Please use a released version instead.",
);
return undefined;
}
return matchedVersion;
const [tool, ...versions] = content.split(/\s+/);
if (tool === toolName) {
return versions;
}
}
return undefined;
}
function stripVersionPrefix(version: string): string {
return version.startsWith("v") ? version.slice(1) : version;
}