From dd70688b5ea3fcc6b9adbb78aef3e906a83f9990 Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 1 Jul 2026 05:16:00 -0700 Subject: [PATCH] fix(ci): use a colon-prefixed branch for fork tokenless Codecov uploads A real fork PR rebase (gittensory#2496-equivalent traffic) hit "Token required because branch is protected" on the tokenless upload. Per Codecov's own docs, only a branch string with a colon-separated prefix (e.g. forkname:branch) is recognized as unprotected/tokenless-eligible; a bare branch name looks like it could be a real branch on the base repo and gets rejected even with no token configured anywhere. codecov-cli's own auto-detection never adds this prefix (confirmed in its source), so override_branch must supply it explicitly. override_commit/override_pr are unaffected -- the protected-branch check only inspects the branch string. --- .github/workflows/ci.yml | 11 +++++++++-- test/unit/codecov-policy.test.ts | 10 +++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 633581fd67..012ffd17be 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,13 +159,20 @@ jobs: # actual commit, not a synthetic merge), so HEAD has one parent and that recovery can't fire. Pass # the same explicit overrides as the trusted upload above so the report attaches to the real PR head, # not a merge sha GitHub's PR checks list has no reason to display. + # + # override_branch is prefixed with the fork owner (owner:branch) -- per Codecov's own docs, only a + # branch string containing a colon is recognized as "unprotected" and eligible for a tokenless + # upload; a bare branch name looks like it could be a real (possibly protected) branch on the base + # repo and gets rejected with "Token required because branch is protected" even with no token + # configured at all. codecov-cli's own auto-detection never adds this prefix either (verified in its + # source), so this must be supplied explicitly. - name: Upload coverage to Codecov (fork PR tokenless) if: ${{ success() && needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.fork == true }} uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: files: ./coverage/lcov.info disable_search: true - override_branch: ${{ github.event.pull_request.head.ref }} + override_branch: ${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }} override_commit: ${{ github.event.pull_request.head.sha }} override_pr: ${{ github.event.pull_request.number }} fail_ci_if_error: true @@ -191,7 +198,7 @@ jobs: files: ./reports/junit/vitest.xml report_type: test_results disable_search: true - override_branch: ${{ github.event.pull_request.head.ref }} + override_branch: ${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }} override_commit: ${{ github.event.pull_request.head.sha }} override_pr: ${{ github.event.pull_request.number }} fail_ci_if_error: false diff --git a/test/unit/codecov-policy.test.ts b/test/unit/codecov-policy.test.ts index c0ef6edbd3..ac4423a719 100644 --- a/test/unit/codecov-policy.test.ts +++ b/test/unit/codecov-policy.test.ts @@ -100,9 +100,16 @@ describe("Codecov policy", () => { // recover the real head sha assumes a 2-parent merge commit at HEAD -- which our checkout step (it // fetches github.event.pull_request.head.sha directly) never produces. Without an explicit override, // the report would attach to a sha GitHub's PR checks list has no reason to ever display. - expect(forkCoverageWith.override_branch).toBe("${{ github.event.pull_request.head.ref }}"); expect(forkCoverageWith.override_commit).toBe("${{ github.event.pull_request.head.sha }}"); expect(forkCoverageWith.override_pr).toBe("${{ github.event.pull_request.number }}"); + // Codecov only treats a branch as "unprotected" (eligible for tokenless upload) when its name has a + // colon-separated prefix; a bare branch name gets rejected with "Token required because branch is + // protected" even with no token configured anywhere. codecov-cli's own auto-detection never adds + // this prefix, so it must be supplied explicitly -- omitting it is exactly the regression this guards. + expect(String(forkCoverageWith.override_branch)).toContain(":"); + expect(forkCoverageWith.override_branch).toBe( + "${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }}", + ); const forkTestResultsUpload = steps.find( (step) => step.name === "Upload Vitest results to Codecov (fork PR tokenless)", @@ -113,6 +120,7 @@ describe("Codecov policy", () => { expect(forkTestResultsWith.report_type).toBe("test_results"); expect(forkTestResultsWith.fail_ci_if_error).toBe(false); expect(forkTestResultsWith.override_commit).toBe("${{ github.event.pull_request.head.sha }}"); + expect(String(forkTestResultsWith.override_branch)).toContain(":"); // The trusted (token) path must still explicitly exclude forks -- it must never see the token env // used, and the two paths must be mutually exclusive so a fork PR never double-uploads.