This commit is contained in:
aparnajyothi-y 2025-07-14 18:01:08 +05:30 committed by GitHub
commit 0bb10f5fa1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 103 additions and 75 deletions

View file

@ -27,51 +27,35 @@ const mockedGetCacheDistributor = getCacheDistributor as jest.Mock;
describe('cacheDependencies', () => {
const mockRestoreCache = jest.fn();
const actionPath = path.resolve('/github/action');
const workspacePath = path.resolve('/github/workspace');
const relPath = path.join('nested', 'deps.lock');
const sourcePath = path.resolve(actionPath, relPath);
const targetPath = path.resolve(workspacePath, relPath);
beforeEach(() => {
jest.clearAllMocks();
process.env.GITHUB_ACTION_PATH = '/github/action';
process.env.GITHUB_WORKSPACE = '/github/workspace';
mockedCore.getInput.mockReturnValue('nested/deps.lock');
process.env.GITHUB_ACTION_PATH = actionPath;
process.env.GITHUB_WORKSPACE = workspacePath;
// 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);
mockedCore.getInput.mockReturnValue(relPath);
mockedCore.getBooleanInput.mockReturnValue(false);
mockedGetCacheDistributor.mockReturnValue({restoreCache: mockRestoreCache});
mockedFsPromises.mkdir.mockResolvedValue(undefined);
mockedFsPromises.copyFile.mockResolvedValue(undefined);
});
it('copies the dependency file and resolves the path with directory structure', async () => {
it('copies the file if source exists and target does not', async () => {
mockedFsPromises.access.mockImplementation(async p => {
if (p === sourcePath) return Promise.resolve();
throw new Error('target does not exist');
});
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
@ -79,15 +63,40 @@ describe('cacheDependencies', () => {
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'));
it('overwrites file if target exists and overwrite is true', async () => {
mockedCore.getBooleanInput.mockReturnValue(true);
mockedFsPromises.access.mockResolvedValue(); // both exist
await cacheDependencies('pip', '3.12');
expect(mockedFsPromises.copyFile).toHaveBeenCalledWith(
sourcePath,
targetPath
);
expect(mockedCore.info).toHaveBeenCalledWith(
`Overwrote ${sourcePath} to ${targetPath}`
);
});
it('skips copy if file exists and overwrite is false', async () => {
mockedCore.getBooleanInput.mockReturnValue(false);
mockedFsPromises.access.mockResolvedValue(); // both exist
await cacheDependencies('pip', '3.12');
expect(mockedFsPromises.copyFile).not.toHaveBeenCalled();
expect(mockedCore.info).toHaveBeenCalledWith(
expect.stringContaining('Skipped copying')
);
});
it('logs warning if source file does not exist', async () => {
mockedFsPromises.access.mockImplementation(async p => {
if (p === sourcePath) throw new Error('not found');
return Promise.resolve();
});
await cacheDependencies('pip', '3.12');
@ -95,11 +104,13 @@ describe('cacheDependencies', () => {
expect.stringContaining('does not exist')
);
expect(mockedFsPromises.copyFile).not.toHaveBeenCalled();
expect(mockRestoreCache).toHaveBeenCalled();
});
it('warns if file copy fails', async () => {
// Simulate copyFile failure
it('logs warning if copyFile fails', async () => {
mockedFsPromises.access.mockImplementation(async p => {
if (p === sourcePath) return Promise.resolve();
throw new Error('target missing');
});
mockedFsPromises.copyFile.mockRejectedValue(new Error('copy failed'));
await cacheDependencies('pip', '3.12');
@ -107,43 +118,30 @@ describe('cacheDependencies', () => {
expect(mockedCore.warning).toHaveBeenCalledWith(
expect.stringContaining('Failed to copy file')
);
expect(mockRestoreCache).toHaveBeenCalled();
});
it('skips path logic if no input is provided', async () => {
it('skips everything if cache-dependency-path is not 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
it('does not copy if source and target are the same path', async () => {
mockedCore.getInput.mockReturnValue('deps.lock');
const samePath = path.resolve('/github/workspace', 'deps.lock');
process.env.GITHUB_ACTION_PATH = workspacePath;
process.env.GITHUB_WORKSPACE = workspacePath;
// 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}`
`Dependency file is already inside the workspace: ${samePath}`
);
expect(mockedCore.info).toHaveBeenCalledWith(
`Resolved cache-dependency-path: deps.lock`
);
expect(mockRestoreCache).toHaveBeenCalled();
});
});

View file

@ -31,6 +31,9 @@ inputs:
default: false
pip-version:
description: "Used to specify the version of pip to install with the Python. Supported format: major[.minor][.patch]."
overwrite:
description: "Whether to overwrite existing files in the workspace when a composite actions cache-dependency-path points to a file with the same name (e.g., requirements.txt). Defaults to false."
default: 'false'
outputs:
python-version:
description: "The installed Python or PyPy version. Useful when given a version range as input."

18
dist/setup/index.js vendored
View file

@ -96885,6 +96885,7 @@ function cacheDependencies(cache, pythonVersion) {
return __awaiter(this, void 0, void 0, function* () {
const cacheDependencyPath = core.getInput('cache-dependency-path') || undefined;
let resolvedDependencyPath = undefined;
const overwrite = core.getBooleanInput('overwrite', { required: false });
if (cacheDependencyPath) {
const actionPath = process.env.GITHUB_ACTION_PATH || '';
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
@ -96902,11 +96903,20 @@ function cacheDependencies(cache, pythonVersion) {
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}`);
const targetExists = yield fs_1.default.promises
.access(targetPath, fs_1.default.constants.F_OK)
.then(() => true)
.catch(() => false);
if (targetExists && !overwrite) {
const filename = path.basename(cacheDependencyPath);
core.warning(`A file named '${filename}' exists in both the composite action and the workspace. The file in the workspace will be used. To avoid ambiguity, consider renaming one of the files or setting 'overwrite: true'.`);
core.info(`Skipped copying ${sourcePath} — target already exists at ${targetPath}`);
}
else {
yield fs_1.default.promises.copyFile(sourcePath, targetPath);
core.info(`${targetExists ? 'Overwrote' : 'Copied'} ${sourcePath} to ${targetPath}`);
}
}
else {
core.info(`Dependency file is already inside the workspace: ${sourcePath}`);

View file

@ -1,3 +1,4 @@
# Advanced Usage
- [Using the python-version input](advanced-usage.md#using-the-python-version-input)
- [Specifying a Python version](advanced-usage.md#specifying-a-python-version)
@ -413,6 +414,7 @@ steps:
# 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.
A new input overwrite has been introduced to prevent accidental overwriting of existing files in the workspace when a composite actions cache-dependency-path refers to common filenames like requirements.txt. By default, if a file with the same path already exists in the workspace, it will not be copied from the action unless overwrite: true is explicitly set.
# Outputs and environment variables
## Outputs
@ -662,4 +664,4 @@ The version of Pip should be specified in the format `major`, `major.minor`, or
```
> 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/).
> 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/).

View file

@ -21,11 +21,11 @@ function isPyPyVersion(versionSpec: string) {
function isGraalPyVersion(versionSpec: string) {
return versionSpec.startsWith('graalpy');
}
export async function cacheDependencies(cache: string, pythonVersion: string) {
const cacheDependencyPath =
core.getInput('cache-dependency-path') || undefined;
let resolvedDependencyPath: string | undefined = undefined;
const overwrite = core.getBooleanInput('overwrite', {required: false});
if (cacheDependencyPath) {
const actionPath = process.env.GITHUB_ACTION_PATH || '';
@ -48,11 +48,27 @@ export async function cacheDependencies(cache: string, pythonVersion: string) {
} 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}`);
const targetExists = await fs.promises
.access(targetPath, fs.constants.F_OK)
.then(() => true)
.catch(() => false);
if (targetExists && !overwrite) {
const filename = path.basename(cacheDependencyPath);
core.warning(
`A file named '${filename}' exists in both the composite action and the workspace. The file in the workspace will be used. To avoid ambiguity, consider renaming one of the files or setting 'overwrite: true'.`
);
core.info(
`Skipped copying ${sourcePath} — target already exists at ${targetPath}`
);
} else {
await fs.promises.copyFile(sourcePath, targetPath);
core.info(
`${targetExists ? 'Overwrote' : 'Copied'} ${sourcePath} to ${targetPath}`
);
}
} else {
core.info(
`Dependency file is already inside the workspace: ${sourcePath}`
@ -81,7 +97,6 @@ export async function cacheDependencies(cache: string, pythonVersion: string) {
);
await cacheDistributor.restoreCache();
}
function resolveVersionInputFromDefaultFile(): string[] {
const couples: [string, (versionFile: string) => string[]][] = [
['.python-version', getVersionsInputFromPlainFile]