Skip to content

Coverage comment

Coverage comment #80

name: Coverage comment
# Fork PRs only: same-repo PRs get the cobertura table from ci.yml directly
# (scripts/coverage-pr-comment.sh). This workflow posts the PR comment using
# the base repository's GITHUB_TOKEN after CI uploads the coverage-report
# artifact (fork tokens are read-only on the base repo).
#
# Security: this workflow must NOT execute code from the PR. It checks out
# only the base default branch for the comment script, downloads the CI
# artifact, and comments via the GitHub API.
on:
workflow_run:
workflows: [CI]
types: [completed]
permissions:
actions: read # download artifacts from the CI run
contents: read
pull-requests: write
issues: write # PR comments use the issues comments API
jobs:
comment:
# Same-repo PRs already posted from ci.yml and do not upload coverage-report.
# Only fork PRs upload the artifact (see ci.yml head.repo.full_name check).
if: >
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_repository.full_name != github.repository
runs-on: ubuntu-latest
steps:
# Base-repo default branch only — not the PR head.
- name: Checkout comment script (base repo)
uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
sparse-checkout: |
scripts/coverage-pr-comment.sh
sparse-checkout-cone-mode: false
- name: Download coverage artifact
uses: actions/download-artifact@v4
with:
name: coverage-report
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: .
- name: Resolve pull request number
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
# Often empty for fork (cross-repo) PRs.
PR_FROM_EVENT: ${{ github.event.workflow_run.pull_requests[0].number }}
run: |
set -euo pipefail
pr=""
# 1) Preferred: written by CI from github.event.pull_request.number
if [ -f pr-number.txt ]; then
pr="$(tr -d '[:space:]' < pr-number.txt || true)"
if [[ -n "$pr" && "$pr" =~ ^[0-9]+$ ]]; then
echo "Resolved PR from artifact pr-number.txt: #${pr}"
else
echo "Ignoring invalid pr-number.txt content: ${pr:-<empty>}"
pr=""
fi
fi
# 2) workflow_run.pull_requests (often empty for forks).
if [ -z "$pr" ] && [ -n "${PR_FROM_EVENT:-}" ]; then
pr="$PR_FROM_EVENT"
echo "Resolved PR from workflow_run.pull_requests: #${pr}"
fi
# 3) head owner:branch listing.
if [ -z "$pr" ] && [ -n "${HEAD_REPO:-}" ] && [ -n "${HEAD_BRANCH:-}" ]; then
head_owner="${HEAD_REPO%%/*}"
pr="$(gh api \
-H "Accept: application/vnd.github+json" \
"repos/${REPO}/pulls?state=open&head=${head_owner}:${HEAD_BRANCH}" \
--jq '.[0].number // empty')"
if [ -n "$pr" ]; then
echo "Resolved PR from head=${head_owner}:${HEAD_BRANCH}: #${pr}"
fi
fi
# 4) commits/{sha}/pulls (often empty for fork head SHAs).
if [ -z "$pr" ]; then
pr="$(gh api \
-H "Accept: application/vnd.github+json" \
"repos/${REPO}/commits/${HEAD_SHA}/pulls" \
--jq '.[0].number // empty')"
if [ -n "$pr" ]; then
echo "Resolved PR from commits/${HEAD_SHA}/pulls: #${pr}"
fi
fi
if [ -z "$pr" ]; then
echo "No pull request found for sha=${HEAD_SHA} head=${HEAD_REPO:-?}:${HEAD_BRANCH:-?}; skipping comment."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if ! [[ "$pr" =~ ^[0-9]+$ ]]; then
echo "Refusing non-numeric PR number: ${pr}"
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "number=${pr}" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
echo "Commenting on PR #${pr}"
- name: Coverage PR comment
if: steps.pr.outputs.skip != 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ steps.pr.outputs.number }}
GITHUB_SHA: ${{ github.event.workflow_run.head_sha }}
COVERAGE_XML: coverage.xml
MIN_COVERAGE: "75"
run: |
set -euo pipefail
test -f coverage.xml
chmod +x scripts/coverage-pr-comment.sh
scripts/coverage-pr-comment.sh