Skip to content
Latchkey LogoLatchkey home

codecov/codecov-action: the token it sends, OIDC, and uploads that fail green

codecov/codecov-action uploads coverage reports from a GitHub Actions job to Codecov by downloading the Codecov CLI, verifying its signature and running upload-coverage. On v7, authenticate with a repository token, with use_oidc: true and id-token: write, or tokenless where Codecov allows it, and set fail_ci_if_error: true, because by default a failed upload leaves the job green.

The Codecov step is usually the last line of a test job and the one nobody looks at. That is the problem: it is designed to stay green when the upload fails, so a missing token, an expired key or an empty report shows up weeks later as a coverage badge that stopped moving.

Under the action is a Codecov CLI that it downloads and verifies with GPG, so a failure can come from the token, the signing key or the upload itself. Two errors have their own pages: failed to properly upload report and Codecov rate limit reached.

How v7.1.1 picks a credential: OIDC unless a fork, then CODECOV_TOKEN env, then token input, else tokenless
The credential order in action.yml at v7.1.1, and what Codecov does with an upload that has none. Every failure after it is soft unless fail_ci_if_error is true.

Name the report, fail on error

Run your tests with coverage output first, then hand the report to the action. Naming the file in files and setting disable_search: true stops the CLI from also picking up stray reports elsewhere in the workspace. The action needs actions/checkout to have run, and bash, curl, git and gpg on the runner; GitHub-hosted runners have all four, and a container job without them fails at the first step with Error: The following required dependencies are missing: followed by the list.

.github/workflows/test.yml
name: test
on:
  push:
    branches: [main]
  pull_request:
permissions:
  contents: read
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
        with:
          node-version: 22
      - run: npm ci
      - run: npm run test:coverage   # writes coverage/lcov.info
      - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}
          files: ./coverage/lcov.info
          disable_search: true
          flags: unit
          fail_ci_if_error: true

What the action actually runs

v7 is a composite action. It checks for its dependencies, marks the workspace as a git safe directory, detects whether the run is a pull request from a fork, fetches an OIDC token if asked, picks a credential, and then runs dist/codecov.sh. That script downloads the CLI from cli.codecov.io, imports Codecov's PGP key from keybase.io/codecovsecops, checks the signature and the SHA-256 sum, prints Token length: followed by a number, and runs the CLI.

Every failure in that script goes through one function, exit_if_error, which prints the message and exits 1 only when fail_ci_if_error is true. The default is false. So Could not verify signature. Please contact Codecov if problem continues and Failed to run upload-coverage both print in red and leave the step green. The Token length: line is the fastest check you have: 0 means no credential reached the CLI.

Which token the action sends, and when it needs one

The order in action.yml is fixed. If use_oidc is true and the run is not from a fork, the OIDC token wins and any token you passed is ignored. Otherwise a CODECOV_TOKEN environment variable on the step is used, then the token input. With none of those, the upload goes out tokenless, and for a fork pull request the action sets the branch to the fork's head label, such as contributor:fix-typo, which is how Codecov recognizes a fork upload.

Whether Codecov accepts a tokenless upload is decided on its side. Its documentation, read 2026-11-12, says private repositories always need a token. A public repository needs one only for a commit on a protected branch, and not at all if the organization has switched token authentication off in the Global Upload Token settings; new organizations start with tokens Not required, existing ones with Required. Tokenless uploads share a Codecov-wide rate limit, which is what the rate limit page is about.

Repository and eventWhat to configure
Private repository, any eventRepository token or use_oidc: true
Public repository, push to a protected branchToken or OIDC, unless the organization made tokens Not required
Public repository, unprotected branchNothing required; a token avoids the shared rate limit
Pull request from a forkNothing: secrets and OIDC are skipped, the upload goes tokenless
Dependabot pull requestAdd CODECOV_TOKEN as a Dependabot secret

OIDC instead of a stored secret

With use_oidc: true the action asks GitHub for an ID token whose audience is the url input, or https://codecov.io by default, and sends that instead of a long-lived secret. The job needs id-token: write, and because setting permissions replaces the defaults, list contents: read too. If the token request fails, the id-token write permission page covers where the line has to go.

On a fork pull request the action skips the OIDC request entirely, because action.yml checks for a fork before asking, and the upload falls through to tokenless.

