Skip to content

Commit 732613c

Browse files
committed
Coverage CI: cache OPAM env, rewatch build, and coverage dune state
The OPAM install step on a cold runner often takes 10+ minutes and periodically times out / fails. This mirrors the cache strategy used by the main CI workflow: store the OPAM tree (incl. dev-setup deps like bisect_ppx), the rewatch cargo target, and the instrumented dune build state. PRs restore the latest master cache; only master pushes write to it, so PRs can't poison it. The OPAM cache key is distinct from the main CI workflow's because coverage installs --with-dev-setup (for bisect_ppx) while CI only uses --with-test.
1 parent de28ae6 commit 732613c

1 file changed

Lines changed: 126 additions & 2 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,17 @@ concurrency:
1212

1313
env:
1414
OCAMLRUNPARAM: b
15+
# When changing the setup-ocaml version, also adjust it in the setup step
16+
# further below.
17+
SETUP_OCAML_VERSION: 3.6.0 # OPAM <2.6.0
1518

1619
jobs:
1720
coverage:
1821
runs-on: ubuntu-24.04-arm
22+
env:
23+
OS: ubuntu-24.04-arm
24+
OCAML_COMPILER: 5.3.0
25+
RUST_TARGET: aarch64-unknown-linux-gnu
1926
steps:
2027
- name: Checkout
2128
uses: actions/checkout@v6
@@ -35,38 +42,155 @@ jobs:
3542
packages: bubblewrap darcs g++-multilib gcc-multilib mercurial musl-tools rsync cmake
3643
version: v4
3744

45+
# --- rewatch build cache ---------------------------------------------
46+
# `make coverage` shells out to `make` which builds rewatch; cache the
47+
# cargo target dir so subsequent runs skip the Rust toolchain install
48+
# and the cargo build when the rewatch sources haven't changed.
49+
- name: Restore rewatch build cache
50+
id: rewatch-build-cache
51+
uses: actions/cache@v5
52+
with:
53+
path: rewatch/target
54+
key: rewatch-build-v3-${{ env.RUST_TARGET }}-${{ hashFiles('rewatch/src/**', 'rewatch/Cargo.lock') }}
55+
3856
- name: Determine Rust toolchain version
3957
id: rust-version
4058
run: |
4159
version="$(awk -F '"' '/^rust-version[[:space:]]*=/ { print $2; exit }' rewatch/Cargo.toml)"
4260
echo "version=${version}" >> "$GITHUB_OUTPUT"
4361
4462
- name: Install Rust toolchain
63+
if: steps.rewatch-build-cache.outputs.cache-hit != 'true'
4564
uses: dtolnay/rust-toolchain@master
4665
with:
4766
toolchain: ${{ steps.rust-version.outputs.version }}
4867
components: clippy, rustfmt
4968

5069
- name: Build rewatch
70+
if: steps.rewatch-build-cache.outputs.cache-hit != 'true'
5171
run: cargo build --manifest-path rewatch/Cargo.toml --release
5272

5373
- name: Copy rewatch binary
5474
run: |
5575
cp rewatch/target/release/rescript rescript
5676
./scripts/copyExes.js --rewatch
5777
58-
- name: Use OCaml
78+
# --- OPAM environment cache ------------------------------------------
79+
# The OPAM install step is the dominant cost on a cold run (often >10
80+
# min). Cache the whole opam tree, keyed on the OS, setup-ocaml
81+
# version, compiler, and the `.opam` files. The cache also includes
82+
# the dev-setup dependencies (e.g. bisect_ppx) so the key is distinct
83+
# from the main CI workflow's cache.
84+
- name: Get OPAM cache key
85+
run: echo "opam_cache_key=opam-coverage-v1-${{ env.OS }}-${{ env.SETUP_OCAML_VERSION }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('*.opam') }}" >> $GITHUB_ENV
86+
87+
- name: Restore OPAM environment
88+
id: cache-opam-env
89+
uses: actions/cache/restore@v5
90+
with:
91+
path: |
92+
${{ runner.tool_cache }}/opam
93+
~/.opam
94+
_opam
95+
.opam-path
96+
key: ${{ env.opam_cache_key }}
97+
98+
- name: Use OCaml ${{ env.OCAML_COMPILER }}
99+
if: steps.cache-opam-env.outputs.cache-hit != 'true'
59100
uses: ocaml/setup-ocaml@v3.6.0
60101
with:
61-
ocaml-compiler: 5.3.0
102+
ocaml-compiler: ${{ env.OCAML_COMPILER }}
62103
opam-pin: false
63104

