From 45fe5a27c5243264ceb0f877014ef9ed4e29dd09 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Tue, 3 Feb 2026 15:25:04 +0100 Subject: [PATCH] chore: normalize venv-path and warn when unused - Normalize venv-path and trim trailing separators - Improve activation log message - Warn when venv-path is set without activate-environment - Add test coverage for trailing slash + warning --- __tests__/utils/inputs.test.ts | 34 ++++++++++++++++++++++++++++++++++ dist/save-cache/index.js | 17 +++++++++++++++-- dist/setup/index.js | 19 ++++++++++++++++--- src/setup-uv.ts | 2 +- src/utils/inputs.ts | 20 ++++++++++++++++++-- 5 files changed, 84 insertions(+), 8 deletions(-) diff --git a/__tests__/utils/inputs.test.ts b/__tests__/utils/inputs.test.ts index bbfc4c8..7d58c5c 100644 --- a/__tests__/utils/inputs.test.ts +++ b/__tests__/utils/inputs.test.ts @@ -5,6 +5,7 @@ jest.mock("@actions/core", () => { (name: string) => (mockInputs[name] ?? "") === "true", ), getInput: jest.fn((name: string) => mockInputs[name] ?? ""), + warning: jest.fn(), }; }); @@ -24,6 +25,7 @@ const ORIGINAL_HOME = process.env.HOME; describe("cacheDependencyGlob", () => { beforeEach(() => { jest.resetModules(); + jest.clearAllMocks(); mockInputs = {}; process.env.HOME = "/home/testuser"; }); @@ -88,6 +90,7 @@ describe("cacheDependencyGlob", () => { describe("venvPath", () => { beforeEach(() => { jest.resetModules(); + jest.clearAllMocks(); mockInputs = {}; process.env.HOME = "/home/testuser"; }); @@ -104,13 +107,23 @@ describe("venvPath", () => { it("resolves a relative venv-path", async () => { mockInputs["working-directory"] = "/workspace"; + mockInputs["activate-environment"] = "true"; mockInputs["venv-path"] = "custom-venv"; const { venvPath } = await import("../../src/utils/inputs"); expect(venvPath).toBe("/workspace/custom-venv"); }); + it("normalizes venv-path with trailing slash", async () => { + mockInputs["working-directory"] = "/workspace"; + mockInputs["activate-environment"] = "true"; + mockInputs["venv-path"] = "custom-venv/"; + const { venvPath } = await import("../../src/utils/inputs"); + expect(venvPath).toBe("/workspace/custom-venv"); + }); + it("keeps an absolute venv-path unchanged", async () => { mockInputs["working-directory"] = "/workspace"; + mockInputs["activate-environment"] = "true"; mockInputs["venv-path"] = "/tmp/custom-venv"; const { venvPath } = await import("../../src/utils/inputs"); expect(venvPath).toBe("/tmp/custom-venv"); @@ -118,8 +131,29 @@ describe("venvPath", () => { it("expands tilde in venv-path", async () => { mockInputs["working-directory"] = "/workspace"; + mockInputs["activate-environment"] = "true"; mockInputs["venv-path"] = "~/.venv"; const { venvPath } = await import("../../src/utils/inputs"); expect(venvPath).toBe("/home/testuser/.venv"); }); + + it("warns when venv-path is set but activate-environment is false", async () => { + mockInputs["working-directory"] = "/workspace"; + mockInputs["venv-path"] = "custom-venv"; + + const { activateEnvironment, venvPath } = await import( + "../../src/utils/inputs" + ); + + expect(activateEnvironment).toBe(false); + expect(venvPath).toBe("/workspace/custom-venv"); + + const mockedCore = jest.requireMock("@actions/core") as { + warning: jest.Mock; + }; + + expect(mockedCore.warning).toHaveBeenCalledWith( + "venv-path is only used when activate-environment is true", + ); + }); }); diff --git a/dist/save-cache/index.js b/dist/save-cache/index.js index f5b369c..23586bd 100644 --- a/dist/save-cache/index.js +++ b/dist/save-cache/index.js @@ -91079,10 +91079,13 @@ function getVersionFile() { function getVenvPath() { const venvPathInput = core.getInput("venv-path"); if (venvPathInput !== "") { + if (!exports.activateEnvironment) { + core.warning("venv-path is only used when activate-environment is true"); + } const tildeExpanded = expandTilde(venvPathInput); - return resolveRelativePath(tildeExpanded); + return normalizePath(resolveRelativePath(tildeExpanded)); } - return resolveRelativePath(".venv"); + return normalizePath(resolveRelativePath(".venv")); } function getEnableCache() { const enableCacheInput = core.getInput("enable-cache"); @@ -91212,6 +91215,16 @@ function expandTilde(input) { } return input; } +function normalizePath(inputPath) { + const normalized = node_path_1.default.normalize(inputPath); + const root = node_path_1.default.parse(normalized).root; + // Remove any trailing path separators, except when the whole path is the root. + let trimmed = normalized; + while (trimmed.length > root.length && trimmed.endsWith(node_path_1.default.sep)) { + trimmed = trimmed.slice(0, -1); + } + return trimmed; +} function resolveRelativePath(inputPath) { const hasNegation = inputPath.startsWith("!"); const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath; diff --git a/dist/setup/index.js b/dist/setup/index.js index 04706f3..bcf0648 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -96532,7 +96532,7 @@ async function activateEnvironment() { if (process.env.UV_NO_MODIFY_PATH !== undefined) { throw new Error("UV_NO_MODIFY_PATH and activate-environment cannot be used together."); } - core.info(`Activating python venv at ${inputs_1.venvPath}...`); + core.info(`Creating and activating python venv at ${inputs_1.venvPath}...`); await exec.exec("uv", ["venv", inputs_1.venvPath, "--directory", inputs_1.workingDirectory]); let venvBinPath = `${inputs_1.venvPath}${path.sep}bin`; if (process.platform === "win32") { @@ -96759,10 +96759,13 @@ function getVersionFile() { function getVenvPath() { const venvPathInput = core.getInput("venv-path"); if (venvPathInput !== "") { + if (!exports.activateEnvironment) { + core.warning("venv-path is only used when activate-environment is true"); + } const tildeExpanded = expandTilde(venvPathInput); - return resolveRelativePath(tildeExpanded); + return normalizePath(resolveRelativePath(tildeExpanded)); } - return resolveRelativePath(".venv"); + return normalizePath(resolveRelativePath(".venv")); } function getEnableCache() { const enableCacheInput = core.getInput("enable-cache"); @@ -96892,6 +96895,16 @@ function expandTilde(input) { } return input; } +function normalizePath(inputPath) { + const normalized = node_path_1.default.normalize(inputPath); + const root = node_path_1.default.parse(normalized).root; + // Remove any trailing path separators, except when the whole path is the root. + let trimmed = normalized; + while (trimmed.length > root.length && trimmed.endsWith(node_path_1.default.sep)) { + trimmed = trimmed.slice(0, -1); + } + return trimmed; +} function resolveRelativePath(inputPath) { const hasNegation = inputPath.startsWith("!"); const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath; diff --git a/src/setup-uv.ts b/src/setup-uv.ts index 1a23d1d..ce556af 100644 --- a/src/setup-uv.ts +++ b/src/setup-uv.ts @@ -271,7 +271,7 @@ async function activateEnvironment(): Promise { ); } - core.info(`Activating python venv at ${venvPath}...`); + core.info(`Creating and activating python venv at ${venvPath}...`); await exec.exec("uv", ["venv", venvPath, "--directory", workingDirectory]); let venvBinPath = `${venvPath}${path.sep}bin`; diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts index 59e2fa9..4c189d6 100644 --- a/src/utils/inputs.ts +++ b/src/utils/inputs.ts @@ -49,10 +49,13 @@ function getVersionFile(): string { function getVenvPath(): string { const venvPathInput = core.getInput("venv-path"); if (venvPathInput !== "") { + if (!activateEnvironment) { + core.warning("venv-path is only used when activate-environment is true"); + } const tildeExpanded = expandTilde(venvPathInput); - return resolveRelativePath(tildeExpanded); + return normalizePath(resolveRelativePath(tildeExpanded)); } - return resolveRelativePath(".venv"); + return normalizePath(resolveRelativePath(".venv")); } function getEnableCache(): boolean { @@ -204,6 +207,19 @@ function expandTilde(input: string): string { return input; } +function normalizePath(inputPath: string): string { + const normalized = path.normalize(inputPath); + const root = path.parse(normalized).root; + + // Remove any trailing path separators, except when the whole path is the root. + let trimmed = normalized; + while (trimmed.length > root.length && trimmed.endsWith(path.sep)) { + trimmed = trimmed.slice(0, -1); + } + + return trimmed; +} + function resolveRelativePath(inputPath: string): string { const hasNegation = inputPath.startsWith("!"); const pathWithoutNegation = hasNegation ? inputPath.substring(1) : inputPath;