Skip to content

Commit

Permalink
feat: add built-in caching via inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
csvn committed Nov 26, 2024
1 parent 1c4873e commit b402db6
Show file tree
Hide file tree
Showing 7 changed files with 164 additions and 0 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,41 @@ jobs:
- name: Check binary exists
run: deno_foo -V

test-setup-cache:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4

- name: Setup Deno
uses: ./
with:
cache: true
cache-hash: ${{ hashFiles('**/package-lock.json') }}

- name: Download dependencies for cache
run: deno install --global npm:[email protected]

test-cache:
needs: test-setup-cache
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4

- name: Setup Deno
uses: ./
with:
cache: true
cache-hash: ${{ hashFiles('**/package-lock.json') }}

- name: Run with cached dependencies
run: deno run --cached-only -RE npm:[email protected] "It works!"

lint:
runs-on: ubuntu-latest
steps:
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,23 @@ number.
- run: echo "Deno version is ${{ steps.deno.outputs.deno-version }}"
```

### Caching dependencies downloaded by Deno automatically

Dependencies installed by Deno can be cached automatically, which is similar to
the [`cache` option in `setup-node`](https://github.com/actions/setup-node).

To enable the cache, use `cache: true`. It's recommended to also add the
`cache-hash` property, to scope caches based on lockfile changes.

```yaml
- uses: denoland/setup-deno@v2
with:
cache: true
cache-hash: ${{ hashFiles('**/deno.lock') }}
```

> [!WARNING]
> If an environment variable `DENO_DIR` is set for steps that run/download
> dependencies, then `DENO_DIR` must also be set for the `denoland/setup-deno`
> Action, for the caching to work as intended.
9 changes: 9 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,20 @@ inputs:
deno-binary-name:
description: The name to use for the binary.
default: "deno"
cache:
description: Cache downloaded modules & packages automatically in GitHub Actions cache.
default: "false"
cache-hash:
description: A hash used as part of the cache key. Use e.g. `$\{{ hashFiles('**/deno.lock') }}` to cache based on the lockfile contents.
outputs:
cache-hit:
description: A boolean indicating whether the cache was hit.
deno-version:
description: "The Deno version that was installed."
release-channel:
description: "The release channel of the installed version."
runs:
using: "node20"
main: "main.mjs"
post: "save-cache.mjs"
post-if: always()
5 changes: 5 additions & 0 deletions main.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ async function main() {
core.setOutput("release-channel", version.kind);

core.info("Installation complete.");

if (core.getInput("cache") === "true") {
const { restoreCache } = await import("./src/cache.mjs");
await restoreCache(core.getInput("cache-hash"));
}
} catch (err) {
core.setFailed((err instanceof Error) ? err : String(err));
process.exit();
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"license": "MIT",
"type": "module",
"dependencies": {
"@actions/cache": "^3.3.0",
"@actions/core": "^1.10.1",
"@actions/tool-cache": "^2.0.1",
"semver": "^7.6.3",
Expand Down
14 changes: 14 additions & 0 deletions save-cache.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import process from "node:process";
import core from "@actions/core";
import { saveCache } from "./src/cache.mjs";

async function main() {
try {
await saveCache();
} catch (err) {
core.setFailed((err instanceof Error) ? err : String(err));
process.exit();
}
}

main();
80 changes: 80 additions & 0 deletions src/cache.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import process from "node:process";
import cache from "@actions/cache";
import core from "@actions/core";

const state = {
DENO_DIR: "DENO_DIR",
CACHE_HIT: "CACHE_HIT",
CACHE_SAVE: "CACHE_SAVE",
};

export async function saveCache() {
if (!cache.isFeatureAvailable()) {
core.warning("Caching is not available. Caching is skipped.");
return;
}

const denoDir = core.getState(state.DENO_DIR);
const saveKey = core.getState(state.CACHE_SAVE);
if (!denoDir || !saveKey) {
core.info("Caching is not enabled. Caching is skipped.");
return;
} else if (core.getState(state.CACHE_HIT) === "true") {
core.info(
`Cache hit occurred on the primary key "${saveKey}", not saving cache.`,
);
return;
}

await cache.saveCache([denoDir], saveKey);
core.info(`Cache saved with key: "${saveKey}".`);
}

/**
* @param {string} cacheHash Should be a hash of any lockfiles or similar.
*/
export async function restoreCache(cacheHash) {
try {
const denoDir = await resolveDenoDir();
core.saveState(state.DENO_DIR, denoDir);

const { GITHUB_JOB, RUNNER_OS, RUNNER_ARCH } = process.env;
const restoreKey = `deno-cache-${RUNNER_OS}-${RUNNER_ARCH}`;
// CI jobs often download different dependencies, so include Job ID in the cache key.
const primaryKey = `${restoreKey}-${GITHUB_JOB}-${cacheHash}`;
core.saveState(state.CACHE_SAVE, primaryKey);

const loadedCacheKey = await cache.restoreCache([denoDir], primaryKey, [
restoreKey,
]);
const cacheHit = primaryKey === loadedCacheKey;
core.setOutput("cache-hit", cacheHit);
core.saveState(state.CACHE_HIT, cacheHit);

const message = loadedCacheKey
? `Cache key used: "${loadedCacheKey}".`
: `No cache found for restore key: "${restoreKey}".`;
core.info(message);
} catch (err) {
core.warning(
new Error("Failed to restore cache. Continuing without cache.", {
cause: err,
}),
);
}
}

/**
* @returns {Promise<string>}
*/
async function resolveDenoDir() {
const { DENO_DIR } = process.env;
if (DENO_DIR) return DENO_DIR;

// Retrieve the DENO_DIR from `deno info --json`
const { exec } = await import("node:child_process");
const output = await new Promise((res, rej) => {
exec("deno info --json", (err, stdout) => err ? rej(err) : res(stdout));
});
return JSON.parse(output).denoDir;
}

0 comments on commit b402db6

Please sign in to comment.