Skip to content

ci(coverage): upload fork-PR coverage to Codecov via workflow_run#2009

Merged
JSONbored merged 1 commit into
mainfrom
claude/ci-fork-codecov-upload
Jul 1, 2026
Merged

ci(coverage): upload fork-PR coverage to Codecov via workflow_run#2009
JSONbored merged 1 commit into
mainfrom
claude/ci-fork-codecov-upload

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

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_TOKEN to fork-triggered workflows, so the token is empty, Codecov rejects the tokenless upload ({"message":"Token required because branch is protected"}), and fail_ci_if_error: true turns 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_run two-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 stashes coverage/lcov.info + reports/junit/vitest.xml + PR metadata as the fork-coverage artifact.
  • .github/workflows/codecov-fork-upload.yml (new) — runs on workflow_run of CI in the base-repo context (which can read secrets.CODECOV_TOKEN), downloads the fork's artifact, and uploads to Codecov with override_pr/commit/branch so 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_run job:

  • never checks out or runs fork code — it only downloads the artifact and passes metadata to codecov-action;
  • runs only for successful fork-PR CI runs (workflow_run.conclusion == 'success', event == 'pull_request', head_repository.full_name != github.repository);
  • passes fork-controlled values (branch name) to the shell via env, never ${{ }} interpolation, in ci.yml;
  • re-validates the PR number (digits), head sha (hex), and branch (safe ref charset) before use;
  • requests only contents: read + actions: read.

Validation

  • npm run actionlint — clean (verified it lints the new file: an injected error is caught, then passes after revert).
  • This PR is a same-repo PR, so its own coverage still uploads inline (the fork path is skipped) — no regression to the current flow.

Notes

workflow_run workflows execute from the default branch, so the new uploader only becomes active after this merges to main — 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.

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.
@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label Jul 1, 2026
@JSONbored JSONbored self-assigned this Jul 1, 2026
@JSONbored JSONbored merged commit e71612e into main Jul 1, 2026
6 of 8 checks passed
@JSONbored JSONbored deleted the claude/ci-fork-codecov-upload branch July 1, 2026 08:39
@github-project-automation github-project-automation Bot moved this from Todo to Done in gittensory - v1 roadmap Jul 1, 2026

permissions:
contents: read
actions: read

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 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>

@superagent-security superagent-security Bot added the pr:flagged PR flagged for review by security analysis. label Jul 1, 2026
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

❌ 1 Tests Failed:

Tests completed Failed Passed Skipped
5542 1 5541 13
View the top 1 failed test(s) by shortest run time
test/unit/codecov-policy.test.ts > Codecov policy > fails closed when the backend coverage report is missing or cannot upload
Stack Traces | 0.0738s run time
AssertionError: expected '${{ success() && (github.event_name =…' to be '${{ success() && (github.event_name =…' // Object.is equality

Expected: "${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') && github.event.pull_request.head.repo.fork != true }}"
Received: "${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') }}"

 ❯ test/unit/codecov-policy.test.ts:55:27

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:flagged PR flagged for review by security analysis. size:XS This PR changes 0-9 lines, ignoring generated files.

Projects

No open projects
Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant