denoland / denoland/setup-deno

Default cache key globs the entire workspace and stalls setup on large monorepos

Open
#134 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
TypeScript
Stars
330
Forks
58
PR merge metrics
No merged PRs in 30d

Description

## 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

```ts
export async function restoreCache(cacheHash: string) {
try {
const denoDir = await resolveDenoDir();
core.saveState(state.DENO_DIR, denoDir);

if (cacheHash.length === 0) {
cacheHash = await resolveDefaultCacheKey();
}
```

```ts
function resolveDefaultCacheKey(): Promise {
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.

```

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

- [ ] **(gatekeeper, runnable)** A test or benchmark over a fixture workspace containing a pnpm-style `node_modules` symlink tree and one root `deno.lock` resolves the default cache key in under 10 s, and fails when the change is reverted.
- [ ] `cache: true` with no `cache-hash` on a workspace with more than 500,000 symlink-followed reachable files resolves a key without exceeding a typical 10-minute job budget.
- [ ] A workspace with `deno.lock` files in nested subdirectories still produces a key derived from all of them, not just the root one.
- [ ] At least one log line is emitted before key computation begins, so a slow resolution is diagnosable from the job log rather than appearing as a hang.

## 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`

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with src/cache.ts, especially resolveDefaultCacheKey and restoreCache, then inspect the call site in src/main.ts:65-67 and the relevant @actions/glob defaults. Run the fixture benchmark described in the acceptance criteria against a populated pnpm-style node_modules tree. Done means fast default-key resolution, preserved nested deno.lock coverage, and a log line before computation.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions, typescript
Domain
ci-cd, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.