Skip to content

Add overwrite input to optionally allow copying over existing dependency files from non-root directories #1149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
disable-auto-overwriting
  • Loading branch information
aparnajyothi-y committed Jul 11, 2025
commit bf549696cd48f7400292cba2ddb974c3b0c6f970
110 changes: 56 additions & 54 deletions __tests__/setup-python.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,117 +33,119 @@ describe('cacheDependencies', () => {
process.env.GITHUB_WORKSPACE = '/github/workspace';

mockedCore.getInput.mockReturnValue('nested/deps.lock');
mockedCore.getBooleanInput.mockReturnValue(false);

// 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();
});
mockedGetCacheDistributor.mockReturnValue({restoreCache: mockRestoreCache});

// 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 () => {
it('copies the file if source exists and target does not', async () => {
mockedFsPromises.access.mockImplementation(async filePath => {
if (filePath === '/github/action/nested/deps.lock')
return Promise.resolve(); // source
throw new Error('target does not exist'); // target
});

await cacheDependencies('pip', '3.12');

const sourcePath = path.resolve('/github/action', 'nested/deps.lock');
const targetPath = path.resolve('/github/workspace', 'nested/deps.lock');
const sourcePath = '/github/action/nested/deps.lock';
const targetPath = '/github/workspace/nested/deps.lock';

expect(mockedFsPromises.access).toHaveBeenCalledWith(
expect(mockedFsPromises.copyFile).toHaveBeenCalledWith(
sourcePath,
fs.constants.F_OK
targetPath
);
expect(mockedFsPromises.mkdir).toHaveBeenCalledWith(
path.dirname(targetPath),
{
recursive: true
}
expect(mockedCore.info).toHaveBeenCalledWith(
`Copied ${sourcePath} to ${targetPath}`
);
});

it('overwrites file if target exists and overwrite is true', async () => {
mockedCore.getBooleanInput.mockReturnValue(true);
mockedFsPromises.access.mockResolvedValue(); // both source and target exist

await cacheDependencies('pip', '3.12');

const sourcePath = '/github/action/nested/deps.lock';
const targetPath = '/github/workspace/nested/deps.lock';

expect(mockedFsPromises.copyFile).toHaveBeenCalledWith(
sourcePath,
targetPath
);
expect(mockedCore.info).toHaveBeenCalledWith(
`Copied ${sourcePath} to ${targetPath}`
`Overwrote ${sourcePath} to ${targetPath}`
);
});

it('skips copy if file exists and overwrite is false', async () => {
mockedCore.getBooleanInput.mockReturnValue(false);
mockedFsPromises.access.mockResolvedValue(); // both source and target exist

await cacheDependencies('pip', '3.12');

expect(mockedFsPromises.copyFile).not.toHaveBeenCalled();
expect(mockedCore.info).toHaveBeenCalledWith(
`Resolved cache-dependency-path: nested/deps.lock`
expect.stringContaining('Skipped copying')
);
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('logs warning if source file does not exist', async () => {
mockedFsPromises.access.mockImplementation(async filePath => {
if (filePath === '/github/action/nested/deps.lock') {
throw new Error('source not found');
}
return Promise.resolve(); // fallback for others
});

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
it('logs warning if copyFile fails', async () => {
mockedFsPromises.access.mockImplementation(async filePath => {
if (filePath === '/github/action/nested/deps.lock')
return Promise.resolve();
throw new Error('target does not exist');
});

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 () => {
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');
process.env.GITHUB_ACTION_PATH = '/github/workspace';
process.env.GITHUB_WORKSPACE = '/github/workspace';

// 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

const sourcePath = '/github/workspace/deps.lock';
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();
});
});
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 action’s 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."
Expand Down
19 changes: 15 additions & 4 deletions dist/setup/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -96883,8 +96883,10 @@ function isGraalPyVersion(versionSpec) {
}
function cacheDependencies(cache, pythonVersion) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const cacheDependencyPath = core.getInput('cache-dependency-path') || undefined;
let resolvedDependencyPath = undefined;
const overwrite = (_a = core.getBooleanInput('overwrite', { required: false })) !== null && _a !== void 0 ? _a : false;
if (cacheDependencyPath) {
const actionPath = process.env.GITHUB_ACTION_PATH || '';
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
Expand All @@ -96902,11 +96904,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}`);
Expand Down
4 changes: 3 additions & 1 deletion docs/advanced-usage.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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 action’s 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
Expand Down Expand Up @@ -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/).
28 changes: 22 additions & 6 deletions src/setup-python.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ 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}) ?? false;

if (cacheDependencyPath) {
const actionPath = process.env.GITHUB_ACTION_PATH || '';
Expand All @@ -48,11 +49,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}`
Expand Down Expand Up @@ -81,7 +98,6 @@ export async function cacheDependencies(cache: string, pythonVersion: string) {
);
await cacheDistributor.restoreCache();
}

function resolveVersionInputFromDefaultFile(): string[] {
const couples: [string, (versionFile: string) => string[]][] = [
['.python-version', getVersionsInputFromPlainFile]
Expand Down
Loading
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