From 57b31f770c1c4dad2c1afce328978dcadb0221a0 Mon Sep 17 00:00:00 2001 From: sotashimozono Date: Tue, 25 Aug 2026 02:16:10 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20an=20absent=20report=20is=20not=20a=20co?= =?UTF-8?q?verage=20bug=20=E2=80=94=20and=20the=20test=20#83=20was=20close?= =?UTF-8?q?d=20for=20lacking?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #83 measured all of this and was closed for one stated reason: this repository is public, so its own CI cannot reach the absent-artifact state by accident and the degrade path would ship untested. So the state is constructed here. WHAT CHANGES. `download-artifact` by `name:` HARD-FAILS on absence; by `pattern:` it succeeds with zero results. That asymmetry is why `collect` reaches its completeness gate under a full artifact quota while `Coverage (upload)` died at step two — same missing artifact, two outcomes. The absence is now a warning annotation and a step-summary line, and the uploader is gated on having a report as well as a token. WHY IT MATTERS NOW. Coverage is a REPORT; `Every unit ran, exactly once` is a CORRECTNESS CLAIM, and this file's own rule is that only the second may fail a suite. A private consumer whose org has exhausted Actions storage was getting a red required check for a run whose tests were all green — and it does not clear by deleting artifacts, because storage accrues in GigabyteHours. #83's closing note says so; measured downstream, 9628 expired records were purged org-wide and the newest retained artifact stayed frozen at the moment the quota filled. THE TEST. `action-degrades.yml` asks the action for an artifact no job in the run produces and requires the job to finish GREEN, plus a POSITIVE CONTROL that publishes a real lcov and takes the other branch — without which an action that did nothing at all would pass the first job, and degrading on absence would be indistinguishable from never working. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/action-degrades.yml | 73 +++++++++++++++++++++++++++ Project.toml | 2 +- actions/upload-coverage/action.yml | 29 +++++++++-- 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/action-degrades.yml diff --git a/.github/workflows/action-degrades.yml b/.github/workflows/action-degrades.yml new file mode 100644 index 0000000..d1f56d3 --- /dev/null +++ b/.github/workflows/action-degrades.yml @@ -0,0 +1,73 @@ +name: upload-coverage degrades on an absent report + +# THE TEST #83 DID NOT HAVE, and the reason it was closed: +# +# "This repository is public, so its storage is free and its own CI cannot reach the +# absent-artifact state by accident — it has to be constructed." +# +# So it is constructed here. `upload-coverage` is asked for an artifact that cannot exist, and the +# job must come back GREEN with the absence annotated. An absent report is not a coverage bug: +# coverage is a REPORT, `Every unit ran, exactly once` is a CORRECTNESS CLAIM, and only the second +# should be able to fail a suite. +# +# Without this, the degrade path ships untested and is exercised for the first time on a private +# consumer whose org has run out of Actions storage — which is exactly where it was needed and +# exactly where nobody can watch it. + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + absent: + name: an artifact that cannot exist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # No artifact of this name is produced by any job in this run, so the download finds nothing. + # `name:` would hard-fail here; `pattern:` is what makes the difference the action now relies + # on, and this step is what keeps that true. + - id: run + uses: ./actions/upload-coverage + with: + artifact: no-such-artifact-${{ github.run_id }} + codecov-token: '' + + - name: the job survived, and said why + run: | + set -euo pipefail + echo "reached the step after upload-coverage — the action did not abort the job" + + present: + name: a report that does exist still uploads-or-degrades cleanly + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + # THE POSITIVE CONTROL. Without it, an action that did nothing at all would pass the job + # above, and "degrades on absence" would be indistinguishable from "never works". + - name: Build a real lcov and publish it + run: | + set -euo pipefail + printf 'SF:src/Fake.jl\nDA:1,1\nDA:2,0\nend_of_record\n' > lcov.info + grep -c '^SF:' lcov.info + + - uses: actions/upload-artifact@v7 + with: + name: degrade-probe-lcov + path: lcov.info + retention-days: 1 + + - uses: ./actions/upload-coverage + with: + artifact: degrade-probe-lcov + codecov-token: '' + + - name: it found the report + run: echo "the present-report path ran to completion" diff --git a/Project.toml b/Project.toml index e74f6b2..ba2b23d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "TestShards" uuid = "acceef1d-f5e0-4fe4-a546-818dc56ce7b2" -version = "0.3.38" +version = "0.3.39" authors = ["sota shimozono "] [deps] diff --git a/actions/upload-coverage/action.yml b/actions/upload-coverage/action.yml index 7f5cc04..c487cf0 100644 --- a/actions/upload-coverage/action.yml +++ b/actions/upload-coverage/action.yml @@ -116,18 +116,39 @@ runs: with: submodules: ${{ inputs.submodules }} + # BY PATTERN, NOT BY NAME, and the difference is the whole fix. `download-artifact` by `name:` + # HARD-FAILS when the artifact is absent; by `pattern:` it succeeds with zero results. That + # asymmetry is why `collect` reaches its completeness gate under a full artifact quota while + # this job used to die at step two — same missing artifact, two different outcomes. + # + # An absent report is not a coverage BUG. Coverage is a report; "every unit ran exactly once" is + # a correctness claim, and only the second should be able to fail a suite. A private consumer + # whose org has run out of Actions storage was getting a red required check for a run whose + # tests were all green — for days, because storage is accrued in GigabyteHours and does not fall + # when artifacts are deleted, so it does not recover until the billing period rolls over. - uses: actions/download-artifact@v8 + id: fetch with: - name: ${{ inputs.artifact }} + pattern: ${{ inputs.artifact }} + merge-multiple: true - - shell: bash + - id: report + shell: bash run: | set -euo pipefail - [ -s lcov.info ] || { echo "::error::${{ inputs.artifact }} contained no lcov.info — did the sharded run finish with coverage on?"; exit 1; } + if [ ! -s lcov.info ]; then + # LOUD, and not fatal. The absence is annotated and summarised; what it must not do is + # gate a suite on a report. + echo "::warning::no ${{ inputs.artifact }} to upload — the sharded run produced no merged report. Coverage is NOT being sent; the tests themselves are unaffected." + echo "Coverage **not uploaded**: no \`${{ inputs.artifact }}\` artifact. Under a full Actions storage quota this is expected and says nothing about the tests." >> "$GITHUB_STEP_SUMMARY" + echo "have=false" >> "$GITHUB_OUTPUT" + exit 0 + fi echo "Merged report: $(grep -c '^SF:' lcov.info) source files." + echo "have=true" >> "$GITHUB_OUTPUT" - uses: codecov/codecov-action@v7 - if: ${{ steps.token.outputs.have == 'true' }} + if: ${{ steps.token.outputs.have == 'true' && steps.report.outputs.have == 'true' }} continue-on-error: ${{ inputs.fail-on-error != 'true' }} with: files: lcov.info