105+
- name: Get OPAM executable path
106+
if: steps.cache-opam-env.outputs.cache-hit != 'true'
107+
uses: actions/github-script@v9
108+
with:
109+
script: |
110+
const opamPath = await io.which('opam', true);
111+
console.log('opam executable found: %s', opamPath);
112+
113+
const fs = require('fs/promises');
114+
await fs.writeFile('.opam-path', opamPath, 'utf-8');
115+
console.log('stored path to .opam-path');
116+
64117
- name: Install OPAM dependencies (incl. bisect_ppx)
118+
if: steps.cache-opam-env.outputs.cache-hit != 'true'
65119
run: opam install . --deps-only --with-test --with-dev-setup
66120

121+
- name: Save OPAM environment
122+
if: steps.cache-opam-env.outputs.cache-hit != 'true'
123+
uses: actions/cache/save@v5
124+
with:
125+
path: |
126+
${{ runner.tool_cache }}/opam
127+
~/.opam
128+
_opam
129+
.opam-path
130+
key: ${{ env.opam_cache_key }}
131+
132+
- name: Use cached OPAM environment
133+
if: steps.cache-opam-env.outputs.cache-hit == 'true'
134+
run: |
135+
# https://github.com/ocaml/setup-ocaml/blob/v3.6.0/packages/setup-ocaml/src/installer.ts
136+
echo "OPAMCOLOR=always" >> "$GITHUB_ENV"
137+
echo "OPAMCONFIRMLEVEL=unsafe-yes" >> "$GITHUB_ENV"
138+
echo "OPAMDOWNLOADJOBS=4" >> "$GITHUB_ENV"
139+
echo "OPAMERRLOGLEN=0" >> "$GITHUB_ENV"
140+
echo "OPAMEXTERNALSOLVER=builtin-0install" >> "$GITHUB_ENV"
141+
echo "OPAMPRECISETRACKING=1" >> "$GITHUB_ENV"
142+
echo "OPAMRETRIES=10" >> "$GITHUB_ENV"
143+
echo "OPAMSOLVERTIMEOUT=600" >> "$GITHUB_ENV"
144+
echo "OPAMYES=1" >> "$GITHUB_ENV"
145+
echo "CLICOLOR_FORCE=1" >> "$GITHUB_ENV"
146+
echo "OPAMROOT=$HOME/.opam" >> "$GITHUB_ENV"
147+
148+
OPAM_PATH="$(cat .opam-path)"
149+
chmod +x "$OPAM_PATH"
150+
dirname "$OPAM_PATH" >> "$GITHUB_PATH"
151+
152+
# --- Coverage build cache --------------------------------------------
153+
# The bisect_ppx-instrumented dune build is a separate artifact from
154+
# the main CI build, so it gets its own key. Only restore/save on
155+
# master pushes — PRs share the master cache to stay fast but won't
156+
# poison it.
157+
- name: Coverage build state key
158+
id: coverage-build-state-key
159+
run: echo "value=coverage-build-state-v1-${{ env.OS }}-${{ env.SETUP_OCAML_VERSION }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('*.opam', 'compiler/**', 'dune-project') }}" >> $GITHUB_OUTPUT
160+
161+
- name: Restore coverage build state
162+
if: github.base_ref == 'master' || github.ref == 'refs/heads/master'
163+
id: coverage-build-state
164+
uses: actions/cache/restore@v5
165+
with:
166+
path: |
167+
~/.cache/dune
168+
_build
169+
key: ${{ steps.coverage-build-state-key.outputs.value }}
170+
67171
- name: Run coverage
68172
run: opam exec -- make coverage
69173

174+
- name: Delete stale coverage build state
175+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
176+
run: |
177+
gh extension install actions/gh-actions-cache
178+
gh actions-cache delete ${{ steps.coverage-build-state-key.outputs.value }} \
179+
-R ${{ github.repository }} \
180+
-B "$GITHUB_REF" \
181+
--confirm || echo "not exist"
182+
env:
183+
GH_TOKEN: ${{ github.token }}
184+
185+
- name: Save coverage build state
186+
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
187+
uses: actions/cache/save@v5
188+
with:
189+
path: |
190+
~/.cache/dune
191+
_build
192+
key: ${{ steps.coverage-build-state-key.outputs.value }}
193+
70194
- name: Upload coverage to Codecov
71195
uses: codecov/codecov-action@v5
72196
with:

0 commit comments

Comments
 (0)