From 5db1cf9a59fb97c40a68accab29236f0da7e94db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kry=C5=A1tof=20Korb?= Date: Wed, 21 May 2025 23:19:28 +0200 Subject: [PATCH 1/4] Enhance reading from .python-version (#787) * Enhance reading from .python-version * Fix typos * Fix lint * Add built files * Don't use EOL versions in `utils.test.ts` * Fix Prettier * Don't use unreleased versions in `utils.test.ts` * Update versions in `utils.test.ts` again --- __tests__/utils.test.ts | 66 +++++++++++++++++++++++++++++------------ dist/setup/index.js | 37 ++++++++++++++++------- src/setup-python.ts | 4 +-- src/utils.ts | 31 ++++++++++++++----- 4 files changed, 98 insertions(+), 40 deletions(-) diff --git a/__tests__/utils.test.ts b/__tests__/utils.test.ts index 6c0f0e13..009749c6 100644 --- a/__tests__/utils.test.ts +++ b/__tests__/utils.test.ts @@ -10,7 +10,7 @@ import { validatePythonVersionFormatForPyPy, isCacheFeatureAvailable, getVersionInputFromFile, - getVersionInputFromPlainFile, + getVersionsInputFromPlainFile, getVersionInputFromTomlFile, getNextPageUrl, isGhes, @@ -24,10 +24,10 @@ jest.mock('@actions/core'); describe('validatePythonVersionFormatForPyPy', () => { it.each([ - ['3.6', true], - ['3.7', true], - ['3.6.x', false], - ['3.7.x', false], + ['3.12', true], + ['3.13', true], + ['3.12.x', false], + ['3.13.x', false], ['3.x', false], ['3', false] ])('%s -> %s', (input, expected) => { @@ -95,24 +95,52 @@ const tempDir = path.join( ); describe('Version from file test', () => { - it.each([getVersionInputFromPlainFile, getVersionInputFromFile])( + it.each([getVersionsInputFromPlainFile, getVersionInputFromFile])( 'Version from plain file test', async _fn => { await io.mkdirP(tempDir); const pythonVersionFileName = 'python-version.file'; const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); - const pythonVersionFileContent = '3.7'; + const pythonVersionFileContent = '3.13'; fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersionFileContent]); } ); + it.each([getVersionsInputFromPlainFile, getVersionInputFromFile])( + 'Versions from multiline plain file test', + async _fn => { + await io.mkdirP(tempDir); + const pythonVersionFileName = 'python-version.file'; + const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); + const pythonVersionFileContent = '3.13\r\n3.12'; + fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); + expect(_fn(pythonVersionFilePath)).toEqual(['3.13', '3.12']); + } + ); + it.each([getVersionsInputFromPlainFile, getVersionInputFromFile])( + 'Version from complex plain file test', + async _fn => { + await io.mkdirP(tempDir); + const pythonVersionFileName = 'python-version.file'; + const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); + const pythonVersionFileContent = + '3.13/envs/virtualenv\r# 3.12\n3.11\r\n3.10\r\n 3.9 \r\n'; + fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); + expect(_fn(pythonVersionFilePath)).toEqual([ + '3.13', + '3.11', + '3.10', + '3.9' + ]); + } + ); it.each([getVersionInputFromTomlFile, getVersionInputFromFile])( 'Version from standard pyproject.toml test', async _fn => { await io.mkdirP(tempDir); const pythonVersionFileName = 'pyproject.toml'; const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); - const pythonVersion = '>=3.7.0'; + const pythonVersion = '>=3.13.0'; const pythonVersionFileContent = `[project]\nrequires-python = "${pythonVersion}"`; fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]); @@ -124,7 +152,7 @@ describe('Version from file test', () => { await io.mkdirP(tempDir); const pythonVersionFileName = 'pyproject.toml'; const pythonVersionFilePath = path.join(tempDir, pythonVersionFileName); - const pythonVersion = '>=3.7.0'; + const pythonVersion = '>=3.13.0'; const pythonVersionFileContent = `[tool.poetry.dependencies]\npython = "${pythonVersion}"`; fs.writeFileSync(pythonVersionFilePath, pythonVersionFileContent); expect(_fn(pythonVersionFilePath)).toEqual([pythonVersion]); @@ -145,9 +173,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = 'python 3.9.10\nnodejs 16'; + const toolVersionContent = 'python 3.13.2\nnodejs 16'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']); + expect(_fn(toolVersionFilePath)).toEqual(['3.13.2']); } ); @@ -156,9 +184,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = '# python 3.8\npython 3.9'; + const toolVersionContent = '# python 3.13\npython 3.12'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.9']); + expect(_fn(toolVersionFilePath)).toEqual(['3.12']); } ); @@ -167,9 +195,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = ' python 3.10 '; + const toolVersionContent = ' python 3.13 '; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.10']); + expect(_fn(toolVersionFilePath)).toEqual(['3.13']); } ); @@ -178,9 +206,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = 'python v3.9.10'; + const toolVersionContent = 'python v3.13.2'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['3.9.10']); + expect(_fn(toolVersionFilePath)).toEqual(['3.13.2']); } ); @@ -189,9 +217,9 @@ describe('Version from file test', () => { async _fn => { const toolVersionFileName = '.tool-versions'; const toolVersionFilePath = path.join(tempDir, toolVersionFileName); - const toolVersionContent = 'python pypy3.10-7.3.14'; + const toolVersionContent = 'python pypy3.10-7.3.19'; fs.writeFileSync(toolVersionFilePath, toolVersionContent); - expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.14']); + expect(_fn(toolVersionFilePath)).toEqual(['pypy3.10-7.3.19']); } ); diff --git a/dist/setup/index.js b/dist/setup/index.js index 87c6a816..4e3f2673 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -96929,7 +96929,7 @@ function cacheDependencies(cache, pythonVersion) { } function resolveVersionInputFromDefaultFile() { const couples = [ - ['.python-version', utils_1.getVersionInputFromPlainFile] + ['.python-version', utils_1.getVersionsInputFromPlainFile] ]; for (const [versionFile, _fn] of couples) { (0, utils_1.logWarning)(`Neither 'python-version' nor 'python-version-file' inputs were supplied. Attempting to find '${versionFile}' file.`); @@ -97066,7 +97066,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); -exports.getDownloadFileName = exports.getNextPageUrl = exports.getBinaryDirectory = exports.getVersionInputFromFile = exports.getVersionInputFromToolVersions = exports.getVersionInputFromPlainFile = exports.getVersionInputFromTomlFile = exports.getOSInfo = exports.getLinuxInfo = exports.logWarning = exports.isCacheFeatureAvailable = exports.isGhes = exports.validatePythonVersionFormatForPyPy = exports.writeExactPyPyVersionFile = exports.readExactPyPyVersionFile = exports.getPyPyVersionFromPath = exports.isNightlyKeyword = exports.validateVersion = exports.createSymlinkInFolder = exports.WINDOWS_PLATFORMS = exports.WINDOWS_ARCHS = exports.IS_MAC = exports.IS_LINUX = exports.IS_WINDOWS = void 0; +exports.getDownloadFileName = exports.getNextPageUrl = exports.getBinaryDirectory = exports.getVersionInputFromFile = exports.getVersionInputFromToolVersions = exports.getVersionsInputFromPlainFile = exports.getVersionInputFromTomlFile = exports.getOSInfo = exports.getLinuxInfo = exports.logWarning = exports.isCacheFeatureAvailable = exports.isGhes = exports.validatePythonVersionFormatForPyPy = exports.writeExactPyPyVersionFile = exports.readExactPyPyVersionFile = exports.getPyPyVersionFromPath = exports.isNightlyKeyword = exports.validateVersion = exports.createSymlinkInFolder = exports.WINDOWS_PLATFORMS = exports.WINDOWS_ARCHS = exports.IS_MAC = exports.IS_LINUX = exports.IS_WINDOWS = void 0; /* eslint no-unsafe-finally: "off" */ const cache = __importStar(__nccwpck_require__(5116)); const core = __importStar(__nccwpck_require__(7484)); @@ -97247,7 +97247,7 @@ function extractValue(obj, keys) { * If none is present, returns an empty list. */ function getVersionInputFromTomlFile(versionFile) { - core.debug(`Trying to resolve version form ${versionFile}`); + core.debug(`Trying to resolve version from ${versionFile}`); let pyprojectFile = fs_1.default.readFileSync(versionFile, 'utf8'); // Normalize the line endings in the pyprojectFile pyprojectFile = pyprojectFile.replace(/\r\n/g, '\n'); @@ -97280,15 +97280,30 @@ function getVersionInputFromTomlFile(versionFile) { } exports.getVersionInputFromTomlFile = getVersionInputFromTomlFile; /** - * Python version extracted from a plain text file. + * Python versions extracted from a plain text file. + * - Resolves multiple versions from multiple lines. + * - Handles pyenv-virtualenv pointers (e.g. `3.10/envs/virtualenv`). + * - Ignores empty lines and lines starting with `#` + * - Trims whitespace. */ -function getVersionInputFromPlainFile(versionFile) { - core.debug(`Trying to resolve version form ${versionFile}`); - const version = fs_1.default.readFileSync(versionFile, 'utf8').trim(); - core.info(`Resolved ${versionFile} as ${version}`); - return [version]; +function getVersionsInputFromPlainFile(versionFile) { + core.debug(`Trying to resolve versions from ${versionFile}`); + const content = fs_1.default.readFileSync(versionFile, 'utf8').trim(); + const lines = content.split(/\r\n|\r|\n/); + const versions = lines + .map(line => { + if (line.startsWith('#') || line.trim() === '') { + return undefined; + } + let version = line.trim(); + version = version.split('/')[0]; + return version; + }) + .filter(version => version !== undefined); + core.info(`Resolved ${versionFile} as ${versions.join(', ')}`); + return versions; } -exports.getVersionInputFromPlainFile = getVersionInputFromPlainFile; +exports.getVersionsInputFromPlainFile = getVersionsInputFromPlainFile; /** * Python version extracted from a .tool-versions file. */ @@ -97331,7 +97346,7 @@ function getVersionInputFromFile(versionFile) { return getVersionInputFromToolVersions(versionFile); } else { - return getVersionInputFromPlainFile(versionFile); + return getVersionsInputFromPlainFile(versionFile); } } exports.getVersionInputFromFile = getVersionInputFromFile; diff --git a/src/setup-python.ts b/src/setup-python.ts index ab5931b8..5d585d73 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -11,7 +11,7 @@ import { logWarning, IS_MAC, getVersionInputFromFile, - getVersionInputFromPlainFile + getVersionsInputFromPlainFile } from './utils'; function isPyPyVersion(versionSpec: string) { @@ -35,7 +35,7 @@ async function cacheDependencies(cache: string, pythonVersion: string) { function resolveVersionInputFromDefaultFile(): string[] { const couples: [string, (versionFile: string) => string[]][] = [ - ['.python-version', getVersionInputFromPlainFile] + ['.python-version', getVersionsInputFromPlainFile] ]; for (const [versionFile, _fn] of couples) { logWarning( diff --git a/src/utils.ts b/src/utils.ts index 6274895e..f39006d9 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -228,7 +228,7 @@ function extractValue(obj: any, keys: string[]): string | undefined { * If none is present, returns an empty list. */ export function getVersionInputFromTomlFile(versionFile: string): string[] { - core.debug(`Trying to resolve version form ${versionFile}`); + core.debug(`Trying to resolve version from ${versionFile}`); let pyprojectFile = fs.readFileSync(versionFile, 'utf8'); // Normalize the line endings in the pyprojectFile @@ -269,13 +269,28 @@ export function getVersionInputFromTomlFile(versionFile: string): string[] { } /** - * Python version extracted from a plain text file. + * Python versions extracted from a plain text file. + * - Resolves multiple versions from multiple lines. + * - Handles pyenv-virtualenv pointers (e.g. `3.10/envs/virtualenv`). + * - Ignores empty lines and lines starting with `#` + * - Trims whitespace. */ -export function getVersionInputFromPlainFile(versionFile: string): string[] { - core.debug(`Trying to resolve version form ${versionFile}`); - const version = fs.readFileSync(versionFile, 'utf8').trim(); - core.info(`Resolved ${versionFile} as ${version}`); - return [version]; +export function getVersionsInputFromPlainFile(versionFile: string): string[] { + core.debug(`Trying to resolve versions from ${versionFile}`); + const content = fs.readFileSync(versionFile, 'utf8').trim(); + const lines = content.split(/\r\n|\r|\n/); + const versions = lines + .map(line => { + if (line.startsWith('#') || line.trim() === '') { + return undefined; + } + let version: string = line.trim(); + version = version.split('/')[0]; + return version; + }) + .filter(version => version !== undefined) as string[]; + core.info(`Resolved ${versionFile} as ${versions.join(', ')}`); + return versions; } /** @@ -319,7 +334,7 @@ export function getVersionInputFromFile(versionFile: string): string[] { } else if (versionFile.match('.tool-versions')) { return getVersionInputFromToolVersions(versionFile); } else { - return getVersionInputFromPlainFile(versionFile); + return getVersionsInputFromPlainFile(versionFile); } } From 5fa0ee6f38acba344a11175befcf11c0c1d46f5a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Jun 2025 17:01:16 -0500 Subject: [PATCH 2/4] Bump @actions/tool-cache from 2.0.1 to 2.0.2 (#1095) * Bump @actions/tool-cache from 2.0.1 to 2.0.2 Bumps [@actions/tool-cache](https://github.com/actions/toolkit/tree/HEAD/packages/tool-cache) from 2.0.1 to 2.0.2. - [Changelog](https://github.com/actions/toolkit/blob/main/packages/tool-cache/RELEASES.md) - [Commits](https://github.com/actions/toolkit/commits/HEAD/packages/tool-cache) --- updated-dependencies: - dependency-name: "@actions/tool-cache" dependency-version: 2.0.2 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Fix failures --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: HarithaVattikuti <73516759+HarithaVattikuti@users.noreply.github.com> --- .licenses/npm/@actions/tool-cache.dep.yml | 2 +- .licenses/npm/uuid-3.4.0.dep.yml | 39 ----- .../npm/{uuid-8.3.2.dep.yml => uuid.dep.yml} | 2 +- dist/setup/index.js | 156 +++++------------- package-lock.json | 23 +-- package.json | 2 +- 6 files changed, 49 insertions(+), 175 deletions(-) delete mode 100644 .licenses/npm/uuid-3.4.0.dep.yml rename .licenses/npm/{uuid-8.3.2.dep.yml => uuid.dep.yml} (96%) diff --git a/.licenses/npm/@actions/tool-cache.dep.yml b/.licenses/npm/@actions/tool-cache.dep.yml index fbf911fe..8a9ca1fd 100644 --- a/.licenses/npm/@actions/tool-cache.dep.yml +++ b/.licenses/npm/@actions/tool-cache.dep.yml @@ -1,6 +1,6 @@ --- name: "@actions/tool-cache" -version: 2.0.1 +version: 2.0.2 type: npm summary: Actions tool-cache lib homepage: https://github.com/actions/toolkit/tree/main/packages/tool-cache diff --git a/.licenses/npm/uuid-3.4.0.dep.yml b/.licenses/npm/uuid-3.4.0.dep.yml deleted file mode 100644 index 45970fef..00000000 --- a/.licenses/npm/uuid-3.4.0.dep.yml +++ /dev/null @@ -1,39 +0,0 @@ ---- -name: uuid -version: 3.4.0 -type: npm -summary: RFC4122 (v1, v4, and v5) UUIDs -homepage: https://github.com/uuidjs/uuid#readme -license: mit -licenses: -- sources: LICENSE.md - text: | - The MIT License (MIT) - - Copyright (c) 2010-2016 Robert Kieffer and other contributors - - Permission is hereby granted, free of charge, to any person obtaining a copy - of this software and associated documentation files (the "Software"), to deal - in the Software without restriction, including without limitation the rights - to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - copies of the Software, and to permit persons to whom the Software is - furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all - copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - SOFTWARE. -notices: -- sources: AUTHORS - text: |- - Robert Kieffer - Christoph Tavan - AJ ONeal - Vincent Voyer - Roman Shtylman diff --git a/.licenses/npm/uuid-8.3.2.dep.yml b/.licenses/npm/uuid.dep.yml similarity index 96% rename from .licenses/npm/uuid-8.3.2.dep.yml rename to .licenses/npm/uuid.dep.yml index bf84da08..1aa22dea 100644 --- a/.licenses/npm/uuid-8.3.2.dep.yml +++ b/.licenses/npm/uuid.dep.yml @@ -3,7 +3,7 @@ name: uuid version: 8.3.2 type: npm summary: RFC4122 (v1, v4, and v5) UUIDs -homepage: https://github.com/uuidjs/uuid#readme +homepage: license: mit licenses: - sources: LICENSE.md diff --git a/dist/setup/index.js b/dist/setup/index.js index 4e3f2673..4a3890aa 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -10251,7 +10251,11 @@ function copyFile(srcFile, destFile, force) { var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -10264,7 +10268,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -10294,11 +10298,11 @@ function _findMatch(versionSpec, stable, candidates, archFilter) { let file; for (const candidate of candidates) { const version = candidate.version; - core_1.debug(`check ${version} satisfies ${versionSpec}`); + (0, core_1.debug)(`check ${version} satisfies ${versionSpec}`); if (semver.satisfies(version, versionSpec) && (!stable || candidate.stable === stable)) { file = candidate.files.find(item => { - core_1.debug(`${item.arch}===${archFilter} && ${item.platform}===${platFilter}`); + (0, core_1.debug)(`${item.arch}===${archFilter} && ${item.platform}===${platFilter}`); let chk = item.arch === archFilter && item.platform === platFilter; if (chk && item.platform_version) { const osVersion = module.exports._getOsVersion(); @@ -10312,7 +10316,7 @@ function _findMatch(versionSpec, stable, candidates, archFilter) { return chk; }); if (file) { - core_1.debug(`matched ${candidate.version}`); + (0, core_1.debug)(`matched ${candidate.version}`); match = candidate; break; } @@ -10350,10 +10354,7 @@ function _getOsVersion() { if (parts.length === 2 && (parts[0].trim() === 'VERSION_ID' || parts[0].trim() === 'DISTRIB_RELEASE')) { - version = parts[1] - .trim() - .replace(/^"/, '') - .replace(/"$/, ''); + version = parts[1].trim().replace(/^"/, '').replace(/"$/, ''); break; } } @@ -10386,7 +10387,11 @@ exports._readLinuxVersionFile = _readLinuxVersionFile; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -10399,7 +10404,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -10476,7 +10481,11 @@ exports.RetryHelper = RetryHelper; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; - Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; @@ -10489,7 +10498,7 @@ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? ( var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; - if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); + if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; @@ -10502,13 +10511,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.evaluateVersions = exports.isExplicitVersion = exports.findFromManifest = exports.getManifestFromRepo = exports.findAllVersions = exports.find = exports.cacheFile = exports.cacheDir = exports.extractZip = exports.extractXar = exports.extractTar = exports.extract7z = exports.downloadTool = exports.HTTPError = void 0; const core = __importStar(__nccwpck_require__(7484)); const io = __importStar(__nccwpck_require__(4994)); +const crypto = __importStar(__nccwpck_require__(6982)); const fs = __importStar(__nccwpck_require__(9896)); const mm = __importStar(__nccwpck_require__(8036)); const os = __importStar(__nccwpck_require__(857)); @@ -10518,7 +10525,6 @@ const semver = __importStar(__nccwpck_require__(6193)); const stream = __importStar(__nccwpck_require__(2203)); const util = __importStar(__nccwpck_require__(9023)); const assert_1 = __nccwpck_require__(2613); -const v4_1 = __importDefault(__nccwpck_require__(9021)); const exec_1 = __nccwpck_require__(5236); const retry_helper_1 = __nccwpck_require__(7380); class HTTPError extends Error { @@ -10543,7 +10549,7 @@ const userAgent = 'actions/tool-cache'; */ function downloadTool(url, dest, auth, headers) { return __awaiter(this, void 0, void 0, function* () { - dest = dest || path.join(_getTempDirectory(), v4_1.default()); + dest = dest || path.join(_getTempDirectory(), crypto.randomUUID()); yield io.mkdirP(path.dirname(dest)); core.debug(`Downloading ${url}`); core.debug(`Destination ${dest}`); @@ -10632,8 +10638,8 @@ function downloadToolAttempt(url, dest, auth, headers) { */ function extract7z(file, dest, _7zPath) { return __awaiter(this, void 0, void 0, function* () { - assert_1.ok(IS_WINDOWS, 'extract7z() not supported on current OS'); - assert_1.ok(file, 'parameter "file" is required'); + (0, assert_1.ok)(IS_WINDOWS, 'extract7z() not supported on current OS'); + (0, assert_1.ok)(file, 'parameter "file" is required'); dest = yield _createExtractFolder(dest); const originalCwd = process.cwd(); process.chdir(dest); @@ -10650,7 +10656,7 @@ function extract7z(file, dest, _7zPath) { const options = { silent: true }; - yield exec_1.exec(`"${_7zPath}"`, args, options); + yield (0, exec_1.exec)(`"${_7zPath}"`, args, options); } finally { process.chdir(originalCwd); @@ -10679,7 +10685,7 @@ function extract7z(file, dest, _7zPath) { }; try { const powershellPath = yield io.which('powershell', true); - yield exec_1.exec(`"${powershellPath}"`, args, options); + yield (0, exec_1.exec)(`"${powershellPath}"`, args, options); } finally { process.chdir(originalCwd); @@ -10707,7 +10713,7 @@ function extractTar(file, dest, flags = 'xz') { // Determine whether GNU tar core.debug('Checking tar --version'); let versionOutput = ''; - yield exec_1.exec('tar --version', [], { + yield (0, exec_1.exec)('tar --version', [], { ignoreReturnCode: true, silent: true, listeners: { @@ -10743,7 +10749,7 @@ function extractTar(file, dest, flags = 'xz') { args.push('--overwrite'); } args.push('-C', destArg, '-f', fileArg); - yield exec_1.exec(`tar`, args); + yield (0, exec_1.exec)(`tar`, args); return dest; }); } @@ -10758,8 +10764,8 @@ exports.extractTar = extractTar; */ function extractXar(file, dest, flags = []) { return __awaiter(this, void 0, void 0, function* () { - assert_1.ok(IS_MAC, 'extractXar() not supported on current OS'); - assert_1.ok(file, 'parameter "file" is required'); + (0, assert_1.ok)(IS_MAC, 'extractXar() not supported on current OS'); + (0, assert_1.ok)(file, 'parameter "file" is required'); dest = yield _createExtractFolder(dest); let args; if (flags instanceof Array) { @@ -10773,7 +10779,7 @@ function extractXar(file, dest, flags = []) { args.push('-v'); } const xarPath = yield io.which('xar', true); - yield exec_1.exec(`"${xarPath}"`, _unique(args)); + yield (0, exec_1.exec)(`"${xarPath}"`, _unique(args)); return dest; }); } @@ -10827,7 +10833,7 @@ function extractZipWin(file, dest) { pwshCommand ]; core.debug(`Using pwsh at path: ${pwshPath}`); - yield exec_1.exec(`"${pwshPath}"`, args); + yield (0, exec_1.exec)(`"${pwshPath}"`, args); } else { const powershellCommand = [ @@ -10848,7 +10854,7 @@ function extractZipWin(file, dest) { ]; const powershellPath = yield io.which('powershell', true); core.debug(`Using powershell at path: ${powershellPath}`); - yield exec_1.exec(`"${powershellPath}"`, args); + yield (0, exec_1.exec)(`"${powershellPath}"`, args); } }); } @@ -10860,7 +10866,7 @@ function extractZipNix(file, dest) { args.unshift('-q'); } args.unshift('-o'); //overwrite with -o, otherwise a prompt is shown which freezes the run - yield exec_1.exec(`"${unzipPath}"`, args, { cwd: dest }); + yield (0, exec_1.exec)(`"${unzipPath}"`, args, { cwd: dest }); }); } /** @@ -11037,7 +11043,7 @@ function _createExtractFolder(dest) { return __awaiter(this, void 0, void 0, function* () { if (!dest) { // create a temp dir - dest = path.join(_getTempDirectory(), v4_1.default()); + dest = path.join(_getTempDirectory(), crypto.randomUUID()); } yield io.mkdirP(dest); return dest; @@ -11110,7 +11116,7 @@ exports.evaluateVersions = evaluateVersions; */ function _getCacheDirectory() { const cacheDirectory = process.env['RUNNER_TOOL_CACHE'] || ''; - assert_1.ok(cacheDirectory, 'Expected RUNNER_TOOL_CACHE to be defined'); + (0, assert_1.ok)(cacheDirectory, 'Expected RUNNER_TOOL_CACHE to be defined'); return cacheDirectory; } /** @@ -11118,7 +11124,7 @@ function _getCacheDirectory() { */ function _getTempDirectory() { const tempDirectory = process.env['RUNNER_TEMP'] || ''; - assert_1.ok(tempDirectory, 'Expected RUNNER_TEMP to be defined'); + (0, assert_1.ok)(tempDirectory, 'Expected RUNNER_TEMP to be defined'); return tempDirectory; } /** @@ -88177,90 +88183,6 @@ module.exports = { } -/***/ }), - -/***/ 8682: -/***/ ((module) => { - -/** - * Convert array of 16 byte values to UUID string format of the form: - * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX - */ -var byteToHex = []; -for (var i = 0; i < 256; ++i) { - byteToHex[i] = (i + 0x100).toString(16).substr(1); -} - -function bytesToUuid(buf, offset) { - var i = offset || 0; - var bth = byteToHex; - // join used to fix memory issue caused by concatenation: https://bugs.chromium.org/p/v8/issues/detail?id=3175#c4 - return ([ - bth[buf[i++]], bth[buf[i++]], - bth[buf[i++]], bth[buf[i++]], '-', - bth[buf[i++]], bth[buf[i++]], '-', - bth[buf[i++]], bth[buf[i++]], '-', - bth[buf[i++]], bth[buf[i++]], '-', - bth[buf[i++]], bth[buf[i++]], - bth[buf[i++]], bth[buf[i++]], - bth[buf[i++]], bth[buf[i++]] - ]).join(''); -} - -module.exports = bytesToUuid; - - -/***/ }), - -/***/ 1694: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -// Unique ID creation requires a high quality random # generator. In node.js -// this is pretty straight-forward - we use the crypto API. - -var crypto = __nccwpck_require__(6982); - -module.exports = function nodeRNG() { - return crypto.randomBytes(16); -}; - - -/***/ }), - -/***/ 9021: -/***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { - -var rng = __nccwpck_require__(1694); -var bytesToUuid = __nccwpck_require__(8682); - -function v4(options, buf, offset) { - var i = buf && offset || 0; - - if (typeof(options) == 'string') { - buf = options === 'binary' ? new Array(16) : null; - options = null; - } - options = options || {}; - - var rnds = options.random || (options.rng || rng)(); - - // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` - rnds[6] = (rnds[6] & 0x0f) | 0x40; - rnds[8] = (rnds[8] & 0x3f) | 0x80; - - // Copy bytes to buffer, if provided - if (buf) { - for (var ii = 0; ii < 16; ++ii) { - buf[i + ii] = rnds[ii]; - } - } - - return buf || bytesToUuid(rnds); -} - -module.exports = v4; - - /***/ }), /***/ 7125: diff --git a/package-lock.json b/package-lock.json index 6b05a120..bfbb0d3c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "@actions/glob": "^0.5.0", "@actions/http-client": "^2.2.3", "@actions/io": "^1.0.2", - "@actions/tool-cache": "^2.0.1", + "@actions/tool-cache": "^2.0.2", "@iarna/toml": "^3.0.0", "semver": "^7.7.1" }, @@ -123,16 +123,16 @@ "integrity": "sha512-wi9JjgKLYS7U/z8PPbco+PvTb/nRWjeoFlJ1Qer83k/3C5PHQi28hiVdeE2kHXmIL99mQFawx8qt/JPjZilJ8Q==" }, "node_modules/@actions/tool-cache": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.1.tgz", - "integrity": "sha512-iPU+mNwrbA8jodY8eyo/0S/QqCKDajiR8OxWTnSk/SnYg0sj8Hp4QcUEVC1YFpHWXtrfbQrE13Jz4k4HXJQKcA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@actions/tool-cache/-/tool-cache-2.0.2.tgz", + "integrity": "sha512-fBhNNOWxuoLxztQebpOaWu6WeVmuwa77Z+DxIZ1B+OYvGkGQon6kTVg6Z32Cb13WCuw0szqonK+hh03mJV7Z6w==", + "license": "MIT", "dependencies": { - "@actions/core": "^1.2.6", + "@actions/core": "^1.11.1", "@actions/exec": "^1.0.0", "@actions/http-client": "^2.0.1", "@actions/io": "^1.1.1", - "semver": "^6.1.0", - "uuid": "^3.3.2" + "semver": "^6.1.0" } }, "node_modules/@actions/tool-cache/node_modules/semver": { @@ -5323,15 +5323,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "bin": { - "uuid": "bin/uuid" - } - }, "node_modules/v8-to-istanbul": { "version": "9.1.3", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.3.tgz", diff --git a/package.json b/package.json index a21ccb9f..1017bddd 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "@actions/glob": "^0.5.0", "@actions/http-client": "^2.2.3", "@actions/io": "^1.0.2", - "@actions/tool-cache": "^2.0.1", + "@actions/tool-cache": "^2.0.2", "@iarna/toml": "^3.0.0", "semver": "^7.7.1" }, From e9c40fbc2bf309799101412f178fe3064ebb27ac Mon Sep 17 00:00:00 2001 From: Priya Gupta <147705955+priyagupta108@users.noreply.github.com> Date: Fri, 20 Jun 2025 08:39:35 +0530 Subject: [PATCH 3/4] Add support for `pip-version` (#1129) * Add pip-version input * Update workflow files * Add documentation * Update workflow files --- .github/workflows/e2e-cache-freethreaded.yml | 57 ++++++++++++++++++++ .github/workflows/e2e-cache.yml | 57 ++++++++++++++++++++ README.md | 1 + action.yml | 2 + dist/setup/index.js | 17 ++++++ docs/advanced-usage.md | 20 +++++++ src/find-python.ts | 25 +++++++++ 7 files changed, 179 insertions(+) diff --git a/.github/workflows/e2e-cache-freethreaded.yml b/.github/workflows/e2e-cache-freethreaded.yml index e3b298cc..d3beb5ba 100644 --- a/.github/workflows/e2e-cache-freethreaded.yml +++ b/.github/workflows/e2e-cache-freethreaded.yml @@ -162,3 +162,60 @@ jobs: run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python - name: Install dependencies run: pipenv install requests + + python-pip-dependencies-caching-with-pip-version: + name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + ubuntu-latest, + ubuntu-22.04, + ubuntu-24.04-arm, + ubuntu-22.04-arm, + windows-latest, + macos-latest, + macos-13 + ] + python-version: [3.13.0t, 3.13.1t, 3.13.2t] + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: ./ + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + pip-version: '25.0.1' + - name: Install dependencies + run: pip install numpy pandas requests + + python-pip-dependencies-caching-path-with-pip-version: + name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }}, caching path) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + ubuntu-latest, + ubuntu-22.04, + ubuntu-24.04-arm, + ubuntu-22.04-arm, + windows-latest, + macos-latest, + macos-13 + ] + python-version: [3.13.0t, 3.13.1t, 3.13.2t] + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: ./ + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: __tests__/data/requirements.txt + pip-version: '25.0.1' + - name: Install dependencies + run: pip install numpy pandas requests diff --git a/.github/workflows/e2e-cache.yml b/.github/workflows/e2e-cache.yml index a3c9a2ba..76a8f8c5 100644 --- a/.github/workflows/e2e-cache.yml +++ b/.github/workflows/e2e-cache.yml @@ -249,3 +249,60 @@ jobs: } - name: Run Python Script run: pipenv run python test-pipenv.py + + python-pip-dependencies-caching-with-pip-version: + name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + ubuntu-latest, + ubuntu-24.04-arm, + ubuntu-22.04, + ubuntu-22.04-arm, + windows-latest, + macos-latest, + macos-13 + ] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: ./ + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + pip-version: '25.0.1' + - name: Install dependencies + run: pip install numpy pandas requests + + python-pip-dependencies-caching-path-with-pip-version: + name: Test pip (Python ${{ matrix.python-version}}, ${{ matrix.os }}, caching path) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: + [ + ubuntu-latest, + ubuntu-24.04-arm, + ubuntu-22.04, + ubuntu-22.04-arm, + windows-latest, + macos-latest, + macos-13 + ] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + steps: + - uses: actions/checkout@v4 + - name: Setup Python + uses: ./ + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + cache-dependency-path: __tests__/data/requirements.txt + pip-version: '25.0.1' + - name: Install dependencies + run: pip install numpy pandas requests diff --git a/README.md b/README.md index a91964a7..8dc6d08f 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ See examples of using `cache` and `cache-dependency-path` for `pipenv` and `poet - [Using `setup-python` with a self-hosted runner](docs/advanced-usage.md#using-setup-python-with-a-self-hosted-runner) - [Using `setup-python` on GHES](docs/advanced-usage.md#using-setup-python-on-ghes) - [Allow pre-releases](docs/advanced-usage.md#allow-pre-releases) +- [Using the pip-version input](docs/advanced-usage.md#using-the-pip-version-input) ## Recommended permissions diff --git a/action.yml b/action.yml index efa8de90..e469b7b2 100644 --- a/action.yml +++ b/action.yml @@ -29,6 +29,8 @@ inputs: freethreaded: description: "When 'true', use the freethreaded version of Python." default: false + pip-version: + description: "Used to specify the version of pip to install with the Python. Supported format: major[.minor][.patch]." outputs: python-version: description: "The installed Python or PyPy version. Useful when given a version range as input." diff --git a/dist/setup/index.js b/dist/setup/index.js index 4a3890aa..d8fae2a0 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -95990,6 +95990,7 @@ const semver = __importStar(__nccwpck_require__(2088)); const installer = __importStar(__nccwpck_require__(1919)); const core = __importStar(__nccwpck_require__(7484)); const tc = __importStar(__nccwpck_require__(3472)); +const exec = __importStar(__nccwpck_require__(5236)); // Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. // This is where pip is, along with anything that pip installs. // There is a separate directory for `pip install --user`. @@ -96010,6 +96011,20 @@ function binDir(installDir) { return path.join(installDir, 'bin'); } } +function installPip(pythonLocation) { + return __awaiter(this, void 0, void 0, function* () { + const pipVersion = core.getInput('pip-version'); + // Validate pip-version format: major[.minor][.patch] + const versionRegex = /^\d+(\.\d+)?(\.\d+)?$/; + if (pipVersion && !versionRegex.test(pipVersion)) { + throw new Error(`Invalid pip-version "${pipVersion}". Please specify a version in the format major[.minor][.patch].`); + } + if (pipVersion) { + core.info(`pip-version input is specified. Installing pip version ${pipVersion}`); + yield exec.exec(`${pythonLocation}/python -m pip install --upgrade pip==${pipVersion} --disable-pip-version-check --no-warn-script-location`); + } + }); +} function useCpythonVersion(version, architecture, updateEnvironment, checkLatest, allowPreReleases, freethreaded) { return __awaiter(this, void 0, void 0, function* () { var _a; @@ -96105,6 +96120,8 @@ function useCpythonVersion(version, architecture, updateEnvironment, checkLatest } core.setOutput('python-version', pythonVersion); core.setOutput('python-path', pythonPath); + const binaryPath = utils_1.IS_WINDOWS ? installDir : _binDir; + yield installPip(binaryPath); return { impl: 'CPython', version: pythonVersion }; }); } diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 72b35016..7a8f1187 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -22,6 +22,7 @@ - [macOS](advanced-usage.md#macos) - [Using `setup-python` on GHES](advanced-usage.md#using-setup-python-on-ghes) - [Allow pre-releases](advanced-usage.md#allow-pre-releases) +- [Using the pip-version input](advanced-usage.md#using-the-pip-version-input) ## Using the `python-version` input @@ -643,3 +644,22 @@ jobs: - run: pipx run nox --error-on-missing-interpreters -s tests-${{ matrix.python_version }} ``` +## Using the pip-version input + +The `pip-version` input allows you to specify the desired version of **Pip** to use with the standard Python version. +The version of Pip should be specified in the format `major`, `major.minor`, or `major.minor.patch` (for example: 25, 25.1, or 25.0.1). + +```yaml + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.13' + pip-version: '25.0.1' + - name: Display Pip version + run: pip --version +``` +> The `pip-version` input is supported only with standard Python versions. It is not available when using PyPy or GraalPy. + +> Using a specific or outdated version of pip may result in compatibility or security issues and can cause job failures. For best practices and guidance, refer to the official [pip documentation](https://pip.pypa.io/en/stable/). \ No newline at end of file diff --git a/src/find-python.ts b/src/find-python.ts index ddb027cb..88e530f4 100644 --- a/src/find-python.ts +++ b/src/find-python.ts @@ -8,6 +8,7 @@ import * as installer from './install-python'; import * as core from '@actions/core'; import * as tc from '@actions/tool-cache'; +import * as exec from '@actions/exec'; // Python has "scripts" or "bin" directories where command-line tools that come with packages are installed. // This is where pip is, along with anything that pip installs. @@ -30,6 +31,27 @@ function binDir(installDir: string): string { } } +async function installPip(pythonLocation: string) { + const pipVersion = core.getInput('pip-version'); + + // Validate pip-version format: major[.minor][.patch] + const versionRegex = /^\d+(\.\d+)?(\.\d+)?$/; + if (pipVersion && !versionRegex.test(pipVersion)) { + throw new Error( + `Invalid pip-version "${pipVersion}". Please specify a version in the format major[.minor][.patch].` + ); + } + + if (pipVersion) { + core.info( + `pip-version input is specified. Installing pip version ${pipVersion}` + ); + await exec.exec( + `${pythonLocation}/python -m pip install --upgrade pip==${pipVersion} --disable-pip-version-check --no-warn-script-location` + ); + } +} + export async function useCpythonVersion( version: string, architecture: string, @@ -179,6 +201,9 @@ export async function useCpythonVersion( core.setOutput('python-version', pythonVersion); core.setOutput('python-path', pythonPath); + const binaryPath = IS_WINDOWS ? installDir : _binDir; + await installPip(binaryPath); + return {impl: 'CPython', version: pythonVersion}; } From 12648859835f68b273febdd9aab9972bbb624d8c Mon Sep 17 00:00:00 2001 From: aparnajyothi-y <147696841+aparnajyothi-y@users.noreply.github.com> Date: Wed, 25 Jun 2025 10:10:44 +0530 Subject: [PATCH 4/4] Enhance cache-dependency-path handling to support files outside the workspace root (#1128) * ehnace cache dependency path handling * logic update * npm run format-check * update cacheDependencies tests to cover resolved paths and copy edge cases * check failure fix * depricate-windows-2019 * refactored the code * Check failure fix --- .github/workflows/test-pypy.yml | 1 - __tests__/setup-python.test.ts | 149 ++++++++++++++++++++++++++++++++ dist/setup/index.js | 43 ++++++++- docs/advanced-usage.md | 2 +- src/setup-python.ts | 53 +++++++++++- 5 files changed, 243 insertions(+), 5 deletions(-) create mode 100644 __tests__/setup-python.test.ts diff --git a/.github/workflows/test-pypy.yml b/.github/workflows/test-pypy.yml index 54466e52..6433b7c5 100644 --- a/.github/workflows/test-pypy.yml +++ b/.github/workflows/test-pypy.yml @@ -88,7 +88,6 @@ jobs: - macos-13 - macos-14 - macos-15 - - windows-2019 - windows-2022 - windows-2025 - ubuntu-22.04 diff --git a/__tests__/setup-python.test.ts b/__tests__/setup-python.test.ts new file mode 100644 index 00000000..bb27289d --- /dev/null +++ b/__tests__/setup-python.test.ts @@ -0,0 +1,149 @@ +import * as core from '@actions/core'; +import * as fs from 'fs'; +import * as path from 'path'; +import {cacheDependencies} from '../src/setup-python'; +import {getCacheDistributor} from '../src/cache-distributions/cache-factory'; + +jest.mock('fs', () => { + const actualFs = jest.requireActual('fs'); + return { + ...actualFs, + promises: { + access: jest.fn(), + mkdir: jest.fn(), + copyFile: jest.fn(), + writeFile: jest.fn(), + appendFile: jest.fn() + } + }; +}); +jest.mock('@actions/core'); +jest.mock('../src/cache-distributions/cache-factory'); + +const mockedFsPromises = fs.promises as jest.Mocked; +const mockedCore = core as jest.Mocked; +const mockedGetCacheDistributor = getCacheDistributor as jest.Mock; + +describe('cacheDependencies', () => { + const mockRestoreCache = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + process.env.GITHUB_ACTION_PATH = '/github/action'; + process.env.GITHUB_WORKSPACE = '/github/workspace'; + + mockedCore.getInput.mockReturnValue('nested/deps.lock'); + + // Simulate file exists by resolving access without error + mockedFsPromises.access.mockImplementation(async p => { + const pathStr = typeof p === 'string' ? p : p.toString(); + if (pathStr === '/github/action/nested/deps.lock') { + return Promise.resolve(); + } + // Simulate directory doesn't exist to test mkdir + if (pathStr === path.dirname('/github/workspace/nested/deps.lock')) { + return Promise.reject(new Error('no dir')); + } + return Promise.resolve(); + }); + + // Simulate mkdir success + mockedFsPromises.mkdir.mockResolvedValue(undefined); + + // Simulate copyFile success + mockedFsPromises.copyFile.mockResolvedValue(undefined); + + mockedGetCacheDistributor.mockReturnValue({restoreCache: mockRestoreCache}); + }); + + it('copies the dependency file and resolves the path with directory structure', async () => { + await cacheDependencies('pip', '3.12'); + + const sourcePath = path.resolve('/github/action', 'nested/deps.lock'); + const targetPath = path.resolve('/github/workspace', 'nested/deps.lock'); + + expect(mockedFsPromises.access).toHaveBeenCalledWith( + sourcePath, + fs.constants.F_OK + ); + expect(mockedFsPromises.mkdir).toHaveBeenCalledWith( + path.dirname(targetPath), + { + recursive: true + } + ); + expect(mockedFsPromises.copyFile).toHaveBeenCalledWith( + sourcePath, + targetPath + ); + expect(mockedCore.info).toHaveBeenCalledWith( + `Copied ${sourcePath} to ${targetPath}` + ); + expect(mockedCore.info).toHaveBeenCalledWith( + `Resolved cache-dependency-path: nested/deps.lock` + ); + expect(mockRestoreCache).toHaveBeenCalled(); + }); + + it('warns if the dependency file does not exist', async () => { + // Simulate file does not exist by rejecting access + mockedFsPromises.access.mockRejectedValue(new Error('file not found')); + + await cacheDependencies('pip', '3.12'); + + expect(mockedCore.warning).toHaveBeenCalledWith( + expect.stringContaining('does not exist') + ); + expect(mockedFsPromises.copyFile).not.toHaveBeenCalled(); + expect(mockRestoreCache).toHaveBeenCalled(); + }); + + it('warns if file copy fails', async () => { + // Simulate copyFile failure + mockedFsPromises.copyFile.mockRejectedValue(new Error('copy failed')); + + await cacheDependencies('pip', '3.12'); + + expect(mockedCore.warning).toHaveBeenCalledWith( + expect.stringContaining('Failed to copy file') + ); + expect(mockRestoreCache).toHaveBeenCalled(); + }); + + it('skips path logic if no input is provided', async () => { + mockedCore.getInput.mockReturnValue(''); + + await cacheDependencies('pip', '3.12'); + + expect(mockedFsPromises.copyFile).not.toHaveBeenCalled(); + expect(mockedCore.warning).not.toHaveBeenCalled(); + expect(mockRestoreCache).toHaveBeenCalled(); + }); + + it('does not copy if dependency file is already inside the workspace but still sets resolved path', async () => { + // Simulate cacheDependencyPath inside workspace + mockedCore.getInput.mockReturnValue('deps.lock'); + + // Override sourcePath and targetPath to be equal + const actionPath = '/github/workspace'; // same path for action and workspace + process.env.GITHUB_ACTION_PATH = actionPath; + process.env.GITHUB_WORKSPACE = actionPath; + + // access resolves to simulate file exists + mockedFsPromises.access.mockResolvedValue(); + + await cacheDependencies('pip', '3.12'); + + const sourcePath = path.resolve(actionPath, 'deps.lock'); + const targetPath = sourcePath; // same path + + expect(mockedFsPromises.copyFile).not.toHaveBeenCalled(); + expect(mockedCore.info).toHaveBeenCalledWith( + `Dependency file is already inside the workspace: ${sourcePath}` + ); + expect(mockedCore.info).toHaveBeenCalledWith( + `Resolved cache-dependency-path: deps.lock` + ); + expect(mockRestoreCache).toHaveBeenCalled(); + }); +}); diff --git a/dist/setup/index.js b/dist/setup/index.js index d8fae2a0..c2f220c0 100644 --- a/dist/setup/index.js +++ b/dist/setup/index.js @@ -96844,6 +96844,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", ({ value: true })); +exports.cacheDependencies = void 0; const core = __importStar(__nccwpck_require__(7484)); const finder = __importStar(__nccwpck_require__(6843)); const finderPyPy = __importStar(__nccwpck_require__(2625)); @@ -96862,10 +96863,50 @@ function isGraalPyVersion(versionSpec) { function cacheDependencies(cache, pythonVersion) { return __awaiter(this, void 0, void 0, function* () { const cacheDependencyPath = core.getInput('cache-dependency-path') || undefined; - const cacheDistributor = (0, cache_factory_1.getCacheDistributor)(cache, pythonVersion, cacheDependencyPath); + let resolvedDependencyPath = undefined; + if (cacheDependencyPath) { + const actionPath = process.env.GITHUB_ACTION_PATH || ''; + const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); + const sourcePath = path.resolve(actionPath, cacheDependencyPath); + const relativePath = path.relative(actionPath, sourcePath); + const targetPath = path.resolve(workspace, relativePath); + try { + const sourceExists = yield fs_1.default.promises + .access(sourcePath, fs_1.default.constants.F_OK) + .then(() => true) + .catch(() => false); + if (!sourceExists) { + core.warning(`The resolved cache-dependency-path does not exist: ${sourcePath}`); + } + else { + if (sourcePath !== targetPath) { + const targetDir = path.dirname(targetPath); + // Create target directory if it doesn't exist + yield fs_1.default.promises.mkdir(targetDir, { recursive: true }); + // Copy file asynchronously + yield fs_1.default.promises.copyFile(sourcePath, targetPath); + core.info(`Copied ${sourcePath} to ${targetPath}`); + } + else { + core.info(`Dependency file is already inside the workspace: ${sourcePath}`); + } + resolvedDependencyPath = path + .relative(workspace, targetPath) + .replace(/\\/g, '/'); + core.info(`Resolved cache-dependency-path: ${resolvedDependencyPath}`); + } + } + catch (error) { + core.warning(`Failed to copy file from ${sourcePath} to ${targetPath}: ${error}`); + } + } + // Pass resolvedDependencyPath if available, else fallback to original input + const dependencyPathForCache = resolvedDependencyPath !== null && resolvedDependencyPath !== void 0 ? resolvedDependencyPath : cacheDependencyPath; + const cacheDistributor = (0, cache_factory_1.getCacheDistributor)(cache, pythonVersion, dependencyPathForCache); yield cacheDistributor.restoreCache(); }); } +exports.cacheDependencies = cacheDependencies; function resolveVersionInputFromDefaultFile() { const couples = [ ['.python-version', utils_1.getVersionsInputFromPlainFile] diff --git a/docs/advanced-usage.md b/docs/advanced-usage.md index 7a8f1187..96524823 100644 --- a/docs/advanced-usage.md +++ b/docs/advanced-usage.md @@ -412,7 +412,7 @@ steps: - run: pip install -e . # Or pip install -e '.[test]' to install test dependencies ``` - +Note: cache-dependency-path supports files located outside the workspace root by copying them into the workspace to enable proper caching. # Outputs and environment variables ## Outputs diff --git a/src/setup-python.ts b/src/setup-python.ts index 5d585d73..106b415a 100644 --- a/src/setup-python.ts +++ b/src/setup-python.ts @@ -22,13 +22,62 @@ function isGraalPyVersion(versionSpec: string) { return versionSpec.startsWith('graalpy'); } -async function cacheDependencies(cache: string, pythonVersion: string) { +export async function cacheDependencies(cache: string, pythonVersion: string) { const cacheDependencyPath = core.getInput('cache-dependency-path') || undefined; + let resolvedDependencyPath: string | undefined = undefined; + + if (cacheDependencyPath) { + const actionPath = process.env.GITHUB_ACTION_PATH || ''; + const workspace = process.env.GITHUB_WORKSPACE || process.cwd(); + + const sourcePath = path.resolve(actionPath, cacheDependencyPath); + const relativePath = path.relative(actionPath, sourcePath); + const targetPath = path.resolve(workspace, relativePath); + + try { + const sourceExists = await fs.promises + .access(sourcePath, fs.constants.F_OK) + .then(() => true) + .catch(() => false); + + if (!sourceExists) { + core.warning( + `The resolved cache-dependency-path does not exist: ${sourcePath}` + ); + } else { + if (sourcePath !== targetPath) { + const targetDir = path.dirname(targetPath); + // Create target directory if it doesn't exist + await fs.promises.mkdir(targetDir, {recursive: true}); + // Copy file asynchronously + await fs.promises.copyFile(sourcePath, targetPath); + core.info(`Copied ${sourcePath} to ${targetPath}`); + } else { + core.info( + `Dependency file is already inside the workspace: ${sourcePath}` + ); + } + + resolvedDependencyPath = path + .relative(workspace, targetPath) + .replace(/\\/g, '/'); + core.info(`Resolved cache-dependency-path: ${resolvedDependencyPath}`); + } + } catch (error) { + core.warning( + `Failed to copy file from ${sourcePath} to ${targetPath}: ${error}` + ); + } + } + + // Pass resolvedDependencyPath if available, else fallback to original input + const dependencyPathForCache = resolvedDependencyPath ?? cacheDependencyPath; + const cacheDistributor = getCacheDistributor( cache, pythonVersion, - cacheDependencyPath + dependencyPathForCache ); await cacheDistributor.restoreCache(); }