Skip to content
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

Add cache-save: false option #762

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
64 changes: 35 additions & 29 deletions __tests__/cache-save.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ describe('run', () => {
let debugSpy: jest.SpyInstance;
let saveStateSpy: jest.SpyInstance;
let getStateSpy: jest.SpyInstance;
let getInputSpy: jest.SpyInstance;
let setFailedSpy: jest.SpyInstance;

// cache spy
Expand All @@ -29,10 +28,17 @@ describe('run', () => {
// exec spy
let getExecOutputSpy: jest.SpyInstance;

let inputs = {} as any;
function setInput(name: string, value: string): void {
process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] = value;
}

beforeEach(() => {
process.env['RUNNER_OS'] = process.env['RUNNER_OS'] ?? 'linux';
for (const key in process.env) {
if (key.startsWith('INPUT_')) {
delete process.env[key];
}
}

infoSpy = jest.spyOn(core, 'info');
infoSpy.mockImplementation(input => undefined);
Expand All @@ -56,9 +62,6 @@ describe('run', () => {

setFailedSpy = jest.spyOn(core, 'setFailed');

getInputSpy = jest.spyOn(core, 'getInput');
getInputSpy.mockImplementation(input => inputs[input]);

getExecOutputSpy = jest.spyOn(exec, 'getExecOutput');
getExecOutputSpy.mockImplementation((input: string) => {
if (input.includes('pip')) {
Expand All @@ -74,10 +77,9 @@ describe('run', () => {

describe('Package manager validation', () => {
it('Package manager is not provided, skip caching', async () => {
inputs['cache'] = '';
setInput('cache', '');
await run();

expect(getInputSpy).toHaveBeenCalled();
expect(infoSpy).not.toHaveBeenCalled();
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
Expand All @@ -86,12 +88,11 @@ describe('run', () => {

describe('Validate unchanged cache is not saved', () => {
it('should not save cache for pip', async () => {
inputs['cache'] = 'pip';
inputs['python-version'] = '3.10.0';
setInput('cache', 'pip');
setInput('python-version', '3.10.0');

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(debugSpy).toHaveBeenCalledWith(
`paths for caching are ${__dirname}`
);
Expand All @@ -103,12 +104,11 @@ describe('run', () => {
});

it('should not save cache for pipenv', async () => {
inputs['cache'] = 'pipenv';
inputs['python-version'] = '3.10.0';
setInput('cache', 'pipenv');
setInput('python-version', '3.10.0');

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(debugSpy).toHaveBeenCalledWith(
`paths for caching are ${__dirname}`
);
Expand All @@ -122,8 +122,8 @@ describe('run', () => {

describe('action saves the cache', () => {
it('saves cache from pip', async () => {
inputs['cache'] = 'pip';
inputs['python-version'] = '3.10.0';
setInput('cache', 'pip');
setInput('python-version', '3.10.0');
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return requirementsHash;
Expand All @@ -136,7 +136,6 @@ describe('run', () => {

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${requirementsHash}, not saving cache.`
Expand All @@ -149,8 +148,8 @@ describe('run', () => {
});

it('saves cache from pipenv', async () => {
inputs['cache'] = 'pipenv';
inputs['python-version'] = '3.10.0';
setInput('cache', 'pipenv');
setInput('python-version', '3.10.0');
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return pipFileLockHash;
Expand All @@ -163,7 +162,6 @@ describe('run', () => {

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${pipFileLockHash}, not saving cache.`
Expand All @@ -176,8 +174,8 @@ describe('run', () => {
});

it('saves cache from poetry', async () => {
inputs['cache'] = 'poetry';
inputs['python-version'] = '3.10.0';
setInput('cache', 'poetry');
setInput('python-version', '3.10.0');
getStateSpy.mockImplementation((name: string) => {
if (name === State.CACHE_MATCHED_KEY) {
return poetryLockHash;
Expand All @@ -190,7 +188,6 @@ describe('run', () => {

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(infoSpy).not.toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${poetryLockHash}, not saving cache.`
Expand All @@ -203,8 +200,8 @@ describe('run', () => {
});

it('saves with -1 cacheId , should not fail workflow', async () => {
inputs['cache'] = 'poetry';
inputs['python-version'] = '3.10.0';
setInput('cache', 'poetry');
setInput('python-version', '3.10.0');
getStateSpy.mockImplementation((name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
Expand All @@ -221,7 +218,6 @@ describe('run', () => {

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(infoSpy).not.toHaveBeenCalled();
expect(saveCacheSpy).toHaveBeenCalled();
Expand All @@ -232,8 +228,8 @@ describe('run', () => {
});

it('saves with error from toolkit, should not fail the workflow', async () => {
inputs['cache'] = 'npm';
inputs['python-version'] = '3.10.0';
setInput('cache', 'npm');
setInput('python-version', '3.10.0');
getStateSpy.mockImplementation((name: string) => {
if (name === State.STATE_CACHE_PRIMARY_KEY) {
return poetryLockHash;
Expand All @@ -250,17 +246,27 @@ describe('run', () => {

await run();

expect(getInputSpy).toHaveBeenCalled();
expect(getStateSpy).toHaveBeenCalledTimes(3);
expect(infoSpy).not.toHaveBeenCalledWith();
expect(saveCacheSpy).toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});

it('should not save the cache when requested not to', async () => {
setInput('cache', 'pip');
setInput('cache-save', 'false');
setInput('python-version', '3.10.0');
await run();
expect(infoSpy).toHaveBeenCalledWith(
'Not saving cache since `cache-save` is false'
);
expect(saveCacheSpy).not.toHaveBeenCalled();
expect(setFailedSpy).not.toHaveBeenCalled();
});
});

afterEach(() => {
jest.resetAllMocks();
jest.clearAllMocks();
inputs = {};
});
});
3 changes: 3 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ inputs:
default: ${{ github.server_url == 'https://github.com' && github.token || '' }}
cache-dependency-path:
description: "Used to specify the path to dependency files. Supports wildcards or a list of file names for caching multiple dependencies."
cache-save:
description: "Set this option if you want the action to save the cache after the run. Defaults to true. It can be useful to set this to false if you have e.g. optional dependencies that only some workflows require, and they should not be cached."
default: true
update-environment:
description: "Set this option if you want the action to update environment variables."
default: true
Expand Down
20 changes: 17 additions & 3 deletions dist/cache-save/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -80475,9 +80475,23 @@ function run(earlyExit) {
try {
const cache = core.getInput('cache');
if (cache) {
yield saveCache(cache);
if (earlyExit) {
process.exit(0);
let shouldSave = true;
try {
shouldSave = core.getBooleanInput('cache-save', { required: false });
}
catch (e) {
// If we fail to parse the input, assume it's
// > "Input does not meet YAML 1.2 "core schema" specification."
// and assume it's the `true` default.
}
if (shouldSave) {
yield saveCache(cache);
if (earlyExit) {
process.exit(0);
}
}
else {
core.info('Not saving cache since `cache-save` is false');
}
}
}
Expand Down
54 changes: 48 additions & 6 deletions docs/advanced-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,8 @@ steps:

## Caching packages

**Caching pipenv dependencies:**
### Caching pipenv dependencies

```yaml
steps:
- uses: actions/checkout@v4
Expand All @@ -306,7 +307,8 @@ steps:
- run: pipenv install
```

**Caching poetry dependencies:**
### Caching poetry dependencies

```yaml
steps:
- uses: actions/checkout@v4
Expand All @@ -320,7 +322,8 @@ steps:
- run: poetry run pytest
```

**Using a list of file paths to cache dependencies**
### Using a list of file paths to cache dependencies

```yaml
steps:
- uses: actions/checkout@v4
Expand All @@ -335,7 +338,9 @@ steps:
run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python
- run: pipenv install
```
**Using wildcard patterns to cache dependencies**

### Using wildcard patterns to cache dependencies

```yaml
steps:
- uses: actions/checkout@v4
Expand All @@ -347,7 +352,8 @@ steps:
- run: pip install -r subdirectory/requirements-dev.txt
```

**Using a list of wildcard patterns to cache dependencies**
### Using a list of wildcard patterns to cache dependencies

```yaml
steps:
- uses: actions/checkout@v4
Expand All @@ -361,7 +367,7 @@ steps:
- run: pip install -e . -r subdirectory/requirements-dev.txt
```

**Caching projects that use setup.py:**
### Caching projects that use setup.py (or pyproject.toml)

```yaml
steps:
Expand All @@ -375,6 +381,42 @@ steps:
# Or pip install -e '.[test]' to install test dependencies
```

### Skipping cache saving

For some scenarios, it may be useful to only save a given subset of dependencies,
but restore more of them for other workflows. For instance, there may be a heavy
`extras` dependency that you do not need your entire test matrix to download, but
you want to download and test it separately without it being saved in the cache
archive for all runs.

To achieve this, you can use `cache-save: false` on the run that uses the heavy
dependency.


```yaml
test:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
cache-dependency-path: pyproject.toml
- run: pip install -e .

test-heavy-extra:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
cache-dependency-path: pyproject.toml
cache-save: false
- run: pip install -e '.[heavy-extra]'
```


# Outputs and environment variables

## Outputs
Expand Down
19 changes: 15 additions & 4 deletions src/cache-save.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ export async function run(earlyExit?: boolean) {
try {
const cache = core.getInput('cache');
if (cache) {
await saveCache(cache);

if (earlyExit) {
process.exit(0);
let shouldSave = true;
try {
shouldSave = core.getBooleanInput('cache-save', {required: false});
} catch (e) {
// If we fail to parse the input, assume it's
// > "Input does not meet YAML 1.2 "core schema" specification."
// and assume it's the `true` default.
}
if (shouldSave) {
await saveCache(cache);
if (earlyExit) {
process.exit(0);
}
} else {
core.info('Not saving cache since `cache-save` is false');
}
}
} catch (error) {
Expand Down
Loading