Deno Projects on GitHub Actions

Deno jobs on GitHub Actions start with an empty DENO_DIR. Pin it, restore it with WarpBuilds/cache keyed on deno.lock, and size warp- runners for deno test.

Last verified:

Deno jobs on GitHub Actions repeat the same download every run, because each job starts on a clean virtual machine with an empty DENO_DIR and deno install refetches the whole dependency graph before the first check runs. Pin DENO_DIR to a fixed path, restore it with WarpBuilds/cache@v1 keyed on deno.lock, and put the job on a warp- runner sized for how many test modules deno test --parallel actually has to run.

This page covers the cache directory and its key, the workflow that runs lint, check, and test against a warm cache, and the sizing arithmetic for Deno test parallelism.

Overview

Deno keeps downloaded dependencies in a single global cache directory named DENO_DIR. Running deno info with no arguments prints three paths: the DENO_DIR location, the remote modules cache, and the TypeScript compiler cache. All three live under the same root, so one restored directory brings back remote modules, npm packages, and the type-check output together.

deno install with no arguments installs everything declared in deno.json and package.json into that global cache, and creates a local node_modules directory as well when a package.json is present. The --frozen flag makes the command error out when the lockfile is out of date, which is the behavior a GitHub Actions job wants.

A GitHub Actions job never inherits the previous job's disk. The global cache is empty at the start of every run, so the fetch happens again on every push unless the directory is restored explicitly.

There is no WarpBuild fork of a Deno setup action. The WarpBuild setup actions cover Node.js, Python, Go, Java, .NET, Ruby, Zig, Rust, Gradle, and mise, so Deno installs through the upstream denoland/setup-deno@v2 action and the cache is handled as its own step. One detail matters here: setup-deno has a cache input that defaults to false and writes to the GitHub Actions cache when enabled. Leave it off on warp- runners and restore DENO_DIR with WarpBuilds/cache@v1, which is a drop-in replacement for actions/cache@v4 and stores entries in WarpBuild Cache.

Deno work belongs on the Linux sizes described in the cloud runners documentation, and moving a job onto them is a one-line change to runs-on.

Configuration

The workflow installs Deno, restores DENO_DIR keyed on deno.lock, and runs format, lint, type check, and test on a 4 vCPU WarpBuild runner.

name: deno-ci
on:
  push:
    branches: [main]
  pull_request:

env:
  DENO_DIR: ${{ github.workspace }}/.deno-cache

jobs:
  check:
    runs-on: warp-ubuntu-latest-x64-4x
    steps:
      - uses: actions/checkout@v4

      - uses: denoland/setup-deno@v2
        with:
          deno-version: 2.x

      - name: Restore Deno cache
        uses: WarpBuilds/cache@v1
        with:
          path: ${{ env.DENO_DIR }}
          key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }}
          restore-keys: |
            ${{ runner.os }}-deno-

      - run: deno install --frozen
      - run: deno fmt --check
      - run: deno lint
      - run: deno check src/
      - run: deno test --parallel --coverage=coverage

Four details carry the behavior.

The job-level env block pins DENO_DIR instead of relying on the per-user default, which differs by operating system. A pinned path gives the cache step one path value that stays correct when the same workflow later grows a macOS leg. Keep the cache step after actions/checkout@v4, since checkout cleans untracked files in the workspace before it fetches.

key hashes deno.lock, so the entry rolls exactly when the resolved dependency set changes. Hashing deno.json instead is the common mistake, because an edit to a task definition then throws away a cache that was still valid.

restore-keys keeps dependency-update pull requests cheap. When the exact key misses, the action restores the newest entry with the same prefix and deno install fetches only the packages that moved.

--frozen fails the job when deno.lock does not match the configuration files, which keeps the cache key honest.

Three behaviors from the caching documentation shape how entries age. The cache is scoped to key, version, and branch, so an entry written on a feature branch is separate from the one on main; seed main first and let branch builds pick it up through restore-keys. The version part is a hash over the compression tool and the cached paths, so a DENO_DIR written on a macOS runner does not restore on a Linux runner even under the same key. Entries expire 7 days after last use.

Cache usage is metered at $0.20 per GB-month of storage and $0.0001 per write or restore operation.

Sizing

Deno repositories run on the Linux sizes. These are the rows that matter:

Runner labelvCPUMemoryPrice per minute
warp-ubuntu-latest-x64-2x28 GB$0.004
warp-ubuntu-latest-x64-4x416 GB$0.008
warp-ubuntu-latest-x64-8x832 GB$0.016
warp-ubuntu-latest-arm64-4x416 GB$0.006

Test parallelism is the reason to care about vCPU count. The deno test reference states that --parallel runs test modules in parallel and that parallelism defaults to the number of available CPUs or the value of the DENO_JOBS environment variable. Without the flag, the suite runs in one process and a larger machine changes nothing. With the flag, wall time falls only while there are at least as many test modules as workers, so a suite of six files stops improving somewhere around 4 to 8 vCPUs.

deno check and the implicit type check inside deno test are the other consumers. deno test type-checks local modules unless --no-check is passed, and type checking is memory-hungry on a large program rather than core-hungry: 16 GB on the 4x size covers most projects, and a heap failure is the signal to move up.

