commit fb6ed62786fc30e8017f30ea6962c3a2bc2b5c4c Author: Joshua Hale Date: Mon Mar 30 11:33:34 2026 +0100 Add composite action for code coverage upload Wraps the PUT /repos/{owner}/{repo}/code-coverage/report API call, handling gzip/base64 encoding, PR vs push event detection, SHA/ref resolution, and PR number lookup automatically. Supports both pull_request-triggered and push-only workflows. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> diff --git a/README.md b/README.md new file mode 100644 index 0000000..b4f1a63 --- /dev/null +++ b/README.md @@ -0,0 +1,100 @@ +# Upload Code Coverage Action + +Upload a Cobertura XML coverage report to GitHub's code coverage API. + +## Usage + +```yaml +- uses: code-quality-org/upload-code-coverage-action@main + with: + file: cobertura.xml + language: Java + label: code-coverage/jacoco +``` + +The action handles everything else automatically: gzip/base64 encoding, resolving the correct commit SHA and ref, detecting PR number (from both `pull_request` and `push` events), and calling the upload API. + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| `file` | Yes | Path to the Cobertura XML coverage report | +| `language` | Yes | Linguist language name (e.g. `Java`, `Go`, `Python`) | +| `label` | Yes | Label for the report (e.g. `code-coverage/jacoco`) | +| `token` | No | GitHub token (defaults to `github.token`) | + +## Permissions + +The calling workflow or job must grant `security-events: write`. The action cannot declare this itself. + +```yaml +permissions: + contents: read + # Required for coverage upload. Will be reduced to code-quality:write + # once that permission scope is available (github/code-scanning#22168). + security-events: write +``` + +For push-only workflows where the action looks up PR numbers via `gh pr list`, also add `pull-requests: read`. + +## Event handling + +The action auto-detects the event type and resolves the correct values: + +- **`pull_request` / `pull_request_target`**: Uses the PR head SHA and ref (not the merge commit), and includes the PR number. +- **`push`**: Uses `github.sha` and `github.ref`, and looks up whether the branch has an open PR via `gh pr list`. + +This means it works with both patterns — workflows triggered by `pull_request` and push-only workflows that serve PRs via branch pushes. + +## Full example (separate upload job) + +```yaml +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + + # ... build and generate cobertura.xml ... + + - uses: actions/upload-artifact@v4 + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + with: + name: cobertura-report + path: cobertura.xml + + upload-coverage: + needs: build + if: ${{ !cancelled() && needs.build.result == 'success' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }} + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/download-artifact@v4 + with: + name: cobertura-report + + - uses: code-quality-org/upload-code-coverage-action@main + with: + file: cobertura.xml + language: Java + label: code-coverage/jacoco +``` + +## Prerequisites + +The repository must have the following feature flags enabled: +- `code_coverage_upload_api` (org-level) +- `code_coverage_processing` (repo-level) diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..f0870a9 --- /dev/null +++ b/action.yml @@ -0,0 +1,78 @@ +name: 'Upload Code Coverage' +description: 'Upload a Cobertura XML coverage report to GitHub code coverage API' + +inputs: + file: + description: 'Path to the Cobertura XML coverage report' + required: true + language: + description: 'Linguist language name (e.g. "Java", "Go", "Python")' + required: true + label: + description: 'Label for the coverage report (e.g. "code-coverage/jacoco")' + required: true + token: + description: 'GitHub token with security-events:write permission' + required: false + default: ${{ github.token }} + +runs: + using: composite + steps: + - name: Upload coverage report + shell: bash + env: + GH_TOKEN: ${{ inputs.token }} + INPUT_FILE: ${{ inputs.file }} + INPUT_LANGUAGE: ${{ inputs.language }} + INPUT_LABEL: ${{ inputs.label }} + run: | + set -euo pipefail + + if [ ! -f "$INPUT_FILE" ]; then + echo "::error::Coverage file not found: $INPUT_FILE" + exit 1 + fi + + # Resolve the commit SHA and ref. On pull_request events, github.sha + # and github.ref point to the merge commit — use the PR head instead. + if [ "${{ github.event_name }}" = "pull_request" ] || [ "${{ github.event_name }}" = "pull_request_target" ]; then + COMMIT_OID="${{ github.event.pull_request.head.sha }}" + REF="refs/heads/${{ github.event.pull_request.head.ref }}" + PR_NUMBER="${{ github.event.pull_request.number }}" + else + COMMIT_OID="${{ github.sha }}" + REF="${{ github.ref }}" + # For push events, check if this branch has an open PR. + PR_NUMBER=$(gh pr list \ + --repo "${{ github.repository }}" \ + --head "${{ github.ref_name }}" \ + --state open \ + --json number \ + --jq '.[0].number // empty' 2>/dev/null || true) + fi + + # Gzip and base64-encode the report. We write to files and use jq + # --rawfile to avoid hitting the OS argument length limit on large + # coverage reports. + gzip -c "$INPUT_FILE" | base64 -w 0 > __coverage_b64.txt + + jq -n \ + --arg commit_oid "$COMMIT_OID" \ + --arg ref "$REF" \ + --rawfile coverage_report __coverage_b64.txt \ + --arg language_name "$INPUT_LANGUAGE" \ + --arg label "$INPUT_LABEL" \ + '{commit_oid: $commit_oid, ref: $ref, coverage_report: $coverage_report, language_name: $language_name, label: $label}' \ + > __body.json + + if [ -n "${PR_NUMBER:-}" ]; then + jq --argjson pr_number "$PR_NUMBER" \ + '. + {pull_request_number: $pr_number}' __body.json > __body_tmp.json \ + && mv __body_tmp.json __body.json + fi + + gh api --method PUT "/repos/${{ github.repository }}/code-coverage/report" \ + --input __body.json + + rm -f __coverage_b64.txt __body.json