feat: add venv-path input for activate-environment

Allow customizing the venv location while preserving working-directory semantics via --directory.
This commit is contained in:
Kevin Stillhammer 2026-02-03 14:41:13 +01:00
parent 98e1309028
commit 4b51b416f8
No known key found for this signature in database
9 changed files with 151 additions and 15 deletions

View file

@ -386,6 +386,60 @@ jobs:
env:
UV_VENV: ${{ steps.setup-uv.outputs.venv }}
test-activate-environment-custom-path:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
persist-credentials: false
- name: Install latest version
id: setup-uv
uses: ./
with:
python-version: 3.13.1t
activate-environment: true
venv-path: ${{ runner.temp }}/custom-venv
- name: Verify VIRTUAL_ENV matches output
run: |
if [ "$VIRTUAL_ENV" != "$UV_VENV" ]; then
echo "VIRTUAL_ENV does not match venv output: $VIRTUAL_ENV vs $UV_VENV"
exit 1
fi
shell: bash
env:
UV_VENV: ${{ steps.setup-uv.outputs.venv }}
- name: Verify venv location is runner.temp/custom-venv
run: |
python - <<'PY'
import os
from pathlib import Path
venv = Path(os.environ["VIRTUAL_ENV"]).resolve()
temp = Path(os.environ["RUNNER_TEMP"]).resolve()
if venv.name != "custom-venv":
raise SystemExit(f"Expected venv name 'custom-venv', got: {venv}")
if venv.parent != temp:
raise SystemExit(f"Expected venv under {temp}, got: {venv}")
if not venv.is_dir():
raise SystemExit(f"Venv directory does not exist: {venv}")
PY
shell: bash
- name: Verify packages can be installed
run: uv pip install pip
shell: bash
- name: Verify python runs from custom venv
run: |
python - <<'PY'
import sys
if "custom-venv" not in sys.executable:
raise SystemExit(f"Python is not running from custom venv: {sys.executable}")
PY
shell: bash
test-musl:
runs-on: ubuntu-latest
container: alpine
@ -1069,6 +1123,7 @@ jobs:
- test-tilde-expansion-tool-dirs
- test-python-version
- test-activate-environment
- test-activate-environment-custom-path
- test-musl
- test-cache-key-os-version
- test-cache-local

View file

