ci(coverage): upload fork-PR coverage to Codecov via workflow_run#2009
Merged
Conversation
Fork PRs run without access to secrets, so `secrets.CODECOV_TOKEN` is empty and
the inline codecov upload fails ("Token required because branch is protected")
with `fail_ci_if_error: true`, turning every backend-touching fork PR red through
no fault of the contributor — and the auto-close gate then closes it.
Keep the inline upload for trusted contexts (push + same-repo PRs) but skip it on
forks; instead ci.yml stashes the coverage report + PR metadata as the
`fork-coverage` artifact, and a new `Codecov fork upload` workflow runs on
workflow_run in the base-repo context (which CAN read the token) to perform the
upload. codecov/patch stays enforced on fork PRs.
Security: the privileged workflow never checks out or runs fork code — it only
downloads the artifact and passes metadata to codecov-action. It runs only for
successful fork-PR CI runs, fork-controlled values reach the shell via env (never
`${{ }}` interpolation), and the PR number / sha / branch are re-validated against
a strict charset before use. Validated with actionlint.
|
|
||
| permissions: | ||
| contents: read | ||
| actions: read |
There was a problem hiding this comment.
workflow_run trigger enables privileged execution downstream of untrusted fork PRs
The workflow_run trigger runs in the base-repo context with access to secrets, indirectly reachable by fork PRs via the CI workflow artifact chain.
Consider replacing the upload with a GitHub App, or add a deployment environment with required reviewers for defense-in-depth.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name=".github/workflows/codecov-fork-upload.yml">
<violation number="1" location=".github/workflows/codecov-fork-upload.yml:19">
<priority>P1</priority>
<title>workflow_run trigger enables privileged execution downstream of untrusted fork PRs</title>
<evidence>The codecov-fork-upload.yml workflow uses a workflow_run trigger, which executes in the privileged base-repo context with access to secrets.CODECOV_TOKEN. This trigger is indirectly reachable by any fork PR that completes the CI workflow. While the workflow includes mitigations (no checkout, strict charset validation of artifact metadata, minimal permissions), the workflow_run pattern remains the highest-risk trigger in GitHub Actions. A bypass in the validation logic or in actions/download-artifact could allow attacker-controlled values to reach the privileged context.</evidence>
<recommendation>Consider replacing the privileged workflow_run upload with a GitHub App that listens for check_run or workflow_run webhook events and uploads to Codecov from outside Actions. If keeping workflow_run, add a deployment environment with required reviewers to the upload job for defense-in-depth.</recommendation>
</violation>
</file>
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
24 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fork PRs (e.g. #2003) fail CI in the coverage-upload step — not in the contributor's code. GitHub does not expose
secrets.CODECOV_TOKENto fork-triggered workflows, so the token is empty, Codecov rejects the tokenless upload ({"message":"Token required because branch is protected"}), andfail_ci_if_error: trueturns that into a red run. Since the gate auto-closes contributor PRs on red CI, this false-fails every backend-touching fork PR.This fixes it with Codecov's recommended
workflow_runtwo-stage pattern, so 99% patch coverage stays enforced on fork PRs.What changed
.github/workflows/ci.yml— the inline Codecov uploads now run only for trusted contexts (push + same-repo PRs, where the token exists;head.repo.fork != true). For fork PRs it instead stashescoverage/lcov.info+reports/junit/vitest.xml+ PR metadata as thefork-coverageartifact..github/workflows/codecov-fork-upload.yml(new) — runs onworkflow_runofCIin the base-repo context (which can readsecrets.CODECOV_TOKEN), downloads the fork's artifact, and uploads to Codecov withoverride_pr/commit/branchso it attributes to the fork PR's head commit.Why
So fork contributors aren't false-failed/auto-closed by an infra limitation, without weakening the coverage gate.
Security
The privileged
workflow_runjob:codecov-action;workflow_run.conclusion == 'success',event == 'pull_request',head_repository.full_name != github.repository);env, never${{ }}interpolation, in ci.yml;contents: read+actions: read.Validation
npm run actionlint— clean (verified it lints the new file: an injected error is caught, then passes after revert).Notes
workflow_runworkflows execute from the default branch, so the new uploader only becomes active after this merges tomain— it cannot run on this PR itself. That's inherent to the pattern. This touches CI config (a guarded path), so it's for maintainer review, not auto-merge.