The ARM64 row is worth a run when the service also ships on ARM, since warp-ubuntu-latest-arm64-4x costs $0.006 per minute against $0.008 for the x64 machine of the same shape. Full specifications for each size are on the Linux x64 runner page.

Worked cost model

GitHub publishes per-minute list prices for its hosted runners at github.com/pricing and in the minute multipliers reference. Checked on 2026-08-13, the 4-core Linux larger runner is $0.012 per minute.

Stated as list-price arithmetic: warp-ubuntu-latest-x64-4x (4 vCPU, 16 GB) costs $0.008 per minute against $0.012 per minute for the 4-core Linux larger runner (4 vCPU, 16 GB): 33 percent lower list price. GitHub list price checked on 2026-08-13.

Take a repository running 1,200 jobs a month at 5 minutes each on a 4 vCPU machine with a warm DENO_DIR, which is 6,000 runner minutes.

ScenarioRateMonthly minutesMonthly cost
GitHub-hosted 4-core Linux larger runner$0.012/min6,000$72.00
warp-ubuntu-latest-x64-4x$0.008/min6,000$48.00
Cache storage and operations (500 MB, 2,400 ops)see aboven/a$0.34

Cache metering adds $0.10 for storage and $0.24 for operations, so the WarpBuild column lands at $48.34. Rates for every size and platform are on the pricing page.

Bottlenecks

Four problems account for most slow Deno pipelines on GitHub Actions.

A cold DENO_DIR. Without a restored cache directory, every job refetches remote modules and npm packages and recompiles the TypeScript it just discarded. Confirm the fix from the job log: a warm run shows the cache step restoring an entry and deno install --frozen finishing without a long resolution phase. A key that always misses usually hashes a file that changes on every commit, or was only ever written on a feature branch. The mechanics of key design are covered in cache key.

Type checking twice. deno check in one step and deno test in the next both type-check the same local modules. When the check step has already run and the results are in the restored compiler cache, the repeat is cheap; when the cache is cold, it is paid twice. Splitting check and test into separate jobs makes this worse, since each job restores its own copy.

npm interop. A package.json in the repository makes deno install create a local node_modules directory alongside the global cache. That directory is not covered by a DENO_DIR cache entry, so a job that depends on it either reinstalls or needs a second cache path. Prefer restoring DENO_DIR alone and letting deno install relink node_modules, which is faster than restoring a large tree of small files.

Unbounded parallelism. --parallel starts one worker per available CPU, and tests that bind a fixed port or share a temporary database collide as soon as the machine gets bigger. Cap the workers with DENO_JOBS rather than dropping back to a smaller runner:

  test:
    runs-on: warp-ubuntu-latest-x64-8x
    env:
      DENO_DIR: ${{ github.workspace }}/.deno-cache
      DENO_JOBS: 4
    steps:
      - uses: actions/checkout@v4
      - uses: denoland/setup-deno@v2
      - uses: WarpBuilds/cache@v1
        with:
          path: ${{ env.DENO_DIR }}
          key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }}
          restore-keys: |
            ${{ runner.os }}-deno-
      - run: deno install --frozen
      - run: deno test --parallel --no-check

When a job is slow and the cause is unclear, WarpBuild CI observability reports system metrics from the runner agent alongside GitHub Actions job logs, which separates a CPU-bound test phase from an install stuck on the network. The Action Debugger pauses a workflow and opens a session on the runner, so you can list the restored DENO_DIR on the machine and confirm what the cache step actually wrote.

Proof

Public OSS repositories running warp- labels are citable evidence, and the workflow files are open to read. The Trigger.dev TypeScript monorepo runs its end-to-end matrix on warp-ubuntu-latest-x64-4x and warp-windows-latest-x64-8x in triggerdotdev/trigger.dev's e2e.yml, checked on 2026-08-13.

Every cost number on this page carries a source and a date: WarpBuild rates come from the pricing page, and the GitHub list price comes from GitHub's own billing reference, checked on 2026-08-13.

If the repository mixes Deno with other JavaScript runtimes, Bun builds and tests on GitHub Actions covers the install cache path and key for Bun, and Node.js builds on GitHub Actions covers the package-manager store caching for the rest of the tree.

FAQ

Which directory holds the Deno dependency cache?

DENO_DIR. It holds the remote module cache, the npm package cache, and the TypeScript compiler cache, and deno info prints its location. Set DENO_DIR explicitly in the workflow so the same path works on Linux and macOS jobs, then restore that one path with WarpBuilds/cache@v1.

What should a Deno cache key hash?

deno.lock. Hash the lockfile with hashFiles and add a restore-keys prefix, so a dependency bump restores the previous entry and deno install fetches only what changed instead of rebuilding the cache from empty.

Does a bigger runner make deno test faster?

Only with --parallel. Deno runs test modules in one process unless --parallel is passed, and parallelism then defaults to the number of available CPUs or the value of DENO_JOBS, so extra vCPUs pay off once both the flag and enough test modules are present.

Should I use the cache input on setup-deno?

No. That input writes to the GitHub Actions cache. On warp- runners, leave it at its default of false and restore DENO_DIR with WarpBuilds/cache@v1 so the entry lands in WarpBuild Cache.

Start with $10 in free credits

Change the runner label in your workflow and keep the rest of your GitHub Actions setup. Runner time is billed per minute.