diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1767de9..3264ed7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: with: persist-credentials: false - name: Actionlint - uses: eifinger/actionlint-action@23c85443d840cd73bbecb9cddfc933cc21649a38 # v1.9.1 + uses: eifinger/actionlint-action@23c85443d840cd73bbecb9cddfc933cc21649a38 # v1.9.1 - name: Run zizmor uses: zizmorcore/zizmor-action@e673c3917a1aef3c65c972347ed84ccd013ecda4 # v0.2.0 - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 @@ -269,13 +269,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: - [ - ubuntu-latest, - macos-latest, - macos-14, - windows-latest, - ] + os: [ubuntu-latest, macos-latest, macos-14, windows-latest] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -334,7 +328,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - os: [ ubuntu-latest, macos-latest, windows-latest ] + os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -371,8 +365,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - enable-cache: [ "true", "false", "auto" ] - os: [ "ubuntu-latest", "selfhosted-ubuntu-arm64", "windows-latest" ] + enable-cache: ["true", "false", "auto"] + os: ["ubuntu-latest", "selfhosted-ubuntu-arm64", "windows-latest"] steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 with: @@ -389,8 +383,8 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - enable-cache: [ "true", "false", "auto" ] - os: [ "ubuntu-latest", "selfhosted-ubuntu-arm64", "windows-latest" ] + enable-cache: ["true", "false", "auto"] + os: ["ubuntu-latest", "selfhosted-ubuntu-arm64", "windows-latest"] needs: test-setup-cache steps: - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 @@ -836,6 +830,68 @@ jobs: exit 1 fi + test-cache-python-installs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Verify Python install dir is not populated + run: | + if [ -d ~/.local/share/uv/python ]; then + echo "Python install dir should not exist" + exit 1 + fi + - name: Setup uv with cache + uses: ./ + with: + enable-cache: true + cache-python: true + cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-cache-python-installs + - run: uv sync --managed-python + working-directory: __tests__/fixtures/uv-project + - name: Verify Python install dir exists + run: | + if [ ! -d ~/.local/share/uv/python ]; then + echo "Python install dir should exist" + exit 1 + fi + test-restore-python-installs: + runs-on: ubuntu-latest + needs: test-cache-python-installs + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Verify Python install dir does not exist + run: | + if [ -d ~/.local/share/uv/python ]; then + echo "Python install dir should not exist" + exit 1 + fi + - name: Restore with cache + id: restore + uses: ./ + with: + enable-cache: true + cache-python: true + cache-suffix: ${{ github.run_id }}-${{ github.run_attempt }}-test-cache-python-installs + - name: Verify Python install dir exists + run: | + if [ ! -d ~/.local/share/uv/python ]; then + echo "Python install dir should exist" + exit 1 + fi + - name: Cache was hit + run: | + if [ "$CACHE_HIT" != "true" ]; then + exit 1 + fi + env: + CACHE_HIT: ${{ steps.restore.outputs.cache-hit }} + - run: uv sync --managed-python + working-directory: __tests__/fixtures/uv-project + all-tests-passed: runs-on: ubuntu-latest needs: @@ -878,6 +934,8 @@ jobs: - test-relative-path - test-cache-prune-force - test-cache-dir-from-file + - test-cache-python-installs + - test-restore-python-installs if: always() steps: - name: All tests passed diff --git a/README.md b/README.md index abd319e..b9f642a 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ Set up your GitHub Actions workflow with a specific version of [uv](https://docs - [Save cache](#save-cache) - [Local cache path](#local-cache-path) - [Disable cache pruning](#disable-cache-pruning) + - [Cache Python installs](#cache-python-installs) - [Ignore nothing to cache](#ignore-nothing-to-cache) - [GitHub authentication token](#github-authentication-token) - [UV_TOOL_DIR](#uv_tool_dir) @@ -355,6 +356,20 @@ input. prune-cache: false ``` +### Cache Python installs + +By default, the Python install dir (`uv python dir` / `UV_PYTHON_INSTALL_DIR`) is not cached, +for the same reason that the dependency cache is pruned. +If you want to cache Python installs along with your dependencies, set the `cache-python` input to `true`. + +```yaml +- name: Cache Python installs + uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + cache-python: true +``` + ### Ignore nothing to cache By default, the action will fail if caching is enabled but there is nothing to upload (the uv cache directory does not exist). diff --git a/action.yml b/action.yml index ea7133b..997ecb5 100644 --- a/action.yml +++ b/action.yml @@ -56,6 +56,9 @@ inputs: prune-cache: description: "Prune cache before saving." default: "true" + cache-python: + description: "Upload managed Python installations to the Github Actions cache." + default: "false" ignore-nothing-to-cache: description: "Ignore when nothing is found to cache." default: "false" diff --git a/dist/save-cache/index.js b/dist/save-cache/index.js index 5fc3dd8..0100416 100644 --- a/dist/save-cache/index.js +++ b/dist/save-cache/index.js @@ -90593,8 +90593,12 @@ async function restoreCache() { } let matchedKey; core.info(`Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`); + const cachePaths = [inputs_1.cacheLocalPath]; + if (inputs_1.cachePython) { + cachePaths.push(await (0, inputs_1.getUvPythonDir)()); + } try { - matchedKey = await cache.restoreCache([inputs_1.cacheLocalPath], cacheKey); + matchedKey = await cache.restoreCache(cachePaths, cacheKey); } catch (err) { const message = err.message; @@ -90620,7 +90624,8 @@ async function computeKeys() { const pythonVersion = await getPythonVersion(); const platform = await (0, platforms_1.getPlatform)(); const pruned = inputs_1.pruneCache ? "-pruned" : ""; - return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; + const python = inputs_1.cachePython ? "-py" : ""; + return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`; } async function getPythonVersion() { if (inputs_1.pythonVersion !== "") { @@ -90844,8 +90849,18 @@ async function saveCache() { if (!fs.existsSync(actualCachePath) && !inputs_1.ignoreNothingToCache) { throw new Error(`Cache path ${actualCachePath} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`); } + const cachePaths = [actualCachePath]; + if (inputs_1.cachePython) { + const pythonDir = await (0, inputs_1.getUvPythonDir)(); + core.info(`Including Python cache path: ${pythonDir}`); + if (!fs.existsSync(pythonDir) && !inputs_1.ignoreNothingToCache) { + throw new Error(`Python cache path ${pythonDir} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`); + } + cachePaths.push(pythonDir); + } + core.info(`Final cache paths: ${cachePaths.join(", ")}`); try { - await cache.saveCache([actualCachePath], cacheKey); + await cache.saveCache(cachePaths, cacheKey); core.info(`cache saved with the key: ${cacheKey}`); } catch (e) { @@ -90996,9 +91011,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = 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 = void 0; +exports.addProblemMatchers = exports.manifestFile = exports.githubToken = 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 = void 0; +exports.getUvPythonDir = getUvPythonDir; const node_path_1 = __importDefault(__nccwpck_require__(6760)); const core = __importStar(__nccwpck_require__(7484)); +const exec = __importStar(__nccwpck_require__(5236)); const config_file_1 = __nccwpck_require__(5465); exports.workingDirectory = core.getInput("working-directory"); exports.version = core.getInput("version"); @@ -91013,6 +91030,7 @@ exports.cacheSuffix = core.getInput("cache-suffix") || ""; exports.cacheLocalPath = getCacheLocalPath(); exports.cacheDependencyGlob = getCacheDependencyGlob(); exports.pruneCache = core.getInput("prune-cache") === "true"; +exports.cachePython = core.getInput("cache-python") === "true"; exports.ignoreNothingToCache = core.getInput("ignore-nothing-to-cache") === "true"; exports.ignoreEmptyWorkdir = core.getInput("ignore-empty-workdir") === "true"; exports.toolBinDir = getToolBinDir(); @@ -91106,6 +91124,20 @@ function getCacheDirFromConfig() { } return undefined; } +async function getUvPythonDir() { + if (process.env.UV_PYTHON_INSTALL_DIR !== undefined) { + core.info(`Using UV_PYTHON_INSTALL_DIR from environment: ${process.env.UV_PYTHON_INSTALL_DIR}`); + return process.env.UV_PYTHON_INSTALL_DIR; + } + core.info("Determining uv python dir using `uv python dir`..."); + const result = await exec.getExecOutput("uv", ["python", "dir"]); + if (result.exitCode !== 0) { + throw new Error(`Failed to get uv python dir: ${result.stderr || result.stdout}`); + } + const dir = result.stdout.trim(); + core.info(`Determined uv python dir: ${dir}`); + return dir; +} function getCacheDependencyGlob() { const cacheDependencyGlobInput = core.getInput("cache-dependency-glob"); if (cacheDependencyGlobInput !== "") { diff --git a/dist/setup/index.js b/dist/setup/index.js index f443a3e..299113a 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -125151,8 +125151,12 @@ async function restoreCache() { } let matchedKey; core.info(`Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`); + const cachePaths = [inputs_1.cacheLocalPath]; + if (inputs_1.cachePython) { + cachePaths.push(await (0, inputs_1.getUvPythonDir)()); + } try { - matchedKey = await cache.restoreCache([inputs_1.cacheLocalPath], cacheKey); + matchedKey = await cache.restoreCache(cachePaths, cacheKey); } catch (err) { const message = err.message; @@ -125178,7 +125182,8 @@ async function computeKeys() { const pythonVersion = await getPythonVersion(); const platform = await (0, platforms_1.getPlatform)(); const pruned = inputs_1.pruneCache ? "-pruned" : ""; - return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; + const python = inputs_1.cachePython ? "-py" : ""; + return `setup-uv-${CACHE_VERSION}-${(0, platforms_1.getArch)()}-${platform}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`; } async function getPythonVersion() { if (inputs_1.pythonVersion !== "") { @@ -129708,9 +129713,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.addProblemMatchers = exports.manifestFile = exports.githubToken = exports.toolDir = exports.toolBinDir = exports.ignoreEmptyWorkdir = exports.ignoreNothingToCache = 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 = void 0; +exports.addProblemMatchers = exports.manifestFile = exports.githubToken = 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 = void 0; +exports.getUvPythonDir = getUvPythonDir; const node_path_1 = __importDefault(__nccwpck_require__(76760)); const core = __importStar(__nccwpck_require__(37484)); +const exec = __importStar(__nccwpck_require__(95236)); const config_file_1 = __nccwpck_require__(27846); exports.workingDirectory = core.getInput("working-directory"); exports.version = core.getInput("version"); @@ -129725,6 +129732,7 @@ exports.cacheSuffix = core.getInput("cache-suffix") || ""; exports.cacheLocalPath = getCacheLocalPath(); exports.cacheDependencyGlob = getCacheDependencyGlob(); exports.pruneCache = core.getInput("prune-cache") === "true"; +exports.cachePython = core.getInput("cache-python") === "true"; exports.ignoreNothingToCache = core.getInput("ignore-nothing-to-cache") === "true"; exports.ignoreEmptyWorkdir = core.getInput("ignore-empty-workdir") === "true"; exports.toolBinDir = getToolBinDir(); @@ -129818,6 +129826,20 @@ function getCacheDirFromConfig() { } return undefined; } +async function getUvPythonDir() { + if (process.env.UV_PYTHON_INSTALL_DIR !== undefined) { + core.info(`Using UV_PYTHON_INSTALL_DIR from environment: ${process.env.UV_PYTHON_INSTALL_DIR}`); + return process.env.UV_PYTHON_INSTALL_DIR; + } + core.info("Determining uv python dir using `uv python dir`..."); + const result = await exec.getExecOutput("uv", ["python", "dir"]); + if (result.exitCode !== 0) { + throw new Error(`Failed to get uv python dir: ${result.stderr || result.stdout}`); + } + const dir = result.stdout.trim(); + core.info(`Determined uv python dir: ${dir}`); + return dir; +} function getCacheDependencyGlob() { const cacheDependencyGlobInput = core.getInput("cache-dependency-glob"); if (cacheDependencyGlobInput !== "") { diff --git a/src/cache/restore-cache.ts b/src/cache/restore-cache.ts index fac9f55..3c32c04 100644 --- a/src/cache/restore-cache.ts +++ b/src/cache/restore-cache.ts @@ -5,7 +5,9 @@ import { hashFiles } from "../hash/hash-files"; import { cacheDependencyGlob, cacheLocalPath, + cachePython, cacheSuffix, + getUvPythonDir, pruneCache, pythonVersion as pythonVersionInput, restoreCache as shouldRestoreCache, @@ -30,8 +32,12 @@ export async function restoreCache(): Promise { core.info( `Trying to restore uv cache from GitHub Actions cache with key: ${cacheKey}`, ); + const cachePaths = [cacheLocalPath]; + if (cachePython) { + cachePaths.push(await getUvPythonDir()); + } try { - matchedKey = await cache.restoreCache([cacheLocalPath], cacheKey); + matchedKey = await cache.restoreCache(cachePaths, cacheKey); } catch (err) { const message = (err as Error).message; core.warning(message); @@ -62,7 +68,8 @@ async function computeKeys(): Promise { const pythonVersion = await getPythonVersion(); const platform = await getPlatform(); const pruned = pruneCache ? "-pruned" : ""; - return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${pythonVersion}${pruned}${cacheDependencyPathHash}${suffix}`; + const python = cachePython ? "-py" : ""; + return `setup-uv-${CACHE_VERSION}-${getArch()}-${platform}-${pythonVersion}${pruned}${python}${cacheDependencyPathHash}${suffix}`; } async function getPythonVersion(): Promise { diff --git a/src/save-cache.ts b/src/save-cache.ts index 45cc62a..2e9f684 100644 --- a/src/save-cache.ts +++ b/src/save-cache.ts @@ -10,7 +10,9 @@ import { import { STATE_UV_PATH, STATE_UV_VERSION } from "./utils/constants"; import { cacheLocalPath, + cachePython, enableCache, + getUvPythonDir, ignoreNothingToCache, pruneCache as shouldPruneCache, saveCache as shouldSaveCache, @@ -68,8 +70,22 @@ async function saveCache(): Promise { `Cache path ${actualCachePath} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`, ); } + + const cachePaths = [actualCachePath]; + if (cachePython) { + const pythonDir = await getUvPythonDir(); + core.info(`Including Python cache path: ${pythonDir}`); + if (!fs.existsSync(pythonDir) && !ignoreNothingToCache) { + throw new Error( + `Python cache path ${pythonDir} does not exist on disk. This likely indicates that there are no dependencies to cache. Consider disabling the cache input if it is not needed.`, + ); + } + cachePaths.push(pythonDir); + } + + core.info(`Final cache paths: ${cachePaths.join(", ")}`); try { - await cache.saveCache([actualCachePath], cacheKey); + await cache.saveCache(cachePaths, cacheKey); core.info(`cache saved with the key: ${cacheKey}`); } catch (e) { if ( diff --git a/src/utils/inputs.ts b/src/utils/inputs.ts index faf2f59..b14a50c 100644 --- a/src/utils/inputs.ts +++ b/src/utils/inputs.ts @@ -1,5 +1,6 @@ import path from "node:path"; import * as core from "@actions/core"; +import * as exec from "@actions/exec"; import { getConfigValueFromTomlFile } from "./config-file"; export const workingDirectory = core.getInput("working-directory"); @@ -15,6 +16,7 @@ export const cacheSuffix = core.getInput("cache-suffix") || ""; export const cacheLocalPath = getCacheLocalPath(); export const cacheDependencyGlob = getCacheDependencyGlob(); export const pruneCache = core.getInput("prune-cache") === "true"; +export const cachePython = core.getInput("cache-python") === "true"; export const ignoreNothingToCache = core.getInput("ignore-nothing-to-cache") === "true"; export const ignoreEmptyWorkdir = @@ -123,6 +125,25 @@ function getCacheDirFromConfig(): string | undefined { return undefined; } +export async function getUvPythonDir(): Promise { + if (process.env.UV_PYTHON_INSTALL_DIR !== undefined) { + core.info( + `Using UV_PYTHON_INSTALL_DIR from environment: ${process.env.UV_PYTHON_INSTALL_DIR}`, + ); + return process.env.UV_PYTHON_INSTALL_DIR; + } + core.info("Determining uv python dir using `uv python dir`..."); + const result = await exec.getExecOutput("uv", ["python", "dir"]); + if (result.exitCode !== 0) { + throw new Error( + `Failed to get uv python dir: ${result.stderr || result.stdout}`, + ); + } + const dir = result.stdout.trim(); + core.info(`Determined uv python dir: ${dir}`); + return dir; +} + function getCacheDependencyGlob(): string { const cacheDependencyGlobInput = core.getInput("cache-dependency-glob"); if (cacheDependencyGlobInput !== "") {