Fail setup when activated Python runtime detection fails

This commit is contained in:
William Woodruff
2026-09-03 15:48:24 -04:00
parent 18b7c1ee5f
commit 358b824344
6 changed files with 34 additions and 30 deletions
+1 -1
View File
@@ -140,7 +140,7 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
- `python-version`: The Python version that was set. - `python-version`: The Python version that was set.
- `python-runtime-id`: An opaque identifier for the activated venv's Python runtime, including - `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. 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. Empty when `activate-environment` is false. The action fails if the activated runtime cannot be determined.
- `python-cache-hit`: A boolean value to indicate the Python cache entry was found. - `python-cache-hit`: A boolean value to indicate the Python cache entry was found.
### Python version ### Python version
+17 -12
View File
@@ -7,14 +7,12 @@ const mockExecFile =
jest.fn< jest.fn<
(...args: unknown[]) => Promise<{ stdout: string; stderr: string }> (...args: unknown[]) => Promise<{ stdout: string; stderr: string }>
>(); >();
const mockDebug = jest.fn();
const originalPlatform = process.platform; const originalPlatform = process.platform;
const inputs = createSetupInputs({ const inputs = createSetupInputs({
activateEnvironment: true, activateEnvironment: true,
pythonVersion: "3.15t", pythonVersion: "3.15t",
}); });
jest.unstable_mockModule("@actions/core", () => ({ debug: mockDebug }));
jest.unstable_mockModule("node:child_process", () => ({ jest.unstable_mockModule("node:child_process", () => ({
// execFile's custom promisifier returns both stdout and stderr. // execFile's custom promisifier returns both stdout and stderr.
execFile: Object.assign(mockExecFile, { [promisify.custom]: mockExecFile }), execFile: Object.assign(mockExecFile, { [promisify.custom]: mockExecFile }),
@@ -70,16 +68,13 @@ it.each([
["pypy", [7, 3, 24, "beta", 2], "pypy-7.3.24b2"], ["pypy", [7, 3, 24, "beta", 2], "pypy-7.3.24b2"],
["pypy", [7, 3, 24, "candidate", 3], "pypy-7.3.24rc3"], ["pypy", [7, 3, 24, "candidate", 3], "pypy-7.3.24rc3"],
["graalpy", [25, 0, 0, "final", 0], "graalpy-25.0.0"], ["graalpy", [25, 0, 0, "final", 0], "graalpy-25.0.0"],
["pypy", [7, 3, 24, "unknown", 0], ""],
])("formats %s implementation version %j", async (name, version, expected) => { ])("formats %s implementation version %j", async (name, version, expected) => {
mockRuntime({ mockRuntime({
implementation: name, implementation: name,
implementationVersion: version, implementationVersion: version,
pythonVersion: "3.11.15", pythonVersion: "3.11.15",
}); });
expect(await getPythonRuntimeId(inputs)).toBe( expect(await getPythonRuntimeId(inputs)).toBe(`${expected}-python-3.11.15`);
expected ? `${expected}-python-3.11.15` : "",
);
}); });
it.each([ it.each([
@@ -97,15 +92,25 @@ it.each([
); );
}); });
it.each([new Error("interpreter missing"), "", "not JSON", "null", "{}"])( it.each([
"returns an empty ID for interpreter failure: %s", new Error("interpreter missing"),
async (result) => { "",
"not JSON",
"null",
"{}",
JSON.stringify({
freethreaded: false,
implementation: "pypy",
implementationVersion: [7, 3, 24, "unknown", 0],
pythonVersion: "3.11.15",
}),
])("rejects interpreter failure or invalid metadata: %s", async (result) => {
if (result instanceof Error) { if (result instanceof Error) {
mockExecFile.mockRejectedValue(result); mockExecFile.mockRejectedValue(result);
} else { } else {
mockExecFile.mockResolvedValue({ stderr: "", stdout: result }); mockExecFile.mockResolvedValue({ stderr: "", stdout: result });
} }
expect(await getPythonRuntimeId(inputs)).toBe(""); await expect(getPythonRuntimeId(inputs)).rejects.toThrow(
expect(mockDebug).toHaveBeenCalled(); "Failed to identify the activated environment's Python runtime:",
},
); );
});
+1 -1
View File
@@ -108,7 +108,7 @@ outputs:
python-version: python-version:
description: "The Python version that was set." description: "The Python version that was set."
python-runtime-id: 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." 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. The action fails if the activated runtime cannot be determined."
python-cache-hit: python-cache-hit:
description: "A boolean value to indicate the Python cache entry was found" description: "A boolean value to indicate the Python cache entry was found"
runs: runs:
Generated Vendored
+3 -3
View File
@@ -102116,10 +102116,10 @@ async function getPythonRuntimeId(inputs) {
); );
return formatRuntimeId(JSON.parse(stdout)); return formatRuntimeId(JSON.parse(stdout));
} catch (error2) { } catch (error2) {
debug( throw new Error(
`Failed to identify the activated environment's Python runtime. Error: ${error2 instanceof Error ? error2.message : String(error2)}` `Failed to identify the activated environment's Python runtime: ${error2 instanceof Error ? error2.message : String(error2)}`,
{ cause: error2 }
); );
return "";
} }
} }
+3 -3
View File
@@ -30,9 +30,9 @@ key: build-${{ runner.os }}-${{ runner.arch }}-${{ steps.setup-uv.outputs.python
``` ```
The free-threaded marker describes the interpreter's build even when the GIL is 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 enabled at runtime. The output is empty when `activate-environment` is false. If
runtime cannot be determined. The existing `python-version` output and setup-uv's the activated runtime cannot be determined, the action fails. The existing
cache keys are unaffected. `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: You can customize the venv location with `venv-path`, for example to place it in the runner temp directory:
+3 -4
View File
@@ -1,7 +1,6 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
import { join } from "node:path"; import { join } from "node:path";
import { promisify } from "node:util"; import { promisify } from "node:util";
import * as core from "@actions/core";
import type { SetupInputs } from "./inputs"; import type { SetupInputs } from "./inputs";
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);
@@ -79,9 +78,9 @@ export async function getPythonRuntimeId(inputs: SetupInputs): Promise<string> {
); );
return formatRuntimeId(JSON.parse(stdout)); return formatRuntimeId(JSON.parse(stdout));
} catch (error) { } catch (error) {
core.debug( throw new Error(
`Failed to identify the activated environment's Python runtime. Error: ${error instanceof Error ? error.message : String(error)}`, `Failed to identify the activated environment's Python runtime: ${error instanceof Error ? error.message : String(error)}`,
{ cause: error },
); );
return "";
} }
} }