Expose Python runtime identity for downstream caching

This commit is contained in:
William Woodruff
2026-09-03 15:18:29 -04:00
parent 3956519c9e
commit 70e5842acc
11 changed files with 400 additions and 204 deletions
+31 -11
View File
@@ -347,14 +347,14 @@ jobs:
if [ "$PYTHON_VERSION" != "3.13.1t" ]; then
exit 1
fi
if [ -n "$PYTHON_VERSION_RESOLVED" ]; then
echo "python-version-resolved should be empty without environment activation"
if [ -n "$PYTHON_RUNTIME_ID" ]; then
echo "python-runtime-id should be empty without environment activation"
exit 1
fi
shell: bash
env:
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
PYTHON_VERSION_RESOLVED: ${{ steps.setup-uv.outputs.python-version-resolved }}
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
- run: uv sync
working-directory: __tests__/fixtures/uv-project
@@ -369,6 +369,8 @@ jobs:
python-version: "3.13"
- os: ubuntu-latest
python-version: "3.14.0rc2"
- os: ubuntu-latest
python-version: "3.14.0rc2t"
- os: ubuntu-latest
python-version: "pypy3.11"
steps:
@@ -384,9 +386,27 @@ jobs:
shell: bash
- name: Verify Python version outputs and cache key
run: |
expected=$(python -I -c 'import platform; print(platform.python_version())')
if [ "$PYTHON_VERSION_RESOLVED" != "$expected" ]; then
echo "Wrong resolved Python version: $PYTHON_VERSION_RESOLVED (expected $expected)"
expected=$(python -I - <<'PY'
import platform
import sys
import sysconfig
if sys.implementation.name == "cpython":
runtime = f"cpython-{platform.python_version()}"
else:
version = sys.implementation.version
implementation_version = f"{version.major}.{version.minor}.{version.micro}"
suffix = {"alpha": "a", "beta": "b", "candidate": "rc", "final": ""}[version.releaselevel]
if suffix:
implementation_version += f"{suffix}{version.serial}"
runtime = f"{sys.implementation.name}-{implementation_version}-python-{platform.python_version()}"
if sysconfig.get_config_var("Py_GIL_DISABLED") == 1:
runtime += "-freethreaded"
print(runtime)
PY
)
if [ "$PYTHON_RUNTIME_ID" != "$expected" ]; then
echo "Wrong Python runtime ID: $PYTHON_RUNTIME_ID (expected $expected)"
exit 1
fi
if [ "$PYTHON_VERSION" != "$PYTHON_REQUEST" ]; then
@@ -402,7 +422,7 @@ jobs:
CACHE_KEY: ${{ steps.setup-uv.outputs.cache-key }}
PYTHON_REQUEST: ${{ matrix.python-version }}
PYTHON_VERSION: ${{ steps.setup-uv.outputs.python-version }}
PYTHON_VERSION_RESOLVED: ${{ steps.setup-uv.outputs.python-version-resolved }}
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
- name: Verify output venv is set
run: |
if [ -z "$UV_VENV" ]; then
@@ -467,15 +487,15 @@ jobs:
raise SystemExit(f"Python is not running from custom venv: {sys.executable}")
PY
shell: bash
- name: Verify resolved Python version from custom venv
- name: Verify Python runtime ID from custom venv
run: |
if [ "$PYTHON_VERSION_RESOLVED" != "3.13.1" ]; then
echo "Wrong resolved Python version: $PYTHON_VERSION_RESOLVED"
if [ "$PYTHON_RUNTIME_ID" != "cpython-3.13.1-freethreaded" ]; then
echo "Wrong Python runtime ID: $PYTHON_RUNTIME_ID"
exit 1
fi
shell: bash
env:
PYTHON_VERSION_RESOLVED: ${{ steps.setup-uv.outputs.python-version-resolved }}
PYTHON_RUNTIME_ID: ${{ steps.setup-uv.outputs.python-runtime-id }}
test-activate-environment-no-project:
runs-on: ubuntu-latest
+3 -2
View File
@@ -138,8 +138,9 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
- `cache-hit`: A boolean value to indicate a cache entry was found.
- `venv`: Path to the activated venv if activate-environment is true.
- `python-version`: The Python version that was set.
- `python-version-resolved`: The full Python version of the activated venv, including prerelease suffixes.
Empty when `activate-environment` is false or the version cannot be determined.
- `python-runtime-id`: An opaque identifier for the activated venv's Python runtime, including
implementation, full version, and free-threaded build information. Useful as a cache-key component.
Empty when `activate-environment` is false or the runtime cannot be determined.
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found.
### Python version
+210
View File
@@ -0,0 +1,210 @@
import * as path from "node:path";
import type * as exec from "@actions/exec";
import {
afterEach,
beforeEach,
describe,
expect,
it,
jest,
} from "@jest/globals";
import { createSetupInputs } from "../helpers/setup-inputs";
const mockGetExecOutput = jest.fn<typeof exec.getExecOutput>();
const mockDebug = jest.fn();
const originalPlatform = process.platform;
jest.unstable_mockModule("@actions/core", () => ({
debug: mockDebug,
isDebug: jest.fn(() => false),
}));
jest.unstable_mockModule("@actions/exec", () => ({
getExecOutput: mockGetExecOutput,
}));
const { getPythonRuntimeId } = await import("../../src/utils/python-runtime");
function mockRuntime(overrides: Record<string, unknown> = {}) {
mockGetExecOutput.mockResolvedValue({
exitCode: 0,
stderr: "",
stdout: `${JSON.stringify({
freethreaded: false,
implementation: "cpython",
implementationVersion: [3, 13, 1, "final", 0],
pythonVersion: "3.13.1",
...overrides,
})}\r\n`,
});
}
beforeEach(() => {
mockGetExecOutput.mockReset();
mockRuntime();
});
afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform });
});
describe("getPythonRuntimeId", () => {
it("does not query Python when environment activation is disabled", async () => {
const inputs = createSetupInputs({ pythonVersion: "3.15t" });
expect(await getPythonRuntimeId(inputs)).toBe("");
expect(mockGetExecOutput).not.toHaveBeenCalled();
});
it("identifies the resolved CPython version rather than the request", async () => {
const inputs = createSetupInputs({
activateEnvironment: true,
pythonVersion: "3.13",
});
expect(await getPythonRuntimeId(inputs)).toBe("cpython-3.13.1");
expect(inputs.pythonVersion).toBe("3.13");
});
it.each([
["3.15.0a1", false, "cpython-3.15.0a1"],
["3.15.0b2", false, "cpython-3.15.0b2"],
["3.15.0rc1", false, "cpython-3.15.0rc1"],
["3.15.0rc1", true, "cpython-3.15.0rc1-freethreaded"],
["3.15.0rc2", true, "cpython-3.15.0rc2-freethreaded"],
["3.15.0", true, "cpython-3.15.0-freethreaded"],
])(
"preserves version %s and free-threading=%s as %s",
async (pythonVersion, freethreaded, expected) => {
mockRuntime({ freethreaded, pythonVersion });
expect(
await getPythonRuntimeId(
createSetupInputs({ activateEnvironment: true }),
),
).toBe(expected);
},
);
it("derives free-threading from the interpreter rather than the input", async () => {
const inputs = createSetupInputs({
activateEnvironment: true,
pythonVersion: "3.13.1t",
});
expect(await getPythonRuntimeId(inputs)).toBe("cpython-3.13.1");
});
it.each([
[[7, 3, 23, "final", 0], "pypy-7.3.23-python-3.11.15"],
[[7, 3, 24, "final", 0], "pypy-7.3.24-python-3.11.15"],
[[7, 3, 24, "alpha", 1], "pypy-7.3.24a1-python-3.11.15"],
[[7, 3, 24, "beta", 2], "pypy-7.3.24b2-python-3.11.15"],
[[7, 3, 24, "candidate", 3], "pypy-7.3.24rc3-python-3.11.15"],
])("includes PyPy implementation version %j", async (version, expected) => {
mockRuntime({
implementation: "pypy",
implementationVersion: version,
pythonVersion: "3.11.15",
});
expect(
await getPythonRuntimeId(
createSetupInputs({ activateEnvironment: true }),
),
).toBe(expected);
});
it("includes the name and version of other Python implementations", async () => {
mockRuntime({
implementation: "graalpy",
implementationVersion: [25, 0, 0, "final", 0],
pythonVersion: "3.12.8",
});
expect(
await getPythonRuntimeId(
createSetupInputs({ activateEnvironment: true }),
),
).toBe("graalpy-25.0.0-python-3.12.8");
});
it.each(["linux", "darwin", "win32"])(
"queries the custom venv directly on %s, including paths with spaces",
async (platform) => {
Object.defineProperty(process, "platform", { value: platform });
const inputs = createSetupInputs({
activateEnvironment: true,
venvPath: "/runner temp/custom venv",
workingDirectory: "/different/project",
});
await getPythonRuntimeId(inputs);
const pythonPath =
platform === "win32"
? path.join(inputs.venvPath, "Scripts", "python.exe")
: path.join(inputs.venvPath, "bin", "python");
expect(mockGetExecOutput).toHaveBeenCalledWith(
`"${pythonPath}"`,
["-I", "-c", expect.any(String)],
{ silent: true },
);
},
);
it("escapes quotes in the venv path for @actions/exec", async () => {
Object.defineProperty(process, "platform", { value: "linux" });
const inputs = createSetupInputs({
activateEnvironment: true,
venvPath: '/workspace/a "quoted" venv',
});
await getPythonRuntimeId(inputs);
expect(mockGetExecOutput.mock.calls[0][0]).toContain('a \\"quoted\\" venv');
});
it.each(["", "not JSON", "null", "{}"])(
"returns an empty output for invalid interpreter output: %j",
async (stdout) => {
mockGetExecOutput.mockResolvedValue({ exitCode: 0, stderr: "", stdout });
expect(
await getPythonRuntimeId(
createSetupInputs({ activateEnvironment: true }),
),
).toBe("");
expect(mockDebug).toHaveBeenCalled();
},
);
it("returns an empty output for an invalid implementation version", async () => {
mockRuntime({
implementation: "pypy",
implementationVersion: [7, 3, 24, "unknown", 0],
});
expect(
await getPythonRuntimeId(
createSetupInputs({ activateEnvironment: true }),
),
).toBe("");
});
it.each([new Error("interpreter missing"), "interpreter failed"])(
"returns an empty output if the interpreter cannot be queried: %s",
async (error) => {
mockGetExecOutput.mockRejectedValue(error);
expect(
await getPythonRuntimeId(
createSetupInputs({ activateEnvironment: true }),
),
).toBe("");
expect(mockDebug).toHaveBeenCalledWith(
expect.stringContaining(error instanceof Error ? error.message : error),
);
},
);
});
-131
View File
@@ -1,131 +0,0 @@
import * as path from "node:path";
import type * as exec from "@actions/exec";
import {
afterEach,
beforeEach,
describe,
expect,
it,
jest,
} from "@jest/globals";
import { createSetupInputs } from "../helpers/setup-inputs";
const mockGetExecOutput = jest.fn<typeof exec.getExecOutput>();
const mockDebug = jest.fn();
const originalPlatform = process.platform;
jest.unstable_mockModule("@actions/core", () => ({
debug: mockDebug,
isDebug: jest.fn(() => false),
}));
jest.unstable_mockModule("@actions/exec", () => ({
getExecOutput: mockGetExecOutput,
}));
const { getResolvedPythonVersion } = await import(
"../../src/utils/python-version"
);
beforeEach(() => {
mockGetExecOutput.mockReset();
});
afterEach(() => {
Object.defineProperty(process, "platform", { value: originalPlatform });
});
describe("getResolvedPythonVersion", () => {
it("does not query Python when environment activation is disabled", async () => {
const inputs = createSetupInputs({ pythonVersion: "3.15t" });
expect(await getResolvedPythonVersion(inputs)).toBe("");
expect(mockGetExecOutput).not.toHaveBeenCalled();
});
it.each([
["3.13", "3.13.1"],
["3.13.1t", "3.13.1"],
["3.15t", "3.15.0rc1"],
["pypy3.11", "3.11.11"],
["", "3.12.9"],
])(
"resolves %j to the interpreter's version %s",
async (request, version) => {
mockGetExecOutput.mockResolvedValue({
exitCode: 0,
stderr: "",
stdout: `${version}\r\n`,
});
const inputs = createSetupInputs({
activateEnvironment: true,
pythonVersion: request,
});
expect(await getResolvedPythonVersion(inputs)).toBe(version);
expect(inputs.pythonVersion).toBe(request);
},
);
it.each(["linux", "darwin", "win32"])(
"queries the custom venv directly on %s, including paths with spaces",
async (platform) => {
Object.defineProperty(process, "platform", { value: platform });
mockGetExecOutput.mockResolvedValue({
exitCode: 0,
stderr: "",
stdout: "3.13.1\n",
});
const inputs = createSetupInputs({
activateEnvironment: true,
venvPath: "/runner temp/custom venv",
workingDirectory: "/different/project",
});
await getResolvedPythonVersion(inputs);
const pythonPath =
platform === "win32"
? path.join(inputs.venvPath, "Scripts", "python.exe")
: path.join(inputs.venvPath, "bin", "python");
expect(mockGetExecOutput).toHaveBeenCalledWith(
`"${pythonPath}"`,
["-I", "-c", "import platform; print(platform.python_version())"],
{ silent: true },
);
},
);
it("escapes quotes in the venv path for @actions/exec", async () => {
Object.defineProperty(process, "platform", { value: "linux" });
mockGetExecOutput.mockResolvedValue({
exitCode: 0,
stderr: "",
stdout: "3.13.1\n",
});
const inputs = createSetupInputs({
activateEnvironment: true,
venvPath: '/workspace/a "quoted" venv',
});
await getResolvedPythonVersion(inputs);
expect(mockGetExecOutput.mock.calls[0][0]).toContain('a \\"quoted\\" venv');
});
it.each([new Error("interpreter missing"), "interpreter failed"])(
"returns an empty output if the interpreter cannot be queried: %s",
async (error) => {
mockGetExecOutput.mockRejectedValue(error);
expect(
await getResolvedPythonVersion(
createSetupInputs({ activateEnvironment: true }),
),
).toBe("");
expect(mockDebug).toHaveBeenCalledWith(
expect.stringContaining(error instanceof Error ? error.message : error),
);
},
);
});
+1 -1
View File
@@ -79,7 +79,7 @@ outputs:
type: string
python-version:
type: string
python-version-resolved:
python-runtime-id:
type: string
python-cache-hit:
type: boolean
+2 -2
View File
@@ -107,8 +107,8 @@ outputs:
description: "Path to the activated venv if activate-environment is true"
python-version:
description: "The Python version that was set."
python-version-resolved:
description: "The full Python version of the activated venv, including prerelease suffixes. Empty when activate-environment is false or the version cannot be determined."
python-runtime-id:
description: "An opaque identifier for the activated venv's Python runtime, including implementation, full version, and free-threaded build information. Empty when activate-environment is false or the runtime cannot be determined."
python-cache-hit:
description: "A boolean value to indicate the Python cache entry was found"
runs:
Generated Vendored
+38 -9
View File
@@ -102101,9 +102101,41 @@ function getResolutionStrategy() {
);
}
// src/utils/python-version.ts
// src/utils/python-runtime.ts
var import_node_path2 = require("node:path");
async function getResolvedPythonVersion(inputs) {
var PYTHON_RUNTIME_QUERY = `
import json
import platform
import sys
import sysconfig
print(json.dumps({
"implementation": sys.implementation.name,
"implementationVersion": list(sys.implementation.version),
"pythonVersion": platform.python_version(),
"freethreaded": sysconfig.get_config_var("Py_GIL_DISABLED") == 1,
}))
`;
function formatRuntimeId(runtime) {
if (typeof runtime.implementation !== "string" || runtime.implementation === "" || typeof runtime.pythonVersion !== "string" || runtime.pythonVersion === "" || typeof runtime.freethreaded !== "boolean") {
throw new Error("Invalid Python runtime metadata");
}
let id = `cpython-${runtime.pythonVersion}`;
if (runtime.implementation !== "cpython") {
const [major2, minor2, micro, releaseLevel, serial] = runtime.implementationVersion;
const suffixes = { alpha: "a", beta: "b", candidate: "rc", final: "" };
const suffix = suffixes[releaseLevel];
if (suffix === void 0 || ![major2, minor2, micro, serial].every(
(part) => Number.isInteger(part) && part >= 0
)) {
throw new Error("Invalid Python implementation version");
}
const implementationVersion = `${major2}.${minor2}.${micro}${suffix}${suffix ? serial : ""}`;
id = `${runtime.implementation}-${implementationVersion}-python-${runtime.pythonVersion}`;
}
return runtime.freethreaded ? `${id}-freethreaded` : id;
}
async function getPythonRuntimeId(inputs) {
if (!inputs.activateEnvironment) {
return "";
}
@@ -102111,13 +102143,13 @@ async function getResolvedPythonVersion(inputs) {
try {
const { stdout } = await getExecOutput(
`"${pythonPath.replace(/"/g, '\\"')}"`,
["-I", "-c", "import platform; print(platform.python_version())"],
["-I", "-c", PYTHON_RUNTIME_QUERY],
{ silent: !isDebug() }
);
return stdout.trim();
return formatRuntimeId(JSON.parse(stdout));
} catch (error2) {
debug(
`Failed to get the activated environment's Python version. Error: ${error2 instanceof Error ? error2.message : String(error2)}`
`Failed to identify the activated environment's Python runtime. Error: ${error2 instanceof Error ? error2.message : String(error2)}`
);
return "";
}
@@ -102193,10 +102225,7 @@ async function run() {
info2(`Successfully installed uv version ${setupResult.version}`);
const detectedPythonVersion = await getPythonVersion2(inputs);
setOutput("python-version", detectedPythonVersion);
setOutput(
"python-version-resolved",
await getResolvedPythonVersion(inputs)
);
setOutput("python-runtime-id", await getPythonRuntimeId(inputs));
if (inputs.enableCache) {
await restoreCache2(inputs, detectedPythonVersion);
}
+28 -11
View File
@@ -17,18 +17,35 @@ This allows directly using it in later steps:
By default, the venv is created at `.venv` inside the `working-directory`.
With `activate-environment: true`, the `python-version-resolved` output contains the
venv's full Python version, as reported by `platform.python_version()`. For example,
a `python-version` input of `3.15t` can resolve to `3.15.0rc1`. Use
`${{ steps.setup-uv.outputs.python-version-resolved }}` in later steps to access
the resolved version (with `id: setup-uv` on the setup step).
With `activate-environment: true`, the `python-runtime-id` output identifies the
venv's Python runtime using its implementation, full Python version (including
prerelease numbers), and whether the interpreter was built for free threading.
For implementations other than CPython, it also includes the implementation's own
version, so upgrading PyPy changes the identifier even if its Python version stays
the same.
This is the Python language version: it includes prerelease suffixes, but no
implementation name or free-threaded marker. For cache keys that distinguish PyPy
from CPython or free-threaded builds, include the requested version as well.
The output is empty when `activate-environment` is false or the version cannot be
determined. The existing `python-version` output and setup-uv's cache keys are
unaffected.
Example identifiers:
| Runtime | `python-runtime-id` |
| --- | --- |
| CPython RC1 | `cpython-3.15.0rc1` |
| Free-threaded CPython RC1 | `cpython-3.15.0rc1-freethreaded` |
| Free-threaded CPython RC2 | `cpython-3.15.0rc2-freethreaded` |
| PyPy | `pypy-7.3.23-python-3.11.15` |
This is an opaque identifier in a setup-uv-defined format, not a Python version
specifier. Use the whole value as a cache-key component rather than parsing it.
Combine it with the platform and dependency information relevant to your cache,
for example (with `id: setup-uv` on the setup step):
```yaml
key: build-${{ runner.os }}-${{ runner.arch }}-${{ steps.setup-uv.outputs.python-runtime-id }}-${{ hashFiles('uv.lock') }}
```
The free-threaded marker describes the interpreter's build even when the GIL is
enabled at runtime. The output is empty when `activate-environment` is false or the
runtime cannot be determined. The existing `python-version` output and setup-uv's
cache keys are unaffected.
You can customize the venv location with `venv-path`, for example to place it in the runner temp directory:
+2 -5
View File
@@ -16,7 +16,7 @@ import {
getPlatform,
type Platform,
} from "./utils/platforms";
import { getResolvedPythonVersion } from "./utils/python-version";
import { getPythonRuntimeId } from "./utils/python-runtime";
import { resolveUvVersion } from "./version/resolve";
const sourceDir = __dirname;
@@ -102,10 +102,7 @@ async function run(): Promise<void> {
const detectedPythonVersion = await getPythonVersion(inputs);
core.setOutput("python-version", detectedPythonVersion);
core.setOutput(
"python-version-resolved",
await getResolvedPythonVersion(inputs),
);
core.setOutput("python-runtime-id", await getPythonRuntimeId(inputs));
if (inputs.enableCache) {
await restoreCache(inputs, detectedPythonVersion);
+85
View File
@@ -0,0 +1,85 @@
import { join } from "node:path";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import type { SetupInputs } from "./inputs";
const PYTHON_RUNTIME_QUERY = `
import json
import platform
import sys
import sysconfig
print(json.dumps({
"implementation": sys.implementation.name,
"implementationVersion": list(sys.implementation.version),
"pythonVersion": platform.python_version(),
"freethreaded": sysconfig.get_config_var("Py_GIL_DISABLED") == 1,
}))
`;
type ReleaseLevel = "alpha" | "beta" | "candidate" | "final";
interface PythonRuntime {
implementation: string;
implementationVersion: [number, number, number, ReleaseLevel, number];
pythonVersion: string;
freethreaded: boolean;
}
function formatRuntimeId(runtime: PythonRuntime): string {
if (
typeof runtime.implementation !== "string" ||
runtime.implementation === "" ||
typeof runtime.pythonVersion !== "string" ||
runtime.pythonVersion === "" ||
typeof runtime.freethreaded !== "boolean"
) {
throw new Error("Invalid Python runtime metadata");
}
let id = `cpython-${runtime.pythonVersion}`;
if (runtime.implementation !== "cpython") {
const [major, minor, micro, releaseLevel, serial] =
runtime.implementationVersion;
const suffixes = { alpha: "a", beta: "b", candidate: "rc", final: "" };
const suffix = suffixes[releaseLevel];
if (
suffix === undefined ||
![major, minor, micro, serial].every(
(part) => Number.isInteger(part) && part >= 0,
)
) {
throw new Error("Invalid Python implementation version");
}
const implementationVersion = `${major}.${minor}.${micro}${suffix}${suffix ? serial : ""}`;
id = `${runtime.implementation}-${implementationVersion}-python-${runtime.pythonVersion}`;
}
return runtime.freethreaded ? `${id}-freethreaded` : id;
}
export async function getPythonRuntimeId(inputs: SetupInputs): Promise<string> {
if (!inputs.activateEnvironment) {
return "";
}
const pythonPath =
process.platform === "win32"
? join(inputs.venvPath, "Scripts", "python.exe")
: join(inputs.venvPath, "bin", "python");
try {
// @actions/exec parses the executable as a command line, so quote the path.
const { stdout } = await exec.getExecOutput(
`"${pythonPath.replace(/"/g, '\\"')}"`,
["-I", "-c", PYTHON_RUNTIME_QUERY],
{ silent: !core.isDebug() },
);
return formatRuntimeId(JSON.parse(stdout));
} catch (error) {
core.debug(
`Failed to identify the activated environment's Python runtime. Error: ${error instanceof Error ? error.message : String(error)}`,
);
return "";
}
}
-32
View File
@@ -1,32 +0,0 @@
import { join } from "node:path";
import * as core from "@actions/core";
import * as exec from "@actions/exec";
import type { SetupInputs } from "./inputs";
export async function getResolvedPythonVersion(
inputs: SetupInputs,
): Promise<string> {
if (!inputs.activateEnvironment) {
return "";
}
const pythonPath =
process.platform === "win32"
? join(inputs.venvPath, "Scripts", "python.exe")
: join(inputs.venvPath, "bin", "python");
try {
// @actions/exec parses the executable as a command line, so quote the path.
const { stdout } = await exec.getExecOutput(
`"${pythonPath.replace(/"/g, '\\"')}"`,
["-I", "-c", "import platform; print(platform.python_version())"],
{ silent: !core.isDebug() },
);
return stdout.trim();
} catch (error) {
core.debug(
`Failed to get the activated environment's Python version. Error: ${error instanceof Error ? error.message : String(error)}`,
);
return "";
}
}