diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1113eff..91c73e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,11 +24,70 @@ jobs: - name: Checkout uses: actions/checkout@v4 + # pin-depends float on `#main`: resolve their current commits so the + # opam cache key changes when a pinned repo moves. Without this the + # restored switch is always stale (its pins predate upstream HEAD), + # `opam install` rebuilds them on every run, and the rebuilt result is + # never cached because the save step is skipped on cache hits. + - name: Resolve pinned dependency commits + id: pins + run: | + export GIT_TERMINAL_PROMPT=0 + sha=$( + grep -hoE 'git\+[^"]+#[^"]+' ./*.opam | sort -u | while IFS= read -r pin; do + repo="${pin%%#*}"; ref="${pin##*#}" + git ls-remote "${repo#git+}" "$ref" || echo "unresolved $pin" + done | shasum | awk '{print $1}' + ) + echo "resolved pins sha: $sha" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + # setup-ocaml creates a *local* switch: the compiler and installed + # packages live in `_opam/` (~1GB+), while `~/.opam` is only the opam + # root skeleton (~40MB) — caching `~/.opam` alone never restores deps. + - name: Restore opam root + id: opam-root + uses: actions/cache/restore@v4 + with: + path: ~/.opam + key: opam-root-${{ runner.os }}-ocaml-${{ matrix.ocaml-compiler }} + + - name: Restore opam switch + id: opam-switch + uses: actions/cache/restore@v4 + with: + path: _opam + key: opam-switch-${{ runner.os }}-ocaml-${{ matrix.ocaml-compiler }}-${{ hashFiles('*.opam') }}-${{ steps.pins.outputs.sha }} + restore-keys: | + opam-switch-${{ runner.os }}-ocaml-${{ matrix.ocaml-compiler }}- + + # A restored root + local switch is already complete; only the opam + # binary is needed. This skips ocaml/setup-ocaml, whose unconditional + # `opam update` costs ~1m30s on cache hits. + - name: Check for restored opam environment + id: opam-env + run: | + if [ -f "$HOME/.opam/config" ] && [ -d "_opam" ]; then + echo "restored=true" >> "$GITHUB_OUTPUT" + else + echo "restored=false" >> "$GITHUB_OUTPUT" + fi + + - name: Install opam binary + if: steps.opam-env.outputs.restored == 'true' + run: | + mkdir -p ~/.local/bin + curl -fsSL https://github.com/ocaml/opam/releases/download/2.6.0/opam-2.6.0-x86_64-linux -o ~/.local/bin/opam + chmod +x ~/.local/bin/opam + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + eval "$(opam env)" + opam --version && ocaml -version + - name: Set up OCaml + if: steps.opam-env.outputs.restored != 'true' uses: ocaml/setup-ocaml@v3 with: ocaml-compiler: ${{ matrix.ocaml-compiler }} - dune-cache: true - name: Set up Java uses: actions/setup-java@v4 @@ -65,24 +124,45 @@ jobs: - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libsqlite3-dev pkg-config - - name: Restore opam switch cache - id: opam-cache - uses: actions/cache/restore@v4 - with: - path: ~/.opam - key: ${{ runner.os }}-ocaml-${{ matrix.ocaml-compiler }}-opam-${{ hashFiles('*.opam') }} - restore-keys: | - ${{ runner.os }}-ocaml-${{ matrix.ocaml-compiler }}-opam- - + # On an exact switch-cache hit this only verifies and re-fetches the + # (already matching) pins; on a partial restore it rebuilds just the + # moved pins; on a miss it installs everything. The retry covers a + # stale opam-repository snapshot in a restored root failing to resolve + # newly released versions. - name: Install OCaml dependencies - run: opam install . --deps-only --with-test -y + run: | + opam install . --deps-only --with-test -y || { + opam update -y + opam install . --deps-only --with-test -y + } - - name: Save opam switch cache - if: steps.opam-cache.outputs.cache-hit != 'true' + - name: Save opam root + if: steps.opam-root.outputs.cache-hit != 'true' uses: actions/cache/save@v4 with: path: ~/.opam - key: ${{ steps.opam-cache.outputs.cache-primary-key }} + key: opam-root-${{ runner.os }}-ocaml-${{ matrix.ocaml-compiler }} + + - name: Save opam switch + if: steps.opam-switch.outputs.cache-hit != 'true' + uses: actions/cache/save@v4 + with: + path: _opam + key: ${{ steps.opam-switch.outputs.cache-primary-key }} + + - name: Restore dune build cache + uses: actions/cache/restore@v4 + with: + path: ~/.cache/dune + key: dune-cache-${{ github.ref_name }}-${{ github.sha }} + restore-keys: | + dune-cache-${{ github.ref_name }}- + dune-cache-main- + + - name: Enable dune shared cache + run: | + mkdir -p ~/.config/dune + printf '(lang dune 3.0)\n(cache enabled)\n' > ~/.config/dune/config - name: Cache upstream DataScript id: cache-upstream-datascript @@ -122,3 +202,10 @@ jobs: env: UPSTREAM_DATASCRIPT_JS: ${{ github.workspace }}/_ci/datascript/release-js/datascript.js run: opam exec -- script/benchmark_vs_cljs.sh + + - name: Save dune build cache + if: always() + uses: actions/cache/save@v4 + with: + path: ~/.cache/dune + key: dune-cache-${{ github.ref_name }}-${{ github.sha }}