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-versionacceptsv2.x,v2.1.x, an exact version, orlts. For strict reproducibility pin an exact version and review upgrades as standalone changes.deno-version-filereads the version from files like.tool-versionsto 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 buildproduces the output directory; pass it along withactions/upload-artifactor 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 withdeno 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:
- 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.
- Deploying from external CI: when you need a custom pipeline (for example, running the full matrix before release), use the
deno deployCLI:
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.