Problem Statement
On a large monorepo, denoland/setup-deno@v2 with cache: true logs Installation complete. and then goes completely silent for minutes until the job is killed. Nothing indicates what it is doing. The cause is the default cache key: when no cache-hash is supplied the action globs **/deno.lock across the whole workspace with symlinks followed, so on a repo that has already run pnpm install it walks hundreds of thousands of paths to find one lockfile. Passing an explicit cache-hash makes the same step finish in under two seconds, so the bypass exists — the default just should not be pathological.
This matters more than usual right now because #116 proposes enabling cache by default.
Goal
With cache: true and no cache-hash, cache-key resolution completes in under 10 seconds on a workspace whose symlink-followed reachable file count exceeds 500,000, while still deriving the key from every deno.lock in the workspace.
Evidence: the glob
export async function restoreCache(cacheHash: string) {
try {
const denoDir = await resolveDenoDir();
core.saveState(state.DENO_DIR, denoDir);
if (cacheHash.length === 0) {
cacheHash = await resolveDefaultCacheKey();
}
function resolveDefaultCacheKey(): Promise<string> {
return hashFiles(
"**/deno.lock",
process.env.GITHUB_WORKSPACE,
);
}
Source: src/cache.ts:34-41 and src/cache.ts:69-74 at commit 22d081ff2d3a40755e97629de92e3bcbfa7cf2ed (v2.0.5). Call site: src/main.ts:65-67.
hashFiles from @actions/glob defaults to followSymbolicLinks: true, and a ** pattern must exhaust the tree — finding the single match early does not stop the walk. Every log line inside restoreCache is emitted after the key is computed, which is why the step produces no output at all while it stalls.
Evidence: reproduction
Reproduced locally with act against a real monorepo checkout, container bind-mounted to the workspace:
[deno-cache-probe/default-glob] ⭐ Run Main denoland/setup-deno@v2
[deno-cache-probe/default-glob] | Going to install stable version 2.9.5.
[deno-cache-probe/default-glob] | Using cached Deno installation from /opt/hostedtoolcache/deno/2.9.5/x64.
[deno-cache-probe/default-glob] | Installation complete.
<no further output; killed at 2400s>
The same workspace, same container, same action, with cache-hash: ${{ hashFiles('deno.lock') }} added:
[deno-cache-probe/explicit-hash] | Installation complete.
[deno-cache-probe/explicit-hash] | No cache found for restore key: "deno-cache-Linux-X64".
[deno-cache-probe/explicit-hash] ✅ Success - Main denoland/setup-deno@v2 [1.846721265s]
Measurements
Workspace: one root deno.lock (61 lines, two dependencies), node_modules populated by pnpm, plus vendored subtrees.
| Configuration |
setup-deno step |
cache: true, no cache-hash |
killed at 2400 s, still walking |
cache: true + cache-hash |
1.85 s |
| no cache input |
0.49 s |
Directory walk of the same workspace, measured independently:
| Walk |
Result |
| symlinks not followed |
290,426 files / 47,814 dirs in 19 s |
| symlinks followed |
562,409 files / 58,705 dirs, still running at 300 s |
Top contributors by entry count: node_modules 154,204 and vendored subtrees 36,300.
The original report that led here was a CI job where the step was cancelled at 5m36s with Error: The operation was canceled. immediately after Installation complete. — the same signature, just a shorter leash.
Orientation
Affected: src/cache.ts (resolveDefaultCacheKey, restoreCache) and src/main.ts:65-67. The cache-hash input already short-circuits the expensive path, so the bypass semantics are established; the question is what the default should do instead. Relevant upstream defaults live in @actions/glob (packages/glob/src/glob.ts, packages/glob/src/internal-glob-options-helper.ts).
Non-Counting Outcomes
- Documenting
cache-hash in the README as a workaround while the default is unchanged. The default is the bug.
- Setting
followSymbolicLinks: false and nothing else. That walk still took 19 s and visited 290,426 files on this workspace; it narrows the blast radius without fixing the complexity.
- Restricting the glob to the workspace root only. That breaks multi-package repos that legitimately have nested
deno.lock files, which the current behavior supports.
- Wrapping the glob in a timeout or abort so a stall becomes a silent cache miss. The job stops hanging and the cache silently stops working.
- Benchmarking on a clean checkout. The pathological case requires a populated
node_modules; a fresh clone will not reproduce it.
Acceptance Criteria
Environment
denoland/setup-deno@v2, resolving to v2.0.5 (22d081ff2d3a40755e97629de92e3bcbfa7cf2ed)
- Deno 2.9.5,
x86_64-unknown-linux-gnu
- Originally observed on a self-hosted Linux runner; reproduced locally with
act 0.2.89 on catthehacker/ubuntu:act-latest
Problem Statement
On a large monorepo,
denoland/setup-deno@v2withcache: truelogsInstallation complete.and then goes completely silent for minutes until the job is killed. Nothing indicates what it is doing. The cause is the default cache key: when nocache-hashis supplied the action globs**/deno.lockacross the whole workspace with symlinks followed, so on a repo that has already runpnpm installit walks hundreds of thousands of paths to find one lockfile. Passing an explicitcache-hashmakes the same step finish in under two seconds, so the bypass exists — the default just should not be pathological.This matters more than usual right now because #116 proposes enabling
cacheby default.Goal
With
cache: trueand nocache-hash, cache-key resolution completes in under 10 seconds on a workspace whose symlink-followed reachable file count exceeds 500,000, while still deriving the key from everydeno.lockin the workspace.Evidence: the glob
Source:
src/cache.ts:34-41andsrc/cache.ts:69-74at commit22d081ff2d3a40755e97629de92e3bcbfa7cf2ed(v2.0.5). Call site:src/main.ts:65-67.hashFilesfrom@actions/globdefaults tofollowSymbolicLinks: true, and a**pattern must exhaust the tree — finding the single match early does not stop the walk. Every log line insiderestoreCacheis emitted after the key is computed, which is why the step produces no output at all while it stalls.Evidence: reproduction
Reproduced locally with
actagainst a real monorepo checkout, container bind-mounted to the workspace:The same workspace, same container, same action, with
cache-hash: ${{ hashFiles('deno.lock') }}added:Measurements
Workspace: one root
deno.lock(61 lines, two dependencies),node_modulespopulated by pnpm, plus vendored subtrees.cache: true, nocache-hashcache: true+cache-hashDirectory walk of the same workspace, measured independently:
Top contributors by entry count:
node_modules154,204 and vendored subtrees 36,300.The original report that led here was a CI job where the step was cancelled at 5m36s with
Error: The operation was canceled.immediately afterInstallation complete.— the same signature, just a shorter leash.Orientation
Affected:
src/cache.ts(resolveDefaultCacheKey,restoreCache) andsrc/main.ts:65-67. Thecache-hashinput already short-circuits the expensive path, so the bypass semantics are established; the question is what the default should do instead. Relevant upstream defaults live in@actions/glob(packages/glob/src/glob.ts,packages/glob/src/internal-glob-options-helper.ts).Non-Counting Outcomes
cache-hashin the README as a workaround while the default is unchanged. The default is the bug.followSymbolicLinks: falseand nothing else. That walk still took 19 s and visited 290,426 files on this workspace; it narrows the blast radius without fixing the complexity.deno.lockfiles, which the current behavior supports.node_modules; a fresh clone will not reproduce it.Acceptance Criteria
node_modulessymlink tree and one rootdeno.lockresolves the default cache key in under 10 s, and fails when the change is reverted.cache: truewith nocache-hashon a workspace with more than 500,000 symlink-followed reachable files resolves a key without exceeding a typical 10-minute job budget.deno.lockfiles in nested subdirectories still produces a key derived from all of them, not just the root one.Environment
denoland/setup-deno@v2, resolving to v2.0.5 (22d081ff2d3a40755e97629de92e3bcbfa7cf2ed)x86_64-unknown-linux-gnuact0.2.89 oncatthehacker/ubuntu:act-latest