Skip to content

Push to default branch attributes coverage to an unrelated fork PR with a same-named head branch #14

Closed

Description

@glen-84

Summary

On push events, the action associates an open PR via gh pr list --head "${{ github.ref_name }}". That matches by head-branch name across all repositories, so an open cross-repo (fork) PR whose head branch happens to be named the same as the pushed branch gets selected. Since upload_coverage.py prefers pull_request_number over ref, the push's coverage is filed under that unrelated PR instead of the branch, and the branch baseline is never recorded.

Impact

This reliably breaks the default branch baseline: if any contributor has an open PR from their fork's main branch (head branch main), every push to your main is attributed to that PR. The default-branch coverage never populates, so every other PR's coverage comment shows "Coverage data for the main branch is not yet available."

Root cause

action.yml (push path):

else
  COMMIT_OID="${{ github.sha }}"
  REF="${{ github.ref }}"
  PR_NUMBER=$(gh pr list \
    --repo "$GITHUB_REPOSITORY" \
    --head "${{ github.ref_name }}" \   # not scoped to the base repo's own head branches
    --state open \
    --json number \
    --jq '.[0].number // empty' 2>/dev/null || true)
fi

--repo only selects which repository's PR list to query; a fork PR is still part of that list (it targets the base repo, only its head branch lives in the fork). --head <branch> matches the head-branch name regardless of where the head lives, so a fork PR with head branch <branch> is returned. Then upload_coverage.py:

if pr_number:
    payload["pull_request_number"] = int(pr_number)
elif ref:
    payload["ref"] = ref

pull_request_number wins, so the ref (the branch) is dropped from the payload.

Reproduction

  1. The repo has an open PR from a fork whose head branch is named main (head someuser:main → you:main).
  2. A push to main (the default branch) triggers the upload.
  3. The action log shows:
Upload parameters
  commit_oid: <sha>
  ref: refs/heads/main
  pr_number: <the unrelated fork PR number>
Coverage report uploaded successfully.
  1. Coverage is attributed to that PR; the main baseline stays empty and PRs report "Coverage data for the main branch is not yet available."

Confirmed that gh pr list --repo OWNER/REPO --head main --state open returns the fork PR (isCrossRepository: true), so the existing --repo argument does not prevent this.

Suggested fix

Scope the PR lookup to head branches in the base repository itself, so fork PRs aren't matched:

           COMMIT_OID="${{ github.sha }}"
           REF="${{ github.ref }}"
+          REPO_OWNER="${GITHUB_REPOSITORY%%/*}"
+          REPO_NAME="${GITHUB_REPOSITORY#*/}"
           PR_NUMBER=$(gh pr list \
             --repo "$GITHUB_REPOSITORY" \
             --head "${{ github.ref_name }}" \
             --state open \
-            --json number \
-            --jq '.[0].number // empty' 2>/dev/null || true)
+            --json number,headRepositoryOwner,headRepository \
+            --jq "[.[] | select(.headRepositoryOwner.login == \"$REPO_OWNER\" and .headRepository.name == \"$REPO_NAME\")] | .[0].number // empty" 2>/dev/null || true)

Alternatives: skip PR association entirely when the pushed ref is the default branch, or prefer ref over pull_request_number for push events.

Drafted by Claude (Anthropic AI assistant).

Activity

  1. jiridanek commented on Aug 6, 2026

    @jiridanek

    This is the same gh pr list --repo "$GITHUB_REPOSITORY" --head "${{ github.ref_name }}" line (action.yml, v1.4.1) that also has a script-injection footgun via the interpolated ref name, filed as #26. Both would be resolved together by moving the branch/ref values into env: and scoping the PR lookup by head repository (e.g. GraphQL headRepositoryOwner/headRepository).

    Confirming the misattribution reproduces on v1.4.1 for the GitHub code-coverage upload path: a push-to-default run gets its coverage attached to an unrelated fork PR whose head branch shares the pushed branch's name, so the branch baseline isn't recorded and PR deltas can't compute.

  2. joshhale commented on Sep 23, 2026

    @joshhale
    Collaborator

    Thanks for the detailed report and reproduction, and apologies that we did not respond sooner. We have confirmed that this is a valid bug and that it remains present in v1.4.2.

    The PR lookup currently matches only the head branch name. As you described, this can select an unrelated fork PR whose branch happens to have the same name as the pushed branch. The resulting coverage is then associated with that PR instead of updating the branch baseline.

    We have filed work to correct this behavior. The intended fix is to:

    • always treat a default-branch push as branch coverage, without attempting PR discovery;
    • scope PR lookup for other branch pushes to the head repository owner, repository name, and branch name;
    • avoid selecting an arbitrary result when multiple candidates exist.

    This logic is also involved in the shell-injection issue reported in #26, so it makes sense to address both problems in the same change and add regression coverage for each.

    We do not yet have a release date, but we will keep this issue open and update it when the fix is available. Thanks again for the unusually thorough investigation. It made the failure mode and impact very clear.

  3. cassiomarques commented on Oct 1, 2026

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions