codecov/codecov-action failed to properly upload report on v4
codecov/codecov-action failed to properly upload report is the third of three near-identical sentences the v4 action can print, and the one it prints tells you how far the run got before something broke. The sentence carries no diagnosis of its own: what follows the colon is an @actions/exec message naming a binary and an exit code, and the reason that binary exited lives in the lines above it.

What this error means
A coverage step reports a failure to upload and usually does not turn the job red. The give-away is the word the sentence uses for the stage: create commit, create report or upload report. The action runs those three sub-commands of the Codecov binary in that order and only advances when the previous one exits zero, so the stage named in the failure is also a statement about what already succeeded. Read the prefix too. Warning: means the failure was routed through core.warning and the step stayed green; Error: means core.setFailed ran and the process was ended.
Warning: Codecov:
Failed to properly upload report: The process '/home/runner/work/_actions/codecov/codecov-action/b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238/dist/codecov' failed with exit code 1Three sentences, one chain, and what each one concedes
The v4 action runs the Codecov binary three times inside a chain of promises in src/index.ts. create-commit goes first. If it exits zero, create-report runs. If that exits zero, do-upload runs. Each call has its own catch, and each catch writes a sentence that names its own stage, so there are three failures wearing almost the same words.
That ordering is the whole value of the message. A report-creation failure means the commit was accepted by Codecov, which means the token and the slug were good enough to create one. An upload failure means both of those passed and the thing that broke is the transfer of the file. The table is the chain, in source order.
| Order | Sub-command | Sentence on failure | What the sentence concedes |
|---|---|---|---|
| 1 | create-commit | Codecov: Failed to properly create commit: | Nothing yet; this is the first call |
| 2 | create-report | Codecov: then Failed to properly create report: | The commit call exited zero |
| 3 | do-upload | Codecov: then Failed to properly upload report: | Commit and report both exited zero |
Common causes
The upload sub-command exited non-zero for a reason printed above
The literal cause of this exact sentence, and the only one it actually asserts. The binary ran, did something, and stopped. Its own last lines carry the reason, and they are usually a rejected token, a report the service refused, or a transfer that did not complete. In our experience this is where nearly all of the time goes, because the sentence reads like a diagnosis and is not one.
No coverage file existed, so the upload had nothing to carry
The stage before this one creates a report from the files it can find, and when it finds none it can still exit zero and leave the upload with an empty payload. The tell is the file count the binary logs before the failure. This is not a token problem and adding a token does not move it.
Nobody saw it for weeks because the step was green
Not a cause of the failure so much as a cause of the delay. With fail_ci_if_error unset, v4 prints a warning and continues, so the run is green and the annotation is one line in a step nobody opens. A coverage gate on a later pull request is usually what finally surfaces it.
The workflow is on v4 while the guidance you are reading is not
The sentence exists only in v4. Advice written for v3 talks about an uploader binary and a single failure, and advice written for v5 talks about a shell wrapper and a token chain; neither describes a three-stage chain. Matching the guidance to the major you are actually running removes a whole class of wrong turns.
How to fix it
Find the sub-command line, then read upward
- Expand the Codecov step and search it for
==> Running command. - The last of those lines names the sub-command that failed:
create-commit,create-reportordo-upload. - Read the binary output between that line and the failure sentence; the reason is in there, not in the sentence.
Decide deliberately whether a failed upload should fail the job
Set fail_ci_if_error explicitly rather than inheriting the absent default. True is right when a coverage gate can block a merge, because then the failure is visible the day it happens. False is right when some jobs genuinely produce no coverage, but write it down so the next reader knows it was a choice.
- uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: trueAssert the coverage file before the action runs
An upload with nothing to send is easier to diagnose at the place it went wrong. A one-line guard in the step before turns a confusing upload failure into an obvious missing-file failure, and costs nothing when the file is there.
- run: test -s coverage/lcov.info
- uses: codecov/codecov-action@v4
with:
files: ./coverage/lcov.infoSearch for the fragment, not for the line
- Do not search logs or issue trackers for
Codecov: Failed to properly upload report; those words are never adjacent in the emitted string. - Search for
Failed to properly upload report:on its own. - To tell v3 from v4 in an old log, search for
Failed to properly upload:as well; only v3 prints that form.
Why the sentence wraps onto a second line
The quoted log above is not corrupted and nobody reformatted it. In src/index.ts the second and third messages are template literals broken across source lines, so the string the action hands to the logger genuinely contains a newline followed by the source file's own indentation. The first message, from create-commit, is written on a single source line and therefore prints on a single line.
This matters because it breaks searching. Anyone searching for Codecov: Failed to properly upload report finds nothing, because those two fragments are never adjacent in the emitted text. Search for the fragment after the break on its own.
Failed to properly upload report: The part after the colon is not Codecov speaking
What follows the sentence is the message @actions/exec throws when a process it started exits non-zero: the tool path in single quotes, then the exit code. The tool path is the Codecov binary the action downloaded into its own action directory, which is why it reads as a long hash under _actions/codecov/codecov-action/. The exit code is the binary's, not the action's.
So the visible failure contains no reason at all, and the reason is in the binary's own output higher up in the same step. That output is where a missing token, a report with no files in it or a rejected slug actually announces itself. Treat this sentence as a pointer, scroll up, and read the last thing the binary said before it exited.
Whether the job goes red is a separate decision
Both routes go through one helper. setFailure in src/helpers.ts calls core.setFailed and then process.exit() when its failCi argument is true, and core.warning otherwise. That argument comes from isTrue(core.getInput('fail_ci_if_error')), and in the v4 action.yml the fail_ci_if_error input declares no default at all, so an unset input is an empty string and failCi is false.
The result is that the default behaviour of v4 is to print the failure as a warning and let the job pass. A repository can go weeks in that state, because nothing is red and the only symptom is a coverage graph that stopped moving. If coverage is load-bearing for your merges, set the input rather than relying on the absent default.
The wording tells you which major you are pinned to
These three sentences are a v4 artefact. In v3 the same catch reads Codecov: Failed to properly upload: with no report on the end and no chain of sub-commands behind it, because v3 ran the uploader once. In v5 the TypeScript entry point is gone entirely and the action is a shell wrapper, so none of these strings are reachable. A log carrying Failed to properly upload report is therefore a v4 run whatever the workflow file appears to say.
Why there is no recorded run on this page
Everything interesting here happens inside a binary the action downloads at run time from a service we do not operate, and the exit code in the message is that binary's verdict on one repository's token, slug and coverage files. A run of ours would record our repository failing in one of the three ways, present an exit code that is ours, and teach a reader to match a number that has nothing to do with theirs. The chain, the wrapping and the default are all read from the action's own source, which is where they are stable.
How to prevent it
- Pin the action to a major you have read the source of, so the sentences in your logs match the guidance you follow.
- Set
fail_ci_if_erroron every repository rather than leaving an absent default to decide. - Check that a coverage file exists in the step before the upload, so an empty report fails at its cause.
- Keep the
==> Running commandlines in mind when triaging: they are the only thing in the step that names the stage.
Frequently asked questions
What does "Failed to properly upload report" actually tell me?
@actions/exec message naming the binary path and its exit code, and the reason the binary exited is in its own output further up the step.Why is my Codecov step a warning instead of an error?
setFailure calls core.warning unless its failCi argument is true, and that argument is isTrue(core.getInput('fail_ci_if_error')). The v4 action.yml declares no default for that input, so leaving it out makes the value an empty string and the failure a warning. Set the input to true if you want the job to fail.Why does the message break across two lines in the log?
src/index.ts, so the string itself contains a newline and the file's indentation. The create-commit message is written on one source line and does not wrap. It is not a truncation and nothing is missing.Is this the same as "Failed to properly create report"?
create-report sub-command, one stage earlier, and it means the upload never started. Both are written by the same helper and look nearly identical, so the stage word is the only thing separating them.Related guides
References
- codecov/codecov-action v4: src/index.ts, the three sub-commands and their catch blocks
- codecov/codecov-action v4: src/helpers.ts, setFailure and the command logging
- @actions/exec toolrunner: the "failed with exit code" message appended after the colon
- kakkoyun/pkm-tool#27: the warning quoted here, wrap and indentation included
- mozilla/pontoon#3208: the same failure reported as a warning nobody noticed
- GitHub Actions documentation