Skip to content

fix(upload-coverage): an artifact that was never created is not a coverage bug - #83

Closed
sotashimozono wants to merge 2 commits into
mainfrom
fix/tolerate-absent-lcov
Closed

sotashimozono wants to merge 2 commits into
mainfrom
fix/tolerate-absent-lcov

Conversation

@sotashimozono

Copy link
Copy Markdown
Member

One of the two jobs still failing across the fleet under a full artifact quota. The other is collect's completeness gate, which needs the evidence transport in #82 and is not this PR.

What fails today

Coverage (upload):  Unable to download artifact(s): Artifact not found for name: testshards-lcov

Measured on lab-sotashimozono/ParameterizedITensor.jl run 32238414796: the job's second step, before any coverage work.

Why it is a hard failure here and not in collect

This download names the artifact (name:). collect's names a pattern, and a pattern: download succeeds with zero results. Same missing artifacts, two different outcomes — which is why collect reaches its completeness gate and reports units observed = 0 while this job dies at the download.

Why absence must not be fatal

The Actions storage quota is enforced at CreateArtifact, before any bytes are sent. A 0-byte artifact is refused exactly as a large one is. So when the artifact is absent there is no producer error, nothing to retry, and nothing that failing here can fix — it converts a storage condition into a red test run.

But absent and empty are different claims and now get different treatment:

state before after
artifact absent job red at the download warning naming the likely cause + a step-summary line + exit 0; upload skipped, not sent empty
artifact present, no lcov.info fatal still fatal — something produced it and put no report in it, which is a producer bug

The codecov-action step gains steps.fetch.outcome == 'success' so it cannot be handed a report that was never fetched.

Verification

Three conditionals, and the state they handle is one the action cannot manufacture locally — an absent artifact requires an account that cannot create one. It is verified by the fleet immediately: every private consumer is in exactly that state right now. Stated plainly rather than dressed up as a local test.

🤖 Generated with Claude Code

…erage bug

`Coverage (upload)` went red at the DOWNLOAD, before anything about coverage was
attempted:

    Unable to download artifact(s): Artifact not found for name: testshards-lcov

This download is by `name:`, which hard-fails on absence — unlike the workflow's own
`pattern:` download, which succeeds with zero results. That asymmetry is why `collect`
gets as far as its completeness gate under a full quota while this job dies at step two.

An artifact that was never created is an infrastructure state, not a producer error: the
Actions storage quota is enforced at `CreateArtifact` **before any bytes are sent**, so it
refuses a 0-byte artifact exactly as it refuses a large one, and nothing here can be fixed
by failing. Losing the coverage REPORT is the correct degradation; taking the run down
with it is not.

ABSENT and EMPTY now get different treatment, because they are different claims:

  absent  -> a warning naming the likely cause, a step-summary line saying coverage was
             not reported, and exit 0. The upload is skipped rather than sent empty.
  empty   -> still fatal. Something produced the artifact and put no report in it, which
             is a producer bug and the message it already had.

Verifiable only in a real run — the change is three conditionals and the state it handles
is one the action cannot manufacture locally. The fleet exercises it immediately, since
every private consumer is currently in exactly this state.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the bug Something isn't working label Aug 20, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📚 Docs preview: https://codes.sota-shimozono.com/TestShards.jl/previews/PR83/

(updates on each push to this PR)

@codecov

codecov Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

…t has not established

Three problems with the first version, found by reviewing it against what its own CI
actually exercised.

1. NOTHING TESTED THE BRANCH. Every check was green, and not one of them reached the
   state the change exists for: `action-smoke` runs where the artifact IS present. I
   wrote "verifiable only in a real run" in the PR body, and that was a way of not
   looking — `action-smoke` already uses a local `./actions/upload-coverage`, so a PR
   can exercise its own version of the action. Naming an artifact that cannot exist
   reproduces the state exactly.

   And the thing under test is not the message. It is whether `continue-on-error` on a
   step of a COMPOSITE action is honoured at all, and whether `steps.<id>.outcome` is
   readable from a later step of the same composite. If either is not true, the action
   dies at the download exactly as before and the whole change is inert — with every
   check still green, because nothing looked. `action-absent-artifact` is now the only
   thing that would say so.

   This repository is PUBLIC, so its storage is free and its own CI can never reach that
   state by accident. The state has to be constructed.

2. THE MESSAGE ASSERTED A CAUSE. `outcome != 'success'` is every reason a download can
   fail, not only absence, so "does not exist" was a claim the step had not established
   — a network fault and a genuinely lost `-lcov` land in the same branch. It now says
   the download failed, names what usually does that, and tells the reader to check
   whether the run produced any artifacts at all before assuming the quota.

3. THE TRADE WAS HIDDEN BEHIND A TIDY DISTINCTION. "Absent is infrastructure, empty is a
   bug" reads cleanly and is not airtight: absent can also be a bug, because the
   workflow's `-lcov` upload is itself `continue-on-error`. So a real producer
   regression now degrades to a warning where it used to be red. That is accepted — a
   storage condition must not take down every consumer's test result — but it is written
   down as an accepted cost rather than defined away.

Also `${GITHUB_STEP_SUMMARY:-}`: the block runs under `set -u`, where an unbound
variable would kill the step in the middle of degrading gracefully.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sotashimozono

Copy link
Copy Markdown
Member Author

Closing unmerged — keeping the current behaviour.

What was measured here, so it does not have to be re-derived:

  • continue-on-error on a step of a composite action IS honoured, and steps.<id>.outcome is readable from a later step of the same composite. Measured, not assumed: the download failed with Artifact not found, the degrade branch ran, the job stayed green.
  • actions/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 quota while this job dies at step two.
  • 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 by naming an artifact that cannot exist.

Consequence of keeping things as they are, stated once: Coverage (upload) stays red on every private consumer until the billing period rolls over, since Actions storage is accrued in GigabyteHours and does not fall when artifacts are deleted.

@sotashimozono
sotashimozono deleted the fix/tolerate-absent-lcov branch August 20, 2026 04:41
sotashimozono added a commit that referenced this pull request Aug 25, 2026
…osed for lacking

#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) <noreply@anthropic.com>
sotashimozono added a commit that referenced this pull request Aug 25, 2026
…osed for lacking (#87)

#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) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant