Skip to content

Commit 1264885

Browse files
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
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)