@ -59,6 +59,9 @@ Have a look under [Advanced Configuration](#advanced-configuration) for detailed
# Use uv venv to activate a venv ready to be used by later steps
activate-environment: "false"
# Custom path for the virtual environment when using activate-environment (default: .venv in the working directory)
venv-path: ""
# The directory to execute all commands in and look for files such as pyproject.toml
working-directory: ""
@ -167,7 +170,7 @@ You can set the working directory with the `working-directory` input.
This controls where we look for `pyproject.toml`, `uv.toml` and `.python-version` files
which are used to determine the version of uv and python to install.
It also controls where [the venv gets created](#activate-environment).
It also controls where [the venv gets created](#activate-environment), unless `venv-path` is set.
```yaml
- name: Install uv based on the config files in the working-directory

View file

@ -84,3 +84,42 @@ describe("cacheDependencyGlob", () => {
);
});
});
describe("venvPath", () => {
beforeEach(() => {
jest.resetModules();
mockInputs = {};
process.env.HOME = "/home/testuser";
});
afterEach(() => {
process.env.HOME = ORIGINAL_HOME;
});
it("defaults to .venv in the working directory", async () => {
mockInputs["working-directory"] = "/workspace";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/workspace/.venv");
});
it("resolves a relative venv-path", async () => {
mockInputs["working-directory"] = "/workspace";
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["venv-path"] = "/tmp/custom-venv";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/tmp/custom-venv");
});
it("expands tilde in venv-path", async () => {
mockInputs["working-directory"] = "/workspace";
mockInputs["venv-path"] = "~/.venv";
const { venvPath } = await import("../../src/utils/inputs");
expect(venvPath).toBe("/home/testuser/.venv");
});
});

View file

@ -15,6 +15,9 @@ inputs:
activate-environment:
description: "Use uv venv to activate a venv ready to be used by later steps. "
default: "false"
venv-path:
description: "Custom path for the virtual environment when using activate-environment. Defaults to '.venv' in the working directory."
default: ""
working-directory:
description: "The directory to execute all commands in and look for files such as pyproject.toml"
default: ${{ github.workspace }}

11
dist/save-cache/index.js generated vendored
View file

@ -91032,7 +91032,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.resolutionStrategy = exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = exports.CacheLocalSource = void 0;
exports.resolutionStrategy = exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.venvPath = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = exports.CacheLocalSource = void 0;
exports.getUvPythonDir = getUvPythonDir;
const node_path_1 = __importDefault(__nccwpck_require__(6760));
const core = __importStar(__nccwpck_require__(7484));
@ -91049,6 +91049,7 @@ exports.version = core.getInput("version");
exports.versionFile = getVersionFile();
exports.pythonVersion = core.getInput("python-version");
exports.activateEnvironment = core.getBooleanInput("activate-environment");
exports.venvPath = getVenvPath();
exports.checkSum = core.getInput("checksum");
exports.enableCache = getEnableCache();
exports.restoreCache = core.getInput("restore-cache") === "true";
@ -91075,6 +91076,14 @@ function getVersionFile() {
}
return versionFileInput;
}
function getVenvPath() {
const venvPathInput = core.getInput("venv-path");
if (venvPathInput !== "") {
const tildeExpanded = expandTilde(venvPathInput);
return resolveRelativePath(tildeExpanded);
}
return resolveRelativePath(".venv");
}
function getEnableCache() {
const enableCacheInput = core.getInput("enable-cache");
if (enableCacheInput === "auto") {

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

@ -96532,17 +96532,15 @@ 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.");
}
const execArgs = ["venv", ".venv", "--directory", inputs_1.workingDirectory];
core.info("Activating python venv...");
await exec.exec("uv", execArgs);
const venvPath = path.resolve(`${inputs_1.workingDirectory}${path.sep}.venv`);
let venvBinPath = `${venvPath}${path.sep}bin`;
core.info(`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") {
venvBinPath = `${venvPath}${path.sep}Scripts`;
venvBinPath = `${inputs_1.venvPath}${path.sep}Scripts`;
}
core.addPath(path.resolve(venvBinPath));
core.exportVariable("VIRTUAL_ENV", venvPath);
core.setOutput("venv", venvPath);
core.exportVariable("VIRTUAL_ENV", inputs_1.venvPath);
core.setOutput("venv", inputs_1.venvPath);
}
}
function setCacheDir() {
@ -96714,7 +96712,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.resolutionStrategy = exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = exports.CacheLocalSource = void 0;
exports.resolutionStrategy = exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.pythonDir = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = exports.cachePython = exports.pruneCache = exports.cacheDependencyGlob = exports.cacheLocalPath = exports.cacheSuffix = exports.saveCache = exports.restoreCache = exports.enableCache = exports.checkSum = exports.venvPath = exports.activateEnvironment = exports.pythonVersion = exports.versionFile = exports.version = exports.workingDirectory = exports.CacheLocalSource = void 0;
exports.getUvPythonDir = getUvPythonDir;
const node_path_1 = __importDefault(__nccwpck_require__(6760));
const core = __importStar(__nccwpck_require__(7484));
@ -96731,6 +96729,7 @@ exports.version = core.getInput("version");
exports.versionFile = getVersionFile();
exports.pythonVersion = core.getInput("python-version");
exports.activateEnvironment = core.getBooleanInput("activate-environment");
exports.venvPath = getVenvPath();
exports.checkSum = core.getInput("checksum");
exports.enableCache = getEnableCache();
exports.restoreCache = core.getInput("restore-cache") === "true";
@ -96757,6 +96756,14 @@ function getVersionFile() {
}
return versionFileInput;
}
function getVenvPath() {
const venvPathInput = core.getInput("venv-path");
if (venvPathInput !== "") {
const tildeExpanded = expandTilde(venvPathInput);
return resolveRelativePath(tildeExpanded);
}
return resolveRelativePath(".venv");
}
function getEnableCache() {
const enableCacheInput = core.getInput("enable-cache");
if (enableCacheInput === "auto") {

View file

@ -15,6 +15,17 @@ This allows directly using it in later steps:
- run: uv pip install pip
```
By default, the venv is created at `.venv` inside the `working-directory`.
You can customize the venv location with `venv-path`, for example to place it in the runner temp directory:
```yaml
- uses: astral-sh/setup-uv@v7
with:
activate-environment: true
venv-path: ${{ runner.temp }}/custom-venv
```
> [!WARNING]
>
> Activating the environment adds your dependencies to the `PATH`, which could break some workflows.

View file

@ -24,6 +24,7 @@ import {
resolutionStrategy,
toolBinDir,
toolDir,
venvPath,
versionFile as versionFileInput,
version as versionInput,
workingDirectory,
@ -269,12 +270,10 @@ async function activateEnvironment(): Promise<void> {
"UV_NO_MODIFY_PATH and activate-environment cannot be used together.",
);
}
const execArgs = ["venv", ".venv", "--directory", workingDirectory];
core.info("Activating python venv...");
await exec.exec("uv", execArgs);
core.info(`Activating python venv at ${venvPath}...`);
await exec.exec("uv", ["venv", venvPath, "--directory", workingDirectory]);
const venvPath = path.resolve(`${workingDirectory}${path.sep}.venv`);
let venvBinPath = `${venvPath}${path.sep}bin`;
if (process.platform === "win32") {
venvBinPath = `${venvPath}${path.sep}Scripts`;

View file

@ -14,6 +14,7 @@ export const version = core.getInput("version");
export const versionFile = getVersionFile();
export const pythonVersion = core.getInput("python-version");
export const activateEnvironment = core.getBooleanInput("activate-environment");
export const venvPath = getVenvPath();
export const checkSum = core.getInput("checksum");
export const enableCache = getEnableCache();
export const restoreCache = core.getInput("restore-cache") === "true";
@ -45,6 +46,15 @@ function getVersionFile(): string {
return versionFileInput;
}
function getVenvPath(): string {
const venvPathInput = core.getInput("venv-path");
if (venvPathInput !== "") {
const tildeExpanded = expandTilde(venvPathInput);
return resolveRelativePath(tildeExpanded);
}
return resolveRelativePath(".venv");
}
function getEnableCache(): boolean {
const enableCacheInput = core.getInput("enable-cache");
if (enableCacheInput === "auto") {