From 8f7377bcace1ea5589a08d68d04a6d83d249cfc8 Mon Sep 17 00:00:00 2001 From: sshevchenko Date: Sun, 13 Sep 2026 00:19:15 +0200 Subject: [PATCH 1/3] Cache sbt 2 build output in the coverage job --- .github/workflows/ci.yml | 62 ++++++++++++++++++++++++++++------------ README.md | 15 ++++++---- 2 files changed, 53 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 027078c..8a06cc1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,8 +23,8 @@ on: default: '' clean_task: description: >- - sbt task that cleans the working directory. Defaults to `cleanFull` on sbt 2 and - `clean` on sbt 1, detected from project/build.properties. + Optional sbt task to run before the coverage build. Empty by default: the build output is + restored from cache between runs, and `cleanFull` drops sbt 2's action cache. type: string default: '' sonar: @@ -76,23 +76,20 @@ jobs: echo "test=test" >> "$GITHUB_OUTPUT" fi - - name: resolve clean task - id: clean-task - env: - CLEAN_TASK: ${{ inputs.clean_task }} + # sbt 1 has neither an action cache nor a `target/out`, so the coverage job skips the caching + # steps there rather than pointing them at paths that do not exist. + - name: detect sbt 2 + id: sbt2 run: | - if [[ -n "$CLEAN_TASK" ]]; then - echo "clean=$CLEAN_TASK" >> "$GITHUB_OUTPUT" - elif grep -qE '^sbt\.version\s*=\s*2\.' project/build.properties; then - # in sbt 2 the `clean` doesn't remove all generated classes - echo "clean=cleanFull" >> "$GITHUB_OUTPUT" + if grep -qE '^sbt\.version\s*=\s*2\.' project/build.properties; then + echo "sbt2=true" >> "$GITHUB_OUTPUT" else - echo "clean=clean" >> "$GITHUB_OUTPUT" + echo "sbt2=false" >> "$GITHUB_OUTPUT" fi outputs: test: ${{ steps.test-task.outputs.test }} - clean: ${{ steps.clean-task.outputs.clean }} + sbt2: ${{ steps.sbt2.outputs.sbt2 }} test-coverage: runs-on: ubuntu-latest @@ -123,15 +120,44 @@ jobs: - name: setup SBT uses: sbt/setup-sbt@v1 with: - # sbt 2's disk cache is restored across runs. Disable disk cache to force full coverage run + # the action's own disk cache is shared by every job and every matrix leg under one key, + # so it cannot be paired with this job's `target`. Cached below instead. disk-cache: false - # The coverage build runs before any other compile: scoverage's instrumentation is not part of - # sbt's compile cache key, so a plain compile done first would be reused here and the coverage - # report would come out empty. + # sbt's action cache restores `classes` but not `scoverage-data`, which the compiler writes as + # a side effect of the instrumented compile. Restore the action cache without `target` and the + # tests die writing their measurements into a directory that is not there, so the two are kept + # in one entry under one key: either both come back or neither does. + - name: restore build output + if: needs.sbt-tasks.outputs.sbt2 == 'true' + uses: actions/cache@v6 + with: + path: | + ~/.cache/sbt + target/out + key: sbt-coverage-${{ runner.os }}-java${{ inputs.java_version }}-scala${{ matrix.scala }}-${{ github.sha }} + restore-keys: sbt-coverage-${{ runner.os }}-java${{ inputs.java_version }}-scala${{ matrix.scala }}- + + # Measurements are written per run and never pruned, so a restored `target` would fold the + # previous run's hits into this report. The reports go too, otherwise the empty-report check + # below can pass on a stale file. + - name: drop stale coverage data + if: needs.sbt-tasks.outputs.sbt2 == 'true' + run: | + find . -path '*/scoverage-data/scoverage.measurements.*' -delete + find . -type d \( -name coverage-report -o -name scoverage-report \) -prune -exec rm -rf {} + + - name: build ${{ matrix.scala }} + env: + CLEAN_TASK: ${{ inputs.clean_task }} + TEST_TASK: ${{ needs.sbt-tasks.outputs.test }} + SCALA: ${{ matrix.scala }} run: | - sbt "++${{ matrix.scala }}; ${{ needs.sbt-tasks.outputs.clean }}; coverage; ${{ needs.sbt-tasks.outputs.test }}; coverageAggregate" + tasks="coverage; $TEST_TASK; coverageAggregate" + if [[ -n "$CLEAN_TASK" ]]; then + tasks="$CLEAN_TASK; $tasks" + fi + sbt "++$SCALA; $tasks" - name: locate coverage report id: coverage diff --git a/README.md b/README.md index f855e34..9a902bd 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ alternatives and drops the security rating to C: | `java_version` | `'17'` | | | `java_distribution` | `'temurin'` | | | `test_task` | auto | `testFull` on sbt 2, `test` on sbt 1, read from `project/build.properties` | -| `clean_task` | auto | `cleanFull` on sbt 2, `clean` on sbt 1, read from `project/build.properties` | +| `clean_task` | `''` | optional sbt task run before the coverage build; empty means no clean | | `sonar` | `false` | run a SonarQube Cloud scan, see below | | `sonar_project_key` | `_` | | | `sonar_args` | `''` | extra `-D` arguments for the scanner | @@ -72,11 +72,14 @@ All checks are run concurrently! Ideally, we must strive to keep them all green, some checks are red, for example if code formatting is not introduced, yet. Such red checks must be treated as nudge to improve the quality of code in repo! -* `test-coverage` - runs with disabled disk cache for SBT setup action (`disk-cache: false`) to make sure that - test coverage gets run with fully instrumented compilation. The workflow also fails if the produced Cobertura - report has no valid lines, so a silently empty report is an error rather than a green build. - If project has `sonar` integration configured and - enabled, then `sonar scan` will get run after coverage reports are uploaded +* `test-coverage` - runs the instrumented build and uploads the Cobertura report to Coveralls. On sbt 2 the + action cache (`~/.cache/sbt`) and the build output (`target/out`) are cached together, as a single entry under + one key: sbt's action cache restores `classes` but not scoverage's `scoverage-data`, which the compiler writes + as a side effect, and a run that gets one without the other fails while the tests write their measurements. + Stale measurement files and reports are deleted before the build, so a restored `target/out` cannot fold the + previous run's coverage into this one. The workflow also fails if the produced Cobertura report has no valid + lines, so a silently empty report is an error rather than a green build. If project has `sonar` integration + configured and enabled, then `sonar scan` will get run after coverage reports are uploaded * `binary-compatibility` - runs [sbt-version-policy](https://github.com/scalacenter/sbt-version-policy/)'s `versionPolicyCheck` task on repo with full history (`fetch-depth: 0`) to make sure that plugin can find the tag for previous version From 8f5e43a342c46e5ec88f9ab3d76a572e54aa977c Mon Sep 17 00:00:00 2001 From: sshevchenko Date: Mon, 14 Sep 2026 20:08:37 +0200 Subject: [PATCH 2/3] Rename sbt2 output to uses-sbt2 --- .github/workflows/ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a06cc1..2dfc0ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,17 +79,17 @@ jobs: # sbt 1 has neither an action cache nor a `target/out`, so the coverage job skips the caching # steps there rather than pointing them at paths that do not exist. - name: detect sbt 2 - id: sbt2 + id: uses-sbt2 run: | if grep -qE '^sbt\.version\s*=\s*2\.' project/build.properties; then - echo "sbt2=true" >> "$GITHUB_OUTPUT" + echo "uses-sbt2=true" >> "$GITHUB_OUTPUT" else - echo "sbt2=false" >> "$GITHUB_OUTPUT" + echo "uses-sbt2=false" >> "$GITHUB_OUTPUT" fi outputs: test: ${{ steps.test-task.outputs.test }} - sbt2: ${{ steps.sbt2.outputs.sbt2 }} + uses-sbt2: ${{ steps.uses-sbt2.outputs.uses-sbt2 }} test-coverage: runs-on: ubuntu-latest @@ -129,7 +129,7 @@ jobs: # tests die writing their measurements into a directory that is not there, so the two are kept # in one entry under one key: either both come back or neither does. - name: restore build output - if: needs.sbt-tasks.outputs.sbt2 == 'true' + if: needs.sbt-tasks.outputs.uses-sbt2 == 'true' uses: actions/cache@v6 with: path: | @@ -142,7 +142,7 @@ jobs: # previous run's hits into this report. The reports go too, otherwise the empty-report check # below can pass on a stale file. - name: drop stale coverage data - if: needs.sbt-tasks.outputs.sbt2 == 'true' + if: needs.sbt-tasks.outputs.uses-sbt2 == 'true' run: | find . -path '*/scoverage-data/scoverage.measurements.*' -delete find . -type d \( -name coverage-report -o -name scoverage-report \) -prune -exec rm -rf {} + From c5149c701542c233773574b6428155377b36eea8 Mon Sep 17 00:00:00 2001 From: sshevchenko Date: Mon, 21 Sep 2026 19:07:37 +0200 Subject: [PATCH 3/3] Cache only sbt's action cache, not the launcher --- .github/workflows/ci.yml | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dfc0ba..401e040 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -133,7 +133,7 @@ jobs: uses: actions/cache@v6 with: path: | - ~/.cache/sbt + ~/.cache/sbt/v2 target/out key: sbt-coverage-${{ runner.os }}-java${{ inputs.java_version }}-scala${{ matrix.scala }}-${{ github.sha }} restore-keys: sbt-coverage-${{ runner.os }}-java${{ inputs.java_version }}-scala${{ matrix.scala }}- diff --git a/README.md b/README.md index 16e0664..b477c6c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ some checks are red, for example if code formatting is not introduced, yet. Such to improve the quality of code in repo! * `test-coverage` - runs the instrumented build and uploads the Cobertura report to Coveralls. On sbt 2 the - action cache (`~/.cache/sbt`) and the build output (`target/out`) are cached together, as a single entry under + action cache (`~/.cache/sbt/v2`) and the build output (`target/out`) are cached together, as a single entry under one key: sbt's action cache restores `classes` but not scoverage's `scoverage-data`, which the compiler writes as a side effect, and a run that gets one without the other fails while the tests write their measurements. Stale measurement files and reports are deleted before the build, so a restored `target/out` cannot fold the