DocsProduction deployment

CI/CD in practice

Build a reproducible Deno pipeline with setup-deno, deno ci, and layered gates, then ship artifacts to static hosting or Deno Deploy

Prerequisites: deno.json and deno.lock are committed and local gates pass. This page covers pipeline orchestration only; the gates themselves and the security baseline are in Production engineering baseline.

Install and pin Deno

Use the official action on GitHub Actions and pin the major version:

- uses: denoland/setup-deno@v2
  with:
    deno-version: v2.x
  • deno-version accepts v2.x, v2.1.x, an exact version, or lts. For strict reproducibility pin an exact version and review upgrades as standalone changes.
  • deno-version-file reads the version from files like .tool-versions to keep CI and local machines aligned.

Cache dependencies

setup-deno has built-in caching — no hand-written actions/cache needed:

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

cache: true caches Deno's downloaded dependencies (the DENO_DIR contents), keyed by job id, runner OS/arch, and a hash of deno.lock. Use cache-hash to override the hash (setting it implies caching). If the workflow sets the DENO_DIR environment variable itself, make sure the action and later steps use the same directory.

Install dependencies: deno ci

deno ci

deno ci (Deno 2.8+) is the reproducible install command for CI and Dockerfiles: it errors when deno.lock is missing, removes any existing node_modules, and installs with frozen semantics — the lockfile must match the config file exactly, and any drift fails instead of silently updating. Add --prod when building production artifacts to skip devDependencies; excluding @types/* as well requires a separate --skip-types — it decides by package-name heuristics and may wrongly skip packages that ship runtime code, so verify the output is still complete before relying on it.

Gate order

Order gates cheapest-and-fastest first:

deno ci
deno fmt --check
deno lint
deno check "**/*.ts" "**/*.tsx"
deno test

Format and lint finish in seconds and catch mechanical issues; type checking catches interface errors; tests are the most expensive and run last. Do not merge the gates into one command — keeping them separate makes the failing layer obvious in CI logs. Quote the globs so Deno expands them (a bare **/*.ts is expanded by the shell, which behaves inconsistently across runners and misses .tsx); projects that ship their own check task, such as Fresh, can simply run deno task check.

Cross-OS/arch matrix

Libraries and CLIs should run on at least three systems:

strategy:
  matrix:
    os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}

Watch out for CRLF on Windows: set git config --system core.autocrlf false before checkout so deno fmt --check does not fail on line endings. Add a canary Deno version with continue-on-error to spot upstream changes early without blocking merges. Restrict once-only steps like coverage reports with if: matrix.os == 'ubuntu-latest'.

Build and release artifacts

  • Static sites: deno task build produces the output directory; pass it along with actions/upload-artifact or hand it directly to the deploy step.
  • Single-file binaries: deno compile --target <target> cross-compiles per-platform artifacts to attach to a release. Check current flags with deno help compile.
  • Publishing JSR packages: do not publish on every push. Trigger on tags and publish with OIDC to get provenance — full setup in Publishing JSR packages.

Deploy to Deno Deploy

Two paths; pick one per team preference:

  1. Built-in GitHub integration (the default path): link the app to a GitHub repository in the Deno Deploy console; every push triggers a build, with no deploy YAML to maintain.
  2. Deploying from external CI: when you need a custom pipeline (for example, running the full matrix before release), use the deno deploy CLI:
deno deploy --org <org> --app <app> --prod

CLI authentication in CI uses an organization token: create one, store it as a GitHub repository secret, and pass it through the DENO_DEPLOY_TOKEN environment variable. Note that the OIDC page in the Deno Deploy docs covers running apps authenticating to third-party services (AWS, Vault, etc.) — it is not the CLI deploy authentication mechanism; do not conflate the two. Deploy Classic was officially shut down on 2026-07-20; do not use deployctl in new projects.

Complete example

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

permissions:
  contents: read

jobs:
  test:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    # Turn off CRLF conversion on Windows runners first, or fmt --check will false-positive
    steps:
      - run: |
          git config --system core.autocrlf false
          git config --system core.eol lf
      - uses: actions/checkout@v7
      - uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x
          cache: true
      - run: deno ci
      - run: deno fmt --check
      - run: deno lint
      - run: deno check "**/*.ts" "**/*.tsx"
      - run: deno test

  deploy:
    needs: test
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v7
      - uses: denoland/setup-deno@v2
        with:
          deno-version: v2.x
      - run: deno ci --prod
      - run: deno task build # projects with a build step
      - run: deno deploy --org my-org --app my-app --prod
        env:
          DENO_DEPLOY_TOKEN: ${{ secrets.DENO_DEPLOY_TOKEN }}

Key points: environment: production enables GitHub environment protection rules for manual approval; the token is scoped to the target organization; the deploy job uses the same Deno major version as the test matrix.

Official references: Continuous integration, setup-deno, Deno 2.8 release notes (deno ci), deno deploy CLI reference, Deno Deploy changelog.

Type to search all documentation.