diff --git a/.github/workflows/csharp-ci.yaml b/.github/workflows/csharp-ci.yaml index 2b17680..b6d5aad 100644 --- a/.github/workflows/csharp-ci.yaml +++ b/.github/workflows/csharp-ci.yaml @@ -421,16 +421,18 @@ jobs: coverage-output: name: coverage-output - needs: build-and-test + needs: [normalize, build-and-test] if: ${{ success() }} runs-on: ubuntu-latest permissions: contents: read + env: + COVERAGE_EXPECTED: ${{ contains(fromJSON(needs.normalize.outputs.matrix).*.coverage, true) }} outputs: coverage: ${{ steps.read.outputs.coverage }} steps: - name: Download coverage value - continue-on-error: true + if: ${{ env.COVERAGE_EXPECTED == 'true' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ inputs.artifact-prefix }}-coverage-percent @@ -444,6 +446,10 @@ jobs: if [ -f coverage/coverage-percent.txt ]; then coverage=$(tr -d '[:space:]' < coverage/coverage-percent.txt) fi + if [ "$COVERAGE_EXPECTED" = "true" ] && [ -z "$coverage" ]; then + echo "::error::the coverage shard uploaded a coverage-percent artifact but no coverage value reached coverage-output — failing instead of emitting a blank coverage badge" + exit 1 + fi echo "coverage=$coverage" >> "$GITHUB_OUTPUT" matrix-output: diff --git a/.github/workflows/scala-ci.yaml b/.github/workflows/scala-ci.yaml index 3bee5e4..b6b457d 100644 --- a/.github/workflows/scala-ci.yaml +++ b/.github/workflows/scala-ci.yaml @@ -351,16 +351,18 @@ jobs: coverage-output: name: coverage-output - needs: build-and-test + needs: [normalize, build-and-test] if: ${{ success() }} runs-on: ubuntu-latest permissions: contents: read + env: + COVERAGE_EXPECTED: ${{ contains(fromJSON(needs.normalize.outputs.matrix).*.coverage, true) }} outputs: coverage: ${{ steps.read.outputs.coverage }} steps: - name: Download coverage value - continue-on-error: true + if: ${{ env.COVERAGE_EXPECTED == 'true' }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: ${{ inputs.artifact-prefix }}-coverage-percent @@ -374,6 +376,10 @@ jobs: if [ -f coverage/coverage-percent.txt ]; then coverage=$(tr -d '[:space:]' < coverage/coverage-percent.txt) fi + if [ "$COVERAGE_EXPECTED" = "true" ] && [ -z "$coverage" ]; then + echo "::error::the coverage shard uploaded a coverage-percent artifact but no coverage value reached coverage-output — failing instead of emitting a blank coverage badge" + exit 1 + fi echo "coverage=$coverage" >> "$GITHUB_OUTPUT" matrix-output: diff --git a/CHANGELOG.md b/CHANGELOG.md index eb28949..605947d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Fix `csharp-ci.yaml` and `scala-ci.yaml` silently emitting an empty `coverage` output when the coverage-percent artifact download failed. The `coverage-output` job downloaded the artifact with `continue-on-error: true` and defaulted to empty when the file was absent, conflating "no shard set `coverage: true`" (empty is correct) with "the coverage shard uploaded the artifact but the download failed" (throttling, retention, infra) — the latter fed a blank or stale coverage badge from an otherwise green run. The job now derives whether a coverage shard was configured from the normalized matrix: when one was, the artifact download must succeed and yield a non-empty value or the job fails loud; when none was, the download step is skipped and the empty output stays intentional. Consumers whose coverage shard uploads normally see no change. (#25) - Fix `go-ci.yaml`'s `Augment coverage report with cyclomatic complexity` step staging per-function coverage output at the fixed path `/tmp/cov-func.txt`. On shared self-hosted runners (e.g. the Hetzner pool) `/tmp` outlives the job and is shared across runner instances, so a leftover file owned by a different runner user fails the step with `permission denied`, and two Go jobs landing on the same host can interleave writes and stamp the wrong per-function coverage into each other's `Attended` column. The staging file now lives under `$RUNNER_TEMP`, which is private to the runner instance and wiped between jobs; GitHub-hosted consumers see no change. Closes out the self-hosted-runner hardening of this step begun in 2.3.2's sudo removal. (#12) ## [2.3.4] - 2026-07-10