Skip to content

Commit a67b91e

Browse files
authored
Merge pull request actions#448 from actions/users/aiyan/cache-package
Initial commit to create @actions/cache package
2 parents 2fdf3b7 + d2b2399 commit a67b91e

26 files changed

+2025
-4
lines changed

.github/workflows/artifact-tests.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,8 @@ jobs:
7272
- name: Verify downloadArtifact()
7373
shell: bash
7474
run: |
75-
scripts/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
76-
scripts/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
75+
packages/artifact/__tests__/test-artifact-file.sh "artifact-1-directory/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
76+
packages/artifact/__tests__/test-artifact-file.sh "artifact-2-directory/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
7777
7878
- name: Download artifacts using downloadAllArtifacts()
7979
run: |
@@ -83,5 +83,5 @@ jobs:
8383
- name: Verify downloadAllArtifacts()
8484
shell: bash
8585
run: |
86-
scripts/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
87-
scripts/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"
86+
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-1/artifact-path/world.txt" "${{ env.non-gzip-artifact-content }}"
87+
packages/artifact/__tests__/test-artifact-file.sh "multi-artifact-directory/my-artifact-2/artifact-path/gzip.txt" "${{ env.gzip-artifact-content }}"

.github/workflows/cache-tests.yml

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: cache-unit-tests
2+
on:
3+
push:
4+
branches:
5+
- master
6+
paths-ignore:
7+
- '**.md'
8+
pull_request:
9+
paths-ignore:
10+
- '**.md'
11+
12+
jobs:
13+
build:
14+
name: Build
15+
16+
strategy:
17+
matrix:
18+
runs-on: [ubuntu-latest, windows-latest, macOS-latest]
19+
fail-fast: false
20+
21+
runs-on: ${{ matrix.runs-on }}
22+
23+
steps:
24+
- name: Checkout
25+
uses: actions/checkout@v2
26+
27+
- name: Set Node.js 12.x
28+
uses: actions/setup-node@v1
29+
with:
30+
node-version: 12.x
31+
32+
# In order to save & restore cache from a shell script, certain env variables need to be set that are only available in the
33+
# node context. This runs a local action that gets and sets the necessary env variables that are needed
34+
- name: Set env variables
35+
uses: ./packages/cache/__tests__/__fixtures__/
36+
37+
# Need root node_modules because certain npm packages like jest are configured for the entire repository and it won't be possible
38+
# without these to just compile the cache package
39+
- name: Install root npm packages
40+
run: npm ci
41+
42+
- name: Compile cache package
43+
run: |
44+
npm ci
45+
npm run tsc
46+
working-directory: packages/cache
47+
48+
- name: Generate files in working directory
49+
shell: bash
50+
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} test-cache
51+
52+
- name: Generate files outside working directory
53+
shell: bash
54+
run: packages/cache/__tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache
55+
56+
# We're using node -e to call the functions directly available in the @actions/cache package
57+
- name: Save cache using saveCache()
58+
run: |
59+
node -e "Promise.resolve(require('./packages/cache/lib/cache').saveCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
60+
61+
- name: Restore cache using restoreCache()
62+
run: |
63+
node -e "Promise.resolve(require('./packages/cache/lib/cache').restoreCache(['test-cache','~/test-cache'],'test-${{ runner.os }}-${{ github.run_id }}'))"
64+
65+
- name: Verify cache
66+
shell: bash
67+
run: |
68+
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} test-cache
69+
packages/cache/__tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ $ npm install @actions/io
5959

6060
Provides functions for downloading and caching tools. e.g. setup-* actions. Read more [here](packages/tool-cache)
6161

