# CI/CD in practice

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](/en/docs/deploy/production-baseline).

## Install and pin Deno

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

```yaml
- 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:

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

```bash
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:

```bash
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:

```yaml
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](/en/docs/reference/publishing-jsr).

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

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

```yaml title=".github/workflows/ci.yml"
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.

<Callout type="warn" title="Deploys are irreversible external actions">
`deno deploy --prod` and `deno publish` affect external users immediately. Agents and automation scripts must not bypass environment approvals to trigger production deploys.
</Callout>

Official references: [Continuous integration](https://docs.deno.com/runtime/reference/continuous_integration/), [setup-deno](https://github.com/denoland/setup-deno), [Deno 2.8 release notes (deno ci)](https://deno.com/blog/v2.8), [deno deploy CLI reference](https://docs.deno.com/runtime/reference/cli/deploy/), [Deno Deploy changelog](https://docs.deno.com/deploy/changelog/).
