← Back to BlogDEAuf Deutsch lesen

Dune Package Management on GitHub Actions: A Practical Setup

I'll show you a simple GitHub Actions setup for Dune Package Management using the ocaml-dune/setup-dune action.

This is a short, practical setup. I use it for OCaml repos that use dune pkg and that do not commit the dune.lock directory, following current recommendations from Dune maintainers.

Minimal workflow

Create .github/workflows/ci.yml:

name: CI

on:
  push:
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Install Dune and build
        uses: ocaml-dune/setup-dune@v2

This installs Dune, builds dependencies, builds your project, and runs tests.

The cache strategy

The action already caches the Dune cache and _build via actions/cache. It keys the cache on dune-project, the Dune version, OS, arch, and the commit SHA, and it restores older caches via a prefix. The key still includes the SHA, so restore behavior is the lever you can influence.

That's good, but I want restores to align with dependency changes, not with every commit. In Docker I do that by isolating the compiler and dependency layer behind dune-project and dune.lock - see A Practical Docker Cache Pattern for Dune Package Management. In GitHub Actions, I fold a dependency signal into the cache prefix so the restore keys stay stable across source changes.

      - name: Install Dune and build
        uses: ocaml-dune/setup-dune@v2
        with:
          cache-prefix: v1-${{ hashFiles('dune-project', 'dune-workspace.ci') }}

This keeps restore keys stable across source changes and narrows cache reuse to a specific dependency setup. I pin the opam-repository commit to make builds fully reproducible - otherwise the opam repository moves under you, and the same commit can pull different package versions on different days. That can also create a local/CI mismatch: you resolve newer deps locally while CI keeps restoring an older cached set. A simple approach is to pin the opam-repository commit in a workspace file, like ocaml.org does.

If you want to pin the OCaml version itself, keep that constraint in dune-project, not in the workspace.

(package
 (name your_package)
 (depends
  (ocaml (= 5.3.0))))

On the example repo, the CI build time went from about 3 minutes to 23 seconds after the cache warmed. You can see the cached vs non-cached runs on the latest commits: github.com/sabine/dune-pkg-github-actions-example/commits/main.

(repository
 (name pinned_opam_repository)
 (url git+https://github.com/ocaml/opam-repository#584630e7a7e27e3cf56158696a3fe94623a0cf4f))

Full example: github.com/ocaml/ocaml.org/.../dune-workspace

If you want the newest opam-repository packages on every run, skip the pin and accept solver churn as the repo moves. Cache hits can be stale unless your key includes dune.lock (or similar). I prefer stability in CI and deployment and update the pin manually.

Optional: trim the action steps

If you do not need every step, you can pass a custom list. I typically keep defaults unless I am optimizing for speed on macOS.

      - name: Install Dune and build
        uses: ocaml-dune/setup-dune@v2
        with:
          steps: install-dune enable-pkg build-deps build runtest

Notes

That's it. The existing ocaml-dune/setup-dune action handles the heavy lifting, and a dependency-scoped cache prefix keeps CI fast without extra tooling.

This is my personal, opinionated setup. It is not a recommendation from the Dune maintainers.