Skip to content

Commit d844df8

Browse files
committed
feat: add built-in caching via inputs
1 parent 1c4873e commit d844df8

File tree

7 files changed

+164
-0
lines changed

7 files changed

+164
-0
lines changed

.github/workflows/test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,41 @@ jobs:
7979
- name: Check binary exists
8080
run: deno_foo -V
8181

82+
test-setup-cache:
83+
runs-on: ${{ matrix.os }}
84+
strategy:
85+
matrix:
86+
os: [ubuntu-latest, windows-latest, macos-latest]
87+
steps:
88+
- uses: actions/checkout@v4
89+
90+
- name: Setup Deno
91+
uses: ./
92+
with:
93+
cache: true
94+
cache-hash: bar-${{ hashFiles('**/package-lock.json') }}
95+
96+
- name: Download dependencies for cache
97+
run: deno install --global npm:[email protected]
98+
99+
test-cache:
100+
needs: test-setup-cache
101+
runs-on: ${{ matrix.os }}
102+
strategy:
103+
matrix:
104+
os: [ubuntu-latest, windows-latest, macos-latest]
105+
steps:
106+
- uses: actions/checkout@v4
107+
108+
- name: Setup Deno
109+
uses: ./
110+
with:
111+
cache: true
112+
cache-hash: ${{ hashFiles('**/package-lock.json') }}
113+
114+
- name: Run with cached dependencies
115+
run: deno run --cached-only -RE npm:[email protected] "It works!"
116+
82117
lint:
83118
runs-on: ubuntu-latest
84119
steps:

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,23 @@ number.
133133
134134
- run: echo "Deno version is ${{ steps.deno.outputs.deno-version }}"
135135
```
136+
137+
### Caching dependencies downloaded by Deno automatically
138+
139+
Dependencies installed by Deno can be cached automatically, which is similar to
140+
the [`cache` option in `setup-node`](https://github.com/actions/setup-node).
141+
142+
To enable the cache, use `cache: true`. It's recommended to also add the
143+
`cache-hash` property, to scope caches based on lockfile changes.
144+
145+
```yaml
146+
- uses: denoland/setup-deno@v2
147+
with:
148+
cache: true
149+
cache-hash: ${{ hashFiles('**/deno.lock') }}
150+
```
151+
152+
> [!WARNING]
153+
> If an environment variable `DENO_DIR` is set for steps that run/download
154+
> dependencies, then `DENO_DIR` must also be set for the `denoland/setup-deno`
155+
> Action, for the caching to work as intended.

action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,20 @@ inputs:
1313
deno-binary-name:
1414
description: The name to use for the binary.
1515
default: "deno"
16+
cache:
17+
description: Cache downloaded modules & packages automatically in GitHub Actions cache.
18+
default: "false"
19+
cache-hash:
20+
description: A hash used as part of the cache key. Use e.g. `$\{{ hashFiles('**/deno.lock') }}` to cache based on the lockfile contents.
1621
outputs:
22+
cache-hit:
23+
description: A boolean indicating whether the cache was hit.
1724
deno-version:
1825
description: "The Deno version that was installed."
1926
release-channel:
2027
description: "The release channel of the installed version."
2128
runs:
2229
using: "node20"
2330
main: "main.mjs"
31+
post: "save-cache.mjs"
32+
post-if: always()

main.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ async function main() {
4242
core.setOutput("release-channel", version.kind);
4343

4444
core.info("Installation complete.");
45+
46+
if (core.getInput("cache") === "true") {
47+
const { restoreCache } = await import("./src/cache.mjs");
48+
await restoreCache(core.getInput("cache-hash"));
49+
}
4550
} catch (err) {
4651
core.setFailed((err instanceof Error) ? err : String(err));
4752
process.exit();

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"license": "MIT",
77
"type": "module",
88
"dependencies": {
9+
"@actions/cache": "^3.3.0",
910
"@actions/core": "^1.10.1",
1011
"@actions/tool-cache": "^2.0.1",
1112
"semver": "^7.6.3",

save-cache.mjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import process from "node:process";
2+
import core from "@actions/core";
3+
import { saveCache } from "./src/cache.mjs";
4+
5+
async function main() {
6+
try {
7+
await saveCache();
8+
} catch (err) {
9+
core.setFailed((err instanceof Error) ? err : String(err));
10+
process.exit();
11+
}
12+
}
13+
14+
main();

src/cache.mjs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import process from "node:process";
2+
import cache from "@actions/cache";
3+
import core from "@actions/core";
4+
5+
const state = {
6+
DENO_DIR: "DENO_DIR",
7+
CACHE_HIT: "CACHE_HIT",
8+
CACHE_SAVE: "CACHE_SAVE",
9+
};
10+
11+
export async function saveCache() {
12+
if (!cache.isFeatureAvailable()) {
13+
core.warning("Caching is not available. Caching is skipped.");
14+
return;
15+
}
16+
17+
const denoDir = core.getState(state.DENO_DIR);
18+
const saveKey = core.getState(state.CACHE_SAVE);
19+
if (!denoDir || !saveKey) {
20+
core.info("Caching is not enabled. Caching is skipped.");
21+
return;
22+
} else if (core.getState(state.CACHE_HIT) === "true") {
23+
core.info(
24+
`Cache hit occurred on the primary key "${saveKey}", not saving cache.`,
25+
);
26+
return;
27+
}
28+
29+
await cache.saveCache([denoDir], saveKey);
30+
core.info(`Cache saved with key: "${saveKey}".`);
31+
}
32+
33+
/**
34+
* @param {string} cacheHash Should be a hash of any lockfiles or similar.
35+
*/
36+
export async function restoreCache(cacheHash) {
37+
try {
38+
const denoDir = await resolveDenoDir();
39+
core.saveState(state.DENO_DIR, denoDir);
40+
41+
const { GITHUB_JOB, RUNNER_OS, RUNNER_ARCH } = process.env;
42+
const restoreKey = `deno-cache-${RUNNER_OS}-${RUNNER_ARCH}`;
43+
// CI jobs often download different dependencies, so include Job ID in the cache key.
44+
const primaryKey = `${restoreKey}-${GITHUB_JOB}-${cacheHash}`;
45+
core.saveState(state.CACHE_SAVE, primaryKey);
46+
47+
const loadedCacheKey = await cache.restoreCache([denoDir], primaryKey, [
48+
restoreKey,
49+
]);
50+
const cacheHit = primaryKey === loadedCacheKey;
51+
core.setOutput("cache-hit", cacheHit);
52+
core.saveState(state.CACHE_HIT, cacheHit);
53+
54+
const message = loadedCacheKey
55+
? `Cache key used: "${loadedCacheKey}".`
56+
: `No cache found for restore key: "${restoreKey}".`;
57+
core.info(message);
58+
} catch (err) {
59+
core.warning(
60+
new Error("Failed to restore cache. Continuing without cache.", {
61+
cause: err,
62+
}),
63+
);
64+
}
65+
}
66+
67+
/**
68+
* @returns {Promise<string>}
69+
*/
70+
async function resolveDenoDir() {
71+
const { DENO_DIR } = process.env;
72+
if (DENO_DIR) return DENO_DIR;
73+
74+
// Retrieve the DENO_DIR from `deno info --json`
75+
const { exec } = await import("node:child_process");
76+
const output = await new Promise((res, rej) => {
77+
exec("deno info --json", (err, stdout) => err ? rej(err) : res(stdout));
78+
});
79+
return JSON.parse(output).denoDir;
80+
}

0 commit comments

Comments
 (0)