.github/workflows/test.yml
permissions:
  contents: read
  id-token: write
steps:
  - uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
    with:
      use_oidc: true
      files: ./coverage/lcov.info
      disable_search: true
      fail_ci_if_error: true

fail_ci_if_error, flags and the inputs that matter

v7.1.1 declares 50 inputs, most of them overrides for unusual CI setups. These are the ones that change outcomes in a normal workflow. Flags tag an upload so Codecov can report coverage per group, which in a matrix usually means one flag per OS or language version, for example flags: ${{ matrix.os }}.

InputDefaultWhy you would set it
fail_ci_if_errorfalseMake any failure in the script fail the step
tokennoneRepository upload token
use_oidcfalseAuthenticate with GitHub OIDC instead of a token
filesnoneComma-separated reports, added to what search finds
disable_searchfalseUpload only what files names
flagsnoneComma-separated flags to group coverage
namenoneUpload name shown in the Codecov UI
report_typecoveragetest_results to upload JUnit-style test results instead
handle_no_reports_foundfalseDo not raise when no report is found
cleanupfalseDownload the CLI to a temporary directory and remove it (v7.1.0 and later)
verbosefalseVerbose CLI logging when debugging an upload

Token, GPG and upload errors

failed to properly upload report and its siblings name the CLI stage that broke; the line to read is above it, and the upload report page walks through it. A throttled tokenless upload is on the rate limit page. Token required because branch is protected appears in the titles of issues #1580, #1653 and #1738: Codecov wants a token for that commit and none arrived, so check the Token length: line before the secret itself.

GPG failures have a specific recent cause. Codecov lost control of its keybase.io/codecovsecurity account, deleted it, and moved the key to keybase.io/codecovsecops (issue #1956). The fix is a release that reads the new account: v7.0.0 and later, or the copies published as v6.0.2 and v5.5.5. A workflow pinned to an older SHA keeps fetching from the account that no longer exists, and with fail_ci_if_error unset it says so only in the log.

Terminal
# Find the pinned codecov-action refs that predate the key move
grep -rn 'codecov/codecov-action@' .github/workflows/
# Then read the token line in the last run's log
gh run view <run-id> --log | grep 'Token length'

Which majors need workflow edits

v4 removed several inputs and v5 renamed file to files and plugin to plugins, so those two need workflow edits. v6 and v7 changed the runtime and the signing key, not the inputs.

MajorChange
v4Uploads through the Codecov CLI; tokenless unsupported except for fork pull requests; several inputs removed
v5CLI wrapped by codecov/wrapper; file became files, plugin became plugins; new inputs such as binary, report_type, skip_validation; token opt-out for public repositories
v6Node 24 support
v7PGP key fetched from keybase.io/codecovsecops; v7.1.0 adds cleanup

Key takeaways

  • Failures are soft by default: set fail_ci_if_error: true or a broken upload stays green.
  • Credential order: OIDC (not on forks), then CODECOV_TOKEN env, then token, then none.
  • Private repositories always need a token; public ones depend on branch protection and the org setting.
  • Pins older than v7.0.0, v6.0.2 or v5.5.5 fetch the PGP key from a deleted keybase account.

Frequently asked questions

Is there a way to make codecov-action work with a PR from a fork?
Yes, and since v4 it works without secrets: on a fork pull request the action sends no token, sets the branch to the fork's head label, and Codecov accepts it as a tokenless fork upload. Secrets and OIDC are not available to fork runs, so do not try to pass them.
I have already configured token, but it says Token required because branch is protected. Why?
Look at the Token length: line in the step log. If it is 0, the secret did not reach the step: Dependabot runs read Dependabot secrets, fork runs get none, and a typo in the secret name yields an empty string. If it is not 0, check the token belongs to this repository or is the organization's global token.
How do I migrate from v4 to v5 and later?
Rename file to files and plugin to plugins, then pin the new SHA. From v5 to v7 no input was removed, so the rest of the step carries over; v6 needs a runner that supports Node 24, and v7 is the line that reads Codecov's new PGP key location.

References

Coverage runs on every push. Latchkey bills it at $0.0025/min at 2 vCPU. Start free → 30-day trial · No credit card