@@ -12,10 +12,17 @@ concurrency:
1212
1313env :
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
1619jobs :
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,159 @@ 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+ # Only save on master pushes. PR-scoped caches are tied to
122+ # refs/pull/<n>/merge and can't be restored by master or other PRs,
123+ # so they'd just consume storage and evict the shared entry. PRs
124+ # still get full restore from the latest master-saved cache.
125+ - name : Save OPAM environment
126+ if : steps.cache-opam-env.outputs.cache-hit != 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master'
127+ uses : actions/cache/save@v5
128+ with :
129+ path : |
130+ ${{ runner.tool_cache }}/opam
131+ ~/.opam
132+ _opam
133+ .opam-path
134+ key : ${{ env.opam_cache_key }}
135+
136+ - name : Use cached OPAM environment
137+ if : steps.cache-opam-env.outputs.cache-hit == 'true'
138+ run : |
139+ # https://github.com/ocaml/setup-ocaml/blob/v3.6.0/packages/setup-ocaml/src/installer.ts
140+ echo "OPAMCOLOR=always" >> "$GITHUB_ENV"
141+ echo "OPAMCONFIRMLEVEL=unsafe-yes" >> "$GITHUB_ENV"
142+ echo "OPAMDOWNLOADJOBS=4" >> "$GITHUB_ENV"
143+ echo "OPAMERRLOGLEN=0" >> "$GITHUB_ENV"
144+ echo "OPAMEXTERNALSOLVER=builtin-0install" >> "$GITHUB_ENV"
145+ echo "OPAMPRECISETRACKING=1" >> "$GITHUB_ENV"
146+ echo "OPAMRETRIES=10" >> "$GITHUB_ENV"
147+ echo "OPAMSOLVERTIMEOUT=600" >> "$GITHUB_ENV"
148+ echo "OPAMYES=1" >> "$GITHUB_ENV"
149+ echo "CLICOLOR_FORCE=1" >> "$GITHUB_ENV"
150+ echo "OPAMROOT=$HOME/.opam" >> "$GITHUB_ENV"
151+
152+ OPAM_PATH="$(cat .opam-path)"
153+ chmod +x "$OPAM_PATH"
154+ dirname "$OPAM_PATH" >> "$GITHUB_PATH"
155+
156+ # --- Coverage build cache --------------------------------------------
157+ # The bisect_ppx-instrumented dune build is a separate artifact from
158+ # the main CI build, so it gets its own key. Only restore/save on
159+ # master pushes — PRs share the master cache to stay fast but won't
160+ # poison it.
161+ - name : Coverage build state key
162+ id : coverage-build-state-key
163+ run : echo "value=coverage-build-state-v1-${{ env.OS }}-${{ env.SETUP_OCAML_VERSION }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('*.opam', 'compiler/**', 'dune-project') }}" >> $GITHUB_OUTPUT
164+
165+ - name : Restore coverage build state
166+ if : github.base_ref == 'master' || github.ref == 'refs/heads/master'
167+ id : coverage-build-state
168+ uses : actions/cache/restore@v5
169+ with :
170+ path : |
171+ ~/.cache/dune
172+ _build
173+ key : ${{ steps.coverage-build-state-key.outputs.value }}
174+
67175 - name : Run coverage
68176 run : opam exec -- make coverage
69177
178+ - name : Delete stale coverage build state
179+ if : github.event_name == 'push' && github.ref == 'refs/heads/master'
180+ run : |
181+ gh extension install actions/gh-actions-cache
182+ gh actions-cache delete ${{ steps.coverage-build-state-key.outputs.value }} \
183+ -R ${{ github.repository }} \
184+ -B "$GITHUB_REF" \
185+ --confirm || echo "not exist"
186+ env :
187+ GH_TOKEN : ${{ github.token }}
188+
189+ - name : Save coverage build state
190+ if : github.event_name == 'push' && github.ref == 'refs/heads/master'
191+ uses : actions/cache/save@v5
192+ with :
193+ path : |
194+ ~/.cache/dune
195+ _build
196+ key : ${{ steps.coverage-build-state-key.outputs.value }}
197+
70198 - name : Upload coverage to Codecov
71199 uses : codecov/codecov-action@v5
72200 with :
0 commit comments