From 17c398959b4611a88929fabb5c563a8e43a0ff60 Mon Sep 17 00:00:00 2001 From: Kevin Stillhammer Date: Sun, 5 Jul 2026 10:46:44 +0200 Subject: [PATCH] Fix cache keys for Python version ranges (#937) ## Summary - URL-encode the Python version component before adding it to the cache key - URL-encode the user-provided cache suffix for the same reason - Add cache key tests for Python ranges, comma-containing suffixes, and unchanged simple inputs Fixes #914 Refs: pi-session 019f3164-85e7-7817-bffd-501d89b3a1fd ## Tests - npm run all --- __tests__/cache/restore-cache.test.ts | 74 +++++++++++++++++++++++++++ __tests__/helpers/setup-inputs.ts | 36 +++++++++++++ dist/setup/index.cjs | 4 +- src/cache/restore-cache.ts | 6 ++- 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 __tests__/cache/restore-cache.test.ts create mode 100644 __tests__/helpers/setup-inputs.ts diff --git a/__tests__/cache/restore-cache.test.ts b/__tests__/cache/restore-cache.test.ts new file mode 100644 index 0000000..ec342d3 --- /dev/null +++ b/__tests__/cache/restore-cache.test.ts @@ -0,0 +1,74 @@ +import { beforeEach, describe, expect, it, jest } from "@jest/globals"; +import { createSetupInputs } from "../helpers/setup-inputs"; + +const mockRestoreCache = jest.fn(); +const mockSaveState = jest.fn(); +const mockSetOutput = jest.fn(); + +jest.unstable_mockModule("@actions/cache", () => ({ + restoreCache: mockRestoreCache, +})); + +jest.unstable_mockModule("@actions/core", () => ({ + saveState: mockSaveState, + setOutput: mockSetOutput, +})); + +jest.unstable_mockModule("../../src/hash/hash-files", () => ({ + hashFiles: jest.fn(async () => "dependencyhash"), +})); + +jest.unstable_mockModule("../../src/utils/logging", () => ({ + info: jest.fn(), + warning: jest.fn(), +})); + +jest.unstable_mockModule("../../src/utils/platforms", () => ({ + getArch: jest.fn(() => "x86_64"), + getOSNameVersion: jest.fn(() => "ubuntu-24.04"), + getPlatform: jest.fn(async () => "unknown-linux-gnu"), +})); + +const { restoreCache } = await import("../../src/cache/restore-cache"); + +function cacheKeyOutput(): string { + const call = mockSetOutput.mock.calls.find(([name]) => name === "cache-key"); + expect(call).toBeDefined(); + return call?.[1] as string; +} + +beforeEach(() => { + jest.clearAllMocks(); +}); + +describe("restoreCache", () => { + it("encodes Python version ranges before adding them to the cache key", async () => { + await restoreCache(createSetupInputs(), ">3.10.11,<3.11"); + + const cacheKey = cacheKeyOutput(); + + expect(cacheKey).not.toContain(","); + expect(cacheKey).toContain("-%3E3.10.11%2C%3C3.11-"); + }); + + it("encodes cache suffixes before adding them to the cache key", async () => { + const inputs = createSetupInputs({ cacheSuffix: "tests-3.10,3.11" }); + + await restoreCache(inputs, "3.11"); + + const cacheKey = cacheKeyOutput(); + + expect(cacheKey).not.toContain(","); + expect(cacheKey).toContain("-tests-3.10%2C3.11"); + }); + + it("keeps cache keys unchanged for exact Python versions and simple suffixes", async () => { + const inputs = createSetupInputs({ cacheSuffix: "tests-3.11" }); + + await restoreCache(inputs, "3.11"); + + expect(cacheKeyOutput()).toBe( + "setup-uv-2-x86_64-unknown-linux-gnu-ubuntu-24.04-3.11-pruned-dependencyhash-tests-3.11", + ); + }); +}); diff --git a/__tests__/helpers/setup-inputs.ts b/__tests__/helpers/setup-inputs.ts new file mode 100644 index 0000000..1ca0d0d --- /dev/null +++ b/__tests__/helpers/setup-inputs.ts @@ -0,0 +1,36 @@ +import { CacheLocalSource, type SetupInputs } from "../../src/utils/inputs"; + +export function createSetupInputs( + overrides: Partial = {}, +): SetupInputs { + return { + activateEnvironment: false, + addProblemMatchers: false, + cacheDependencyGlob: "uv.lock", + cacheLocalPath: { + path: "/tmp/setup-uv-cache", + source: CacheLocalSource.Input, + }, + cachePython: false, + cacheSuffix: "", + checksum: "", + downloadFromAstralMirror: false, + enableCache: true, + githubToken: "", + ignoreEmptyWorkdir: false, + ignoreNothingToCache: false, + noProject: false, + pruneCache: true, + pythonDir: "/tmp/uv-python-dir", + pythonVersion: "", + quiet: false, + resolutionStrategy: "highest", + restoreCache: false, + saveCache: true, + venvPath: "/workspace/.venv", + version: "", + versionFile: "", + workingDirectory: "/workspace", + ...overrides, + }; +} diff --git a/dist/setup/index.cjs b/dist/setup/index.cjs index 13f65ae..2231f67 100644 --- a/dist/setup/index.cjs +++ b/dist/setup/index.cjs @@ -90621,8 +90621,8 @@ async function computeKeys(inputs, pythonVersion) { if (cacheDependencyPathHash === "-") { cacheDependencyPathHash = "-no-dependency-glob"; } - const suffix = inputs.cacheSuffix ? `-${inputs.cacheSuffix}` : ""; - const version3 = pythonVersion ?? "unknown"; + const suffix = inputs.cacheSuffix ? `-${encodeURIComponent(inputs.cacheSuffix)}` : ""; + const version3 = encodeURIComponent(pythonVersion ?? "unknown"); const platform2 = await getPlatform(); const osNameVersion = getOSNameVersion(); const pruned = inputs.pruneCache ? "-pruned" : ""; diff --git a/src/cache/restore-cache.ts b/src/cache/restore-cache.ts index 94a7f3d..b5fec8f 100644 --- a/src/cache/restore-cache.ts +++ b/src/cache/restore-cache.ts @@ -94,8 +94,10 @@ async function computeKeys( if (cacheDependencyPathHash === "-") { cacheDependencyPathHash = "-no-dependency-glob"; } - const suffix = inputs.cacheSuffix ? `-${inputs.cacheSuffix}` : ""; - const version = pythonVersion ?? "unknown"; + const suffix = inputs.cacheSuffix + ? `-${encodeURIComponent(inputs.cacheSuffix)}` + : ""; + const version = encodeURIComponent(pythonVersion ?? "unknown"); const platform = await getPlatform(); const osNameVersion = getOSNameVersion(); const pruned = inputs.pruneCache ? "-pruned" : "";