Skip to content

Commit 1264885

Browse files
Enhance cache-dependency-path handling to support files outside the workspace root (actions#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
1 parent e9c40fb commit 1264885

File tree

5 files changed

+243
-5
lines changed

5 files changed

+243
-5
lines changed

.github/workflows/test-pypy.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ jobs:
8888
- macos-13
8989
- macos-14
9090
- macos-15
91-
- windows-2019
9291
- windows-2022
9392
- windows-2025
9493
- ubuntu-22.04

__tests__/setup-python.test.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import * as core from '@actions/core';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import {cacheDependencies} from '../src/setup-python';
5+
import {getCacheDistributor} from '../src/cache-distributions/cache-factory';
6+
7+
jest.mock('fs', () => {
8+
const actualFs = jest.requireActual('fs');
9+
return {
10+
...actualFs,
11+
promises: {
12+
access: jest.fn(),
13+
mkdir: jest.fn(),
14+
copyFile: jest.fn(),
15+
writeFile: jest.fn(),
16+
appendFile: jest.fn()
17+
}
18+
};
19+
});
20+
jest.mock('@actions/core');
21+
jest.mock('../src/cache-distributions/cache-factory');
22+
23+
const mockedFsPromises = fs.promises as jest.Mocked<typeof fs.promises>;
24+
const mockedCore = core as jest.Mocked<typeof core>;
25+
const mockedGetCacheDistributor = getCacheDistributor as jest.Mock;
26+
27+
describe('cacheDependencies', () => {
28+
const mockRestoreCache = jest.fn();
29+
30+
beforeEach(() => {
31+
jest.clearAllMocks();
32+
process.env.GITHUB_ACTION_PATH = '/github/action';
33+
process.env.GITHUB_WORKSPACE = '/github/workspace';
34+
35+
mockedCore.getInput.mockReturnValue('nested/deps.lock');
36+
37+
// Simulate file exists by resolving access without error
38+
mockedFsPromises.access.mockImplementation(async p => {
39+
const pathStr = typeof p === 'string' ? p : p.toString();
40+
if (pathStr === '/github/action/nested/deps.lock') {
41+
return Promise.resolve();
42+
}
43+
// Simulate directory doesn't exist to test mkdir
44+
if (pathStr === path.dirname('/github/workspace/nested/deps.lock')) {
45+
return Promise.reject(new Error('no dir'));
46+
}
47+
return Promise.resolve();
48+
});
49+
50+
// Simulate mkdir success
51+
mockedFsPromises.mkdir.mockResolvedValue(undefined);
52+
53+
// Simulate copyFile success
54+
mockedFsPromises.copyFile.mockResolvedValue(undefined);
55+
56+
mockedGetCacheDistributor.mockReturnValue({restoreCache: mockRestoreCache});
57+
});
58+
59+
it('copies the dependency file and resolves the path with directory structure', async () => {
60+
await cacheDependencies('pip', '3.12');
61+
62+
const sourcePath = path.resolve('/github/action', 'nested/deps.lock');
63+
const targetPath = path.resolve('/github/workspace', 'nested/deps.lock');
64+
65+
expect(mockedFsPromises.access).toHaveBeenCalledWith(
66+
sourcePath,
67+
fs.constants.F_OK
68+
);
69+
expect(mockedFsPromises.mkdir).toHaveBeenCalledWith(
70+
path.dirname(targetPath),
71+
{
72+
recursive: true
73+
}
74+
);
75+
expect(mockedFsPromises.copyFile).toHaveBeenCalledWith(
76+
sourcePath,
77+
targetPath
78+
);
79+
expect(mockedCore.info).toHaveBeenCalledWith(
80+
`Copied ${sourcePath} to ${targetPath}`
81+
);
82+
expect(mockedCore.info).toHaveBeenCalledWith(
83+
`Resolved cache-dependency-path: nested/deps.lock`
84+
);
85+
expect(mockRestoreCache).toHaveBeenCalled();
86+
});
87+
88+
it('warns if the dependency file does not exist', async () => {
89+
// Simulate file does not exist by rejecting access
90+
mockedFsPromises.access.mockRejectedValue(new Error('file not found'));
91+
92+
await cacheDependencies('pip', '3.12');
93+
94+
expect(mockedCore.warning).toHaveBeenCalledWith(
95+
expect.stringContaining('does not exist')
96+
);
97+
expect(mockedFsPromises.copyFile).not.toHaveBeenCalled();
98+
expect(mockRestoreCache).toHaveBeenCalled();
99+
});
100+
101+
it('warns if file copy fails', async () => {
102+
// Simulate copyFile failure
103+
mockedFsPromises.copyFile.mockRejectedValue(new Error('copy failed'));
104+
105+
await cacheDependencies('pip', '3.12');
106+
107+
expect(mockedCore.warning).toHaveBeenCalledWith(
108+
expect.stringContaining('Failed to copy file')
109+
);
110+
expect(mockRestoreCache).toHaveBeenCalled();
111+
});
112+
113+
it('skips path logic if no input is provided', async () => {
114+
mockedCore.getInput.mockReturnValue('');
115+
116+
await cacheDependencies('pip', '3.12');
117+
118+
expect(mockedFsPromises.copyFile).not.toHaveBeenCalled();
119+
expect(mockedCore.warning).not.toHaveBeenCalled();
120+
expect(mockRestoreCache).toHaveBeenCalled();
121+
});
122+
123+
it('does not copy if dependency file is already inside the workspace but still sets resolved path', async () => {
124+
// Simulate cacheDependencyPath inside workspace
125+
mockedCore.getInput.mockReturnValue('deps.lock');
126+
127+
// Override sourcePath and targetPath to be equal
128+
const actionPath = '/github/workspace'; // same path for action and workspace
129+
process.env.GITHUB_ACTION_PATH = actionPath;
130+
process.env.GITHUB_WORKSPACE = actionPath;
131+
132+
// access resolves to simulate file exists
133+
mockedFsPromises.access.mockResolvedValue();
134+
135+
await cacheDependencies('pip', '3.12');
136+
137+
const sourcePath = path.resolve(actionPath, 'deps.lock');
138+
const targetPath = sourcePath; // same path
139+
140+
expect(mockedFsPromises.copyFile).not.toHaveBeenCalled();
141+
expect(mockedCore.info).toHaveBeenCalledWith(
142+
`Dependency file is already inside the workspace: ${sourcePath}`
143+
);
144+
expect(mockedCore.info).toHaveBeenCalledWith(
145+
`Resolved cache-dependency-path: deps.lock`
146+
);
147+
expect(mockRestoreCache).toHaveBeenCalled();
148+
});
149+
});

dist/setup/index.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96844,6 +96844,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9684496844
return (mod && mod.__esModule) ? mod : { "default": mod };
9684596845
};
9684696846
Object.defineProperty(exports, "__esModule", ({ value: true }));
96847+
exports.cacheDependencies = void 0;
9684796848
const core = __importStar(__nccwpck_require__(7484));
9684896849
const finder = __importStar(__nccwpck_require__(6843));
9684996850
const finderPyPy = __importStar(__nccwpck_require__(2625));
@@ -96862,10 +96863,50 @@ function isGraalPyVersion(versionSpec) {
9686296863
function cacheDependencies(cache, pythonVersion) {
9686396864
return __awaiter(this, void 0, void 0, function* () {
9686496865
const cacheDependencyPath = core.getInput('cache-dependency-path') || undefined;
96865-
const cacheDistributor = (0, cache_factory_1.getCacheDistributor)(cache, pythonVersion, cacheDependencyPath);
96866+
let resolvedDependencyPath = undefined;
96867+
if (cacheDependencyPath) {
96868+
const actionPath = process.env.GITHUB_ACTION_PATH || '';
96869+
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
96870+
const sourcePath = path.resolve(actionPath, cacheDependencyPath);
96871+
const relativePath = path.relative(actionPath, sourcePath);
96872+
const targetPath = path.resolve(workspace, relativePath);
96873+
try {
96874+
const sourceExists = yield fs_1.default.promises
96875+
.access(sourcePath, fs_1.default.constants.F_OK)
96876+
.then(() => true)
96877+
.catch(() => false);
96878+
if (!sourceExists) {
96879+
core.warning(`The resolved cache-dependency-path does not exist: ${sourcePath}`);
96880+
}
96881+
else {
96882+
if (sourcePath !== targetPath) {
96883+
const targetDir = path.dirname(targetPath);
96884+
// Create target directory if it doesn't exist
96885+
yield fs_1.default.promises.mkdir(targetDir, { recursive: true });
96886+
// Copy file asynchronously
96887+
yield fs_1.default.promises.copyFile(sourcePath, targetPath);
96888+
core.info(`Copied ${sourcePath} to ${targetPath}`);
96889+
}
96890+
else {
96891+
core.info(`Dependency file is already inside the workspace: ${sourcePath}`);
96892+
}
96893+
resolvedDependencyPath = path
96894+
.relative(workspace, targetPath)
96895+
.replace(/\\/g, '/');
96896+
core.info(`Resolved cache-dependency-path: ${resolvedDependencyPath}`);
96897+
}
96898+
}
96899+
catch (error) {
96900+
core.warning(`Failed to copy file from ${sourcePath} to ${targetPath}: ${error}`);
96901+
}
96902+
}
96903+
// Pass resolvedDependencyPath if available, else fallback to original input
96904+
const dependencyPathForCache = resolvedDependencyPath !== null && resolvedDependencyPath !== void 0 ? resolvedDependencyPath : cacheDependencyPath;
96905+
const cacheDistributor = (0, cache_factory_1.getCacheDistributor)(cache, pythonVersion, dependencyPathForCache);
9686696906
yield cacheDistributor.restoreCache();
9686796907
});
9686896908
}
96909+
exports.cacheDependencies = cacheDependencies;
9686996910
function resolveVersionInputFromDefaultFile() {
9687096911
const couples = [
9687196912
['.python-version', utils_1.getVersionsInputFromPlainFile]

docs/advanced-usage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -412,7 +412,7 @@ steps:
412412
- run: pip install -e .
413413
# Or pip install -e '.[test]' to install test dependencies
414414
```
415-
415+
Note: cache-dependency-path supports files located outside the workspace root by copying them into the workspace to enable proper caching.
416416
# Outputs and environment variables
417417

418418
## Outputs

src/setup-python.ts

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,62 @@ function isGraalPyVersion(versionSpec: string) {
2222
return versionSpec.startsWith('graalpy');
2323
}
2424

25-
async function cacheDependencies(cache: string, pythonVersion: string) {
25+
export async function cacheDependencies(cache: string, pythonVersion: string) {
2626
const cacheDependencyPath =
2727
core.getInput('cache-dependency-path') || undefined;
28+
let resolvedDependencyPath: string | undefined = undefined;
29+
30+
if (cacheDependencyPath) {
31+
const actionPath = process.env.GITHUB_ACTION_PATH || '';
32+
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
33+
34+
const sourcePath = path.resolve(actionPath, cacheDependencyPath);
35+
const relativePath = path.relative(actionPath, sourcePath);
36+
const targetPath = path.resolve(workspace, relativePath);
37+
38+
try {
39+
const sourceExists = await fs.promises
40+
.access(sourcePath, fs.constants.F_OK)
41+
.then(() => true)
42+
.catch(() => false);
43+
44+
if (!sourceExists) {
45+
core.warning(
46+
`The resolved cache-dependency-path does not exist: ${sourcePath}`
47+
);
48+
} else {
49+
if (sourcePath !== targetPath) {
50+
const targetDir = path.dirname(targetPath);
51+
// Create target directory if it doesn't exist
52+
await fs.promises.mkdir(targetDir, {recursive: true});
53+
// Copy file asynchronously
54+
await fs.promises.copyFile(sourcePath, targetPath);
55+
core.info(`Copied ${sourcePath} to ${targetPath}`);
56+
} else {
57+
core.info(
58+
`Dependency file is already inside the workspace: ${sourcePath}`
59+
);
60+
}
61+
62+
resolvedDependencyPath = path
63+
.relative(workspace, targetPath)
64+
.replace(/\\/g, '/');
65+
core.info(`Resolved cache-dependency-path: ${resolvedDependencyPath}`);
66+
}
67+
} catch (error) {
68+
core.warning(
69+
`Failed to copy file from ${sourcePath} to ${targetPath}: ${error}`
70+
);
71+
}
72+
}
73+
74+
// Pass resolvedDependencyPath if available, else fallback to original input
75+
const dependencyPathForCache = resolvedDependencyPath ?? cacheDependencyPath;
76+
2877
const cacheDistributor = getCacheDistributor(
2978
cache,
3079
pythonVersion,
31-
cacheDependencyPath
80+
dependencyPathForCache
3281
);
3382
await cacheDistributor.restoreCache();
3483
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy