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
- GitHub Action repo: github.com/ocaml-dune/setup-dune
- Dune Package Management docs: dune.readthedocs.io
- If you want stable Dune instead of nightly, set
version: latest.
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.