62+
See @actions/cache for caching workflow dependencies.
63+
6264
```bash
6365
$ npm install @actions/tool-cache
6466
```
@@ -82,6 +84,15 @@ $ npm install @actions/artifact
8284
```
8385
<br/>
8486

87+
:dart: [@actions/cache](packages/cache)
88+
89+
Provides functions to cache dependencies and build outputs to improve workflow execution time. Read more [here](packages/cache)
90+
91+
```bash
92+
$ npm install @actions/cache
93+
```
94+
<br/>
95+
8596
## Creating an Action with the Toolkit
8697

8798
:question: [Choosing an action type](docs/action-types.md)

packages/cache/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# `@actions/cache`
2+
3+
> Functions necessary for caching dependencies and build outputs to improve workflow execution time.
4+
5+
See ["Caching dependencies to speed up workflows"](https://help.github.com/github/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows) for how caching works.
6+
7+
Note that GitHub will remove any cache entries that have not been accessed in over 7 days. There is no limit on the number of caches you can store, but the total size of all caches in a repository is limited to 5 GB. If you exceed this limit, GitHub will save your cache but will begin evicting caches until the total size is less than 5 GB.
8+
9+
## Usage
10+
11+
#### Restore Cache
12+
13+
Restores a cache based on `key` and `restoreKeys` to the `paths` provided. Function returns the cache key for cache hit and returns undefined if cache not found.
14+
15+
```js
16+
const cache = require('@actions/cache');
17+
const paths = [
18+
'node_modules',
19+
'packages/*/node_modules/'
20+
]
21+
const key = 'npm-foobar-d5ea0750'
22+
const restoreKeys = [
23+
'npm-foobar-',
24+
'npm-'
25+
]
26+
const cacheKey = await cache.restoreCache(paths, key, restoreKeys)
27+
```
28+
29+
#### Save Cache
30+
31+
Saves a cache containing the files in `paths` using the `key` provided. The files would be compressed using zstandard compression algorithm if zstd is installed, otherwise gzip is used. Function returns the cache id if the cache was saved succesfully and throws an error if cache upload fails.
32+
33+
```js
34+
const cache = require('@actions/cache');
35+
const paths = [
36+
'node_modules',
37+
'packages/*/node_modules/'
38+
]
39+
const key = 'npm-foobar-d5ea0750'
40+
const cacheId = await cache.saveCache(paths, key)
41+
```

packages/cache/RELEASES.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# @actions/cache Releases
2+
3+
### 0.1.0
4+
5+
- Initial release
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
name: 'Set env variables'
2+
description: 'Sets certain env variables so that e2e restore and save cache can be tested in a shell'
3+
runs:
4+
using: 'node12'
5+
main: 'index.js'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
hello world
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
// Certain env variables are not set by default in a shell context and are only available in a node context from a running action
2+
// In order to be able to restore and save cache e2e in a shell when running CI tests, we need these env variables set
3+
console.log(`::set-env name=ACTIONS_RUNTIME_URL::${process.env.ACTIONS_RUNTIME_URL}`)
4+
console.log(`::set-env name=ACTIONS_RUNTIME_TOKEN::${process.env.ACTIONS_RUNTIME_TOKEN}`)
5+
console.log(`::set-env name=GITHUB_RUN_ID::${process.env.GITHUB_RUN_ID}`)
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
import {getCacheVersion, retry} from '../src/internal/cacheHttpClient'
2+
import {CompressionMethod} from '../src/internal/constants'
3+
4+
test('getCacheVersion with one path returns version', async () => {
5+
const paths = ['node_modules']
6+
const result = getCacheVersion(paths)
7+
expect(result).toEqual(
8+
'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
9+
)
10+
})
11+
12+
test('getCacheVersion with multiple paths returns version', async () => {
13+
const paths = ['node_modules', 'dist']
14+
const result = getCacheVersion(paths)
15+
expect(result).toEqual(
16+
'165c3053bc646bf0d4fac17b1f5731caca6fe38e0e464715c0c3c6b6318bf436'
17+
)
18+
})
19+
20+
test('getCacheVersion with zstd compression returns version', async () => {
21+
const paths = ['node_modules']
22+
const result = getCacheVersion(paths, CompressionMethod.Zstd)
23+
24+
expect(result).toEqual(
25+
'273877e14fd65d270b87a198edbfa2db5a43de567c9a548d2a2505b408befe24'
26+
)
27+
})
28+
29+
test('getCacheVersion with gzip compression does not change vesion', async () => {
30+
const paths = ['node_modules']
31+
const result = getCacheVersion(paths, CompressionMethod.Gzip)
32+
33+
expect(result).toEqual(
34+
'b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985'
35+
)
36+
})
37+
38+
interface TestResponse {
39+
statusCode: number
40+
result: string | null
41+
}
42+
43+
async function handleResponse(
44+
response: TestResponse | undefined
45+
): Promise<TestResponse> {
46+
if (!response) {
47+
// eslint-disable-next-line no-undef
48+
fail('Retry method called too many times')
49+
}
50+
51+
if (response.statusCode === 999) {
52+
throw Error('Test Error')
53+
} else {
54+
return Promise.resolve(response)
55+
}
56+
}
57+
58+
async function testRetryExpectingResult(
59+
responses: TestResponse[],
60+
expectedResult: string | null
61+
): Promise<void> {
62+
responses = responses.reverse() // Reverse responses since we pop from end
63+
64+
const actualResult = await retry(
65+
'test',
66+
async () => handleResponse(responses.pop()),
67+
(response: TestResponse) => response.statusCode
68+
)
69+
70+
expect(actualResult.result).toEqual(expectedResult)
71+
}
72+
73+
async function testRetryExpectingError(
74+
responses: TestResponse[]
75+
): Promise<void> {
76+
responses = responses.reverse() // Reverse responses since we pop from end
77+
78+
expect(
79+
retry(
80+
'test',
81+
async () => handleResponse(responses.pop()),
82+
(response: TestResponse) => response.statusCode
83+
)
84+
).rejects.toBeInstanceOf(Error)
85+
}
86+
87+
test('retry works on successful response', async () => {
88+
await testRetryExpectingResult(
89+
[
90+
{
91+
statusCode: 200,
92+
result: 'Ok'
93+
}
94+
],
95+
'Ok'
96+
)
97+
})
98+
99+
test('retry works after retryable status code', async () => {
100+
await testRetryExpectingResult(
101+
[
102+
{
103+
statusCode: 503,
104+
result: null
105+
},
106+
{
107+
statusCode: 200,
108+
result: 'Ok'
109+
}
110+
],
111+
'Ok'
112+
)
113+
})
114+
115+
test('retry fails after exhausting retries', async () => {
116+
await testRetryExpectingError([
117+
{
118+
statusCode: 503,
119+
result: null
120+
},
121+
{
122+
statusCode: 503,
123+
result: null
124+
},
125+
{
126+
statusCode: 200,
127+
result: 'Ok'
128+
}
129+
])
130+
})
131+
132+
test('retry fails after non-retryable status code', async () => {
133+
await testRetryExpectingError([
134+
{
135+
statusCode: 500,
136+
result: null
137+
},
138+
{
139+
statusCode: 200,
140+
result: 'Ok'
141+
}
142+
])
143+
})
144+
145+
test('retry works after error', async () => {
146+
await testRetryExpectingResult(
147+
[
148+
{
149+
statusCode: 999,
150+
result: null
151+
},
152+
{
153+
statusCode: 200,
154+
result: 'Ok'
155+
}
156+
],
157+
'Ok'
158+
)
159+
})
160+
161+
test('retry returns after client error', async () => {
162+
await testRetryExpectingResult(
163+
[
164+
{
165+
statusCode: 400,
166+
result: null
167+
},
168+
{
169+
statusCode: 200,
170+
result: 'Ok'
171+
}
172+
],
173+
null
174+
)
175+
})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import {promises as fs} from 'fs'
2+
import * as path from 'path'
3+
import * as cacheUtils from '../src/internal/cacheUtils'
4+
5+
test('getArchiveFileSizeIsBytes returns file size', () => {
6+
const filePath = path.join(__dirname, '__fixtures__', 'helloWorld.txt')
7+
8+
const size = cacheUtils.getArchiveFileSizeIsBytes(filePath)
9+
10+
expect(size).toBe(11)
11+
})
12+
13+
test('unlinkFile unlinks file', async () => {
14+
const testDirectory = await fs.mkdtemp('unlinkFileTest')
15+
const testFile = path.join(testDirectory, 'test.txt')
16+
await fs.writeFile(testFile, 'hello world')
17+
18+
await expect(fs.stat(testFile)).resolves.not.toThrow()
19+
20+
await cacheUtils.unlinkFile(testFile)
21+
22+
// This should throw as testFile should not exist
23+
await expect(fs.stat(testFile)).rejects.toThrow()
24+
25+
await fs.rmdir(testDirectory)
26+
})

0 commit comments

Comments
 (0)