From 9f652248dc9e1a88fbf24ab36f07e21f97e7ff89 Mon Sep 17 00:00:00 2001 From: Ty J Everett Date: Tue, 22 Sep 2026 18:57:59 -0700 Subject: [PATCH] fix(ci): report coverage for public contributor forks --- .github/workflows/ci.yml | 10 +++++++--- docs/reference/ci-performance.md | 11 +++++++++++ scripts/ci-orchestration.test.mjs | 17 +++++++++++++++++ 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82d459984..017ece2b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1111,8 +1111,12 @@ jobs: --lcov-directory .coverage-artifacts --target 90 + # Public fork PRs use the pinned action's tokenless mode. GitHub does not + # expose repository secrets to these pull_request runs; no privileged + # trigger or OIDC/write permission is needed. Their required codecov/patch + # status must be produced just like same-repository PRs. - name: Upload coverage to Codecov - if: steps.cov.outputs.has-coverage == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + if: steps.cov.outputs.has-coverage == 'true' uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -1129,7 +1133,7 @@ jobs: fail_ci_if_error: false - name: Wait for Codecov to merge the uploaded report - if: steps.cov.outputs.has-coverage == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + if: steps.cov.outputs.has-coverage == 'true' env: CODECOV_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} CODECOV_BUILD_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} @@ -1155,7 +1159,7 @@ jobs: done - name: Publish finalized Codecov notifications - if: steps.cov.outputs.has-coverage == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + if: steps.cov.outputs.has-coverage == 'true' uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/docs/reference/ci-performance.md b/docs/reference/ci-performance.md index 964bebb91..cd23d078a 100644 --- a/docs/reference/ci-performance.md +++ b/docs/reference/ci-performance.md @@ -88,3 +88,14 @@ gate independently checks scope outputs against each job result: selected jobs must succeed; missing, cancelled or skipped selected jobs fail the merge gate. Only genuinely unselected lanes and main's PR-only checks may be skipped. Do not infer full release acceptance from a green documentation-only push. + +## Contributor-fork coverage + +Public fork PRs upload the merged LCOV evidence through the pinned Codecov action's +[tokenless fork mode](https://github.com/codecov/codecov-action#v4-release). +GitHub withholds repository secrets from these ordinary `pull_request` jobs; +no privileged trigger or additional write/OIDC permission is granted. The uploader, +processing check and final notifications run for every nonempty report, so the +required `codecov/patch` status is available for contributors as well as maintainers. +The repository-owned 90% patch-coverage check remains the first blocking check +on the exact diff. diff --git a/scripts/ci-orchestration.test.mjs b/scripts/ci-orchestration.test.mjs index bd3f8d901..4a5082ea8 100644 --- a/scripts/ci-orchestration.test.mjs +++ b/scripts/ci-orchestration.test.mjs @@ -161,3 +161,20 @@ test('all selected execution jobs survive skipped ancestors and expose a strict assert.match(gate, /CI_NEEDS: \$\{\{ toJSON\(needs\) \}\}/) assert.match(gate, /run: node scripts\/ci-result-gate\.mjs/) }) + +test('fork PRs can produce the required Codecov status without a privileged workflow', () => { + const workflow = readFileSync(CI_PATH, 'utf8') + for (const step of [ + 'Upload coverage to Codecov', + 'Wait for Codecov to merge the uploaded report', + 'Publish finalized Codecov notifications' + ]) { + assert.ok( + workflow.includes( + ` - name: ${step}\n if: steps.cov.outputs.has-coverage == 'true'\n` + ) + ) + } + assert.doesNotMatch(workflow, /pull_request_target|id-token:\s*write/) + assert.match(workflow, /run: >-\n node scripts\/patch-coverage\.mjs/) +})