CI/CD for a Deno Fresh App with GitHub Actions
Format-check, lint, type-check, test, and deploy your Deno Fresh app to Deno Deploy on every push.
This recipe runs a Deno Fresh app through Deno's built-in tooling: fmt, lint, check, and test. It then deploys to Deno Deploy with the official deployctl action, no external build tools required.
What the pipeline does
- set up Deno
- run deno fmt --check and deno lint
- type-check with deno check
- run deno test
- deploy to Deno Deploy with deployctl
The workflow
denoland/setup-deno installs the runtime. The Fresh build step (deno task build) precompiles islands; deployctl ships main.ts to Deno Deploy.
name: CI
on:
push:
branches: [main]
pull_request:
permissions:
id-token: write
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: denoland/setup-deno@v2
with:
deno-version: v2.x
- run: deno fmt --check
- run: deno lint
- run: deno check main.ts
- run: deno test -A
- run: deno task build
- if: github.ref == 'refs/heads/main'
uses: denoland/deployctl@v1
with:
project: my-fresh-app
entrypoint: main.tsCaching and speed
Cache the Deno dependency cache (~/.cache/deno or DENO_DIR) with actions/cache keyed on deno.lock. Deno's tooling is fast, but cheaper managed runners such as Latchkey (around 69% cheaper than GitHub-hosted) keep PR runs quick and auto-retry a flaky module fetch from a registry.
Deploying
deployctl uses GitHub OIDC (id-token: write) to authenticate, so no token secret is needed when the project is linked. It uploads the entrypoint and assets to Deno Deploy and prints the deployment URL.
Key takeaways
- Use Deno's built-in fmt, lint, check, and test instead of external tools.
- Deploy with deployctl over OIDC, avoiding a stored token.
- Cache DENO_DIR keyed on deno.lock to speed dependency resolution.