From 732613c2d6b4c91ffca9c879386bbc4407ebd664 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Wed, 20 May 2026 14:06:46 +0200 Subject: [PATCH 1/3] 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. --- .github/workflows/coverage.yml | 128 ++++++++++++++++++++++++++++++++- 1 file changed, 126 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 9f4cac466d..aa8c6c8cd0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,10 +12,17 @@ concurrency: env: OCAMLRUNPARAM: b + # When changing the setup-ocaml version, also adjust it in the setup step + # further below. + SETUP_OCAML_VERSION: 3.6.0 # OPAM <2.6.0 jobs: coverage: runs-on: ubuntu-24.04-arm + env: + OS: ubuntu-24.04-arm + OCAML_COMPILER: 5.3.0 + RUST_TARGET: aarch64-unknown-linux-gnu steps: - name: Checkout uses: actions/checkout@v6 @@ -35,6 +42,17 @@ jobs: packages: bubblewrap darcs g++-multilib gcc-multilib mercurial musl-tools rsync cmake version: v4 + # --- rewatch build cache --------------------------------------------- + # `make coverage` shells out to `make` which builds rewatch; cache the + # cargo target dir so subsequent runs skip the Rust toolchain install + # and the cargo build when the rewatch sources haven't changed. + - name: Restore rewatch build cache + id: rewatch-build-cache + uses: actions/cache@v5 + with: + path: rewatch/target + key: rewatch-build-v3-${{ env.RUST_TARGET }}-${{ hashFiles('rewatch/src/**', 'rewatch/Cargo.lock') }} + - name: Determine Rust toolchain version id: rust-version run: | @@ -42,12 +60,14 @@ jobs: echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Install Rust toolchain + if: steps.rewatch-build-cache.outputs.cache-hit != 'true' uses: dtolnay/rust-toolchain@master with: toolchain: ${{ steps.rust-version.outputs.version }} components: clippy, rustfmt - name: Build rewatch + if: steps.rewatch-build-cache.outputs.cache-hit != 'true' run: cargo build --manifest-path rewatch/Cargo.toml --release - name: Copy rewatch binary @@ -55,18 +75,122 @@ jobs: cp rewatch/target/release/rescript rescript ./scripts/copyExes.js --rewatch - - name: Use OCaml + # --- OPAM environment cache ------------------------------------------ + # The OPAM install step is the dominant cost on a cold run (often >10 + # min). Cache the whole opam tree, keyed on the OS, setup-ocaml + # version, compiler, and the `.opam` files. The cache also includes + # the dev-setup dependencies (e.g. bisect_ppx) so the key is distinct + # from the main CI workflow's cache. + - name: Get OPAM cache key + run: echo "opam_cache_key=opam-coverage-v1-${{ env.OS }}-${{ env.SETUP_OCAML_VERSION }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('*.opam') }}" >> $GITHUB_ENV + + - name: Restore OPAM environment + id: cache-opam-env + uses: actions/cache/restore@v5 + with: + path: | + ${{ runner.tool_cache }}/opam + ~/.opam + _opam + .opam-path + key: ${{ env.opam_cache_key }} + + - name: Use OCaml ${{ env.OCAML_COMPILER }} + if: steps.cache-opam-env.outputs.cache-hit != 'true' uses: ocaml/setup-ocaml@v3.6.0 with: - ocaml-compiler: 5.3.0 + ocaml-compiler: ${{ env.OCAML_COMPILER }} opam-pin: false + - name: Get OPAM executable path + if: steps.cache-opam-env.outputs.cache-hit != 'true' + uses: actions/github-script@v9 + with: + script: | + const opamPath = await io.which('opam', true); + console.log('opam executable found: %s', opamPath); + + const fs = require('fs/promises'); + await fs.writeFile('.opam-path', opamPath, 'utf-8'); + console.log('stored path to .opam-path'); + - name: Install OPAM dependencies (incl. bisect_ppx) + if: steps.cache-opam-env.outputs.cache-hit != 'true' run: opam install . --deps-only --with-test --with-dev-setup + - name: Save OPAM environment + if: steps.cache-opam-env.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + ${{ runner.tool_cache }}/opam + ~/.opam + _opam + .opam-path + key: ${{ env.opam_cache_key }} + + - name: Use cached OPAM environment + if: steps.cache-opam-env.outputs.cache-hit == 'true' + run: | + # https://github.com/ocaml/setup-ocaml/blob/v3.6.0/packages/setup-ocaml/src/installer.ts + echo "OPAMCOLOR=always" >> "$GITHUB_ENV" + echo "OPAMCONFIRMLEVEL=unsafe-yes" >> "$GITHUB_ENV" + echo "OPAMDOWNLOADJOBS=4" >> "$GITHUB_ENV" + echo "OPAMERRLOGLEN=0" >> "$GITHUB_ENV" + echo "OPAMEXTERNALSOLVER=builtin-0install" >> "$GITHUB_ENV" + echo "OPAMPRECISETRACKING=1" >> "$GITHUB_ENV" + echo "OPAMRETRIES=10" >> "$GITHUB_ENV" + echo "OPAMSOLVERTIMEOUT=600" >> "$GITHUB_ENV" + echo "OPAMYES=1" >> "$GITHUB_ENV" + echo "CLICOLOR_FORCE=1" >> "$GITHUB_ENV" + echo "OPAMROOT=$HOME/.opam" >> "$GITHUB_ENV" + + OPAM_PATH="$(cat .opam-path)" + chmod +x "$OPAM_PATH" + dirname "$OPAM_PATH" >> "$GITHUB_PATH" + + # --- Coverage build cache -------------------------------------------- + # The bisect_ppx-instrumented dune build is a separate artifact from + # the main CI build, so it gets its own key. Only restore/save on + # master pushes — PRs share the master cache to stay fast but won't + # poison it. + - name: Coverage build state key + id: coverage-build-state-key + run: echo "value=coverage-build-state-v1-${{ env.OS }}-${{ env.SETUP_OCAML_VERSION }}-${{ env.OCAML_COMPILER }}-${{ hashFiles('*.opam', 'compiler/**', 'dune-project') }}" >> $GITHUB_OUTPUT + + - name: Restore coverage build state + if: github.base_ref == 'master' || github.ref == 'refs/heads/master' + id: coverage-build-state + uses: actions/cache/restore@v5 + with: + path: | + ~/.cache/dune + _build + key: ${{ steps.coverage-build-state-key.outputs.value }} + - name: Run coverage run: opam exec -- make coverage + - name: Delete stale coverage build state + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + run: | + gh extension install actions/gh-actions-cache + gh actions-cache delete ${{ steps.coverage-build-state-key.outputs.value }} \ + -R ${{ github.repository }} \ + -B "$GITHUB_REF" \ + --confirm || echo "not exist" + env: + GH_TOKEN: ${{ github.token }} + + - name: Save coverage build state + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: actions/cache/save@v5 + with: + path: | + ~/.cache/dune + _build + key: ${{ steps.coverage-build-state-key.outputs.value }} + - name: Upload coverage to Codecov uses: codecov/codecov-action@v5 with: From 84b166265b6b1e0c6dcc8217f15c1700c2866f3a Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Wed, 20 May 2026 18:17:30 +0200 Subject: [PATCH 2/3] Update changelog for #8434 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 85350be7e0..4864ff8e9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,7 @@ - Expand `super_errors` fixture coverage for warnings and errors. https://github.com/rescript-lang/rescript/pull/8429 - Run `super_errors` fixtures in parallel (~2.4× faster locally). https://github.com/rescript-lang/rescript/pull/8430 - Expand `super_errors` fixture coverage for the remaining reachable single-file error variants. https://github.com/rescript-lang/rescript/pull/8432 +- Cache OPAM env, rewatch build, and instrumented dune state in the coverage workflow. https://github.com/rescript-lang/rescript/pull/8434 # 13.0.0-alpha.4 From 3a99217d2059330da3353082ff8f07e3ee0370d6 Mon Sep 17 00:00:00 2001 From: Jono Prest Date: Wed, 20 May 2026 18:25:48 +0200 Subject: [PATCH 3/3] Coverage CI: gate OPAM cache save to master pushes (codex P2) actions/cache/save@v5 on cache miss runs on every event including pull_request, which creates PR-scoped cache entries tied to refs/pull//merge. Those entries aren't restorable by master or other PRs, so they consume storage and can evict the shared master-saved entry, reducing hit rate. Restrict the save to push events on refs/heads/master, matching the pattern already used for the coverage-build-state cache. PRs still benefit from full restore via the latest master cache; they just don't write new entries. Flagged by Codex review on #8434. --- .github/workflows/coverage.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index aa8c6c8cd0..ef173a2d93 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -118,8 +118,12 @@ jobs: if: steps.cache-opam-env.outputs.cache-hit != 'true' run: opam install . --deps-only --with-test --with-dev-setup + # Only save on master pushes. PR-scoped caches are tied to + # refs/pull//merge and can't be restored by master or other PRs, + # so they'd just consume storage and evict the shared entry. PRs + # still get full restore from the latest master-saved cache. - name: Save OPAM environment - if: steps.cache-opam-env.outputs.cache-hit != 'true' + if: steps.cache-opam-env.outputs.cache-hit != 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' uses: actions/cache/save@v5 with: path: |