Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions .github/scripts/coverage-pr-comment.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/usr/bin/env bash
#
# Compare two LCOV tracefiles. When the line-coverage delta is significant,
# post a fresh PR comment with the report. On every push that moves
# coverage, a new comment appears — no in-place update, no deletion of
# prior bot comments. On fork PRs the `GITHUB_TOKEN` is read-only, so the
# comment POST is best-effort; the full report is always emitted to the
# job summary so the data is visible either way.
#
# Inputs (env):
# GH_TOKEN — GitHub token (provided by Actions).
# PR_NUMBER — Pull request number.
# REPO — `<owner>/<name>` of the repository.
# BASE_SHA — Base commit SHA (display only).
# HEAD_SHA — PR head commit SHA (display only).
#
# Inputs (args): <base.lcov> <pr.lcov>
#
# Negligible threshold: the change is "negligible" when both the
# line-coverage percentage delta is under LINES_EPSILON AND the
# covered-line count delta is under HIT_EPSILON.
set -euo pipefail

LINES_EPSILON=${LINES_EPSILON:-0.05}
HIT_EPSILON=${HIT_EPSILON:-2}
MARKER='<!-- coverage-bot:v1 -->'

if [[ $# -ne 2 ]]; then
echo "usage: $0 <base.lcov> <pr.lcov>" >&2
exit 2
fi

BASE_LCOV=$1
PR_LCOV=$2
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

load_totals() {
local lcov=$1 prefix=$2
# shellcheck disable=SC2046
eval $("$SCRIPT_DIR/coverage-totals.sh" "$lcov" | sed "s/^/${prefix}_/")
}
load_totals "$BASE_LCOV" BASE
load_totals "$PR_LCOV" PR

delta_pct() { awk -v a="$1" -v b="$2" 'BEGIN { printf "%+.2f", b - a }'; }
delta_int() { awk -v a="$1" -v b="$2" 'BEGIN { printf "%+d", b - a }'; }
abs_lt() { awk -v a="$1" -v b="$2" 'BEGIN { if ((a < 0 ? -a : a) < b) print 1; else print 0 }'; }

LINES_DELTA=$(delta_pct "$BASE_LINES_PCT" "$PR_LINES_PCT")
LINES_HIT_DELTA=$(delta_int "$BASE_LINES_HIT" "$PR_LINES_HIT")
FUNCTIONS_DELTA=$(delta_pct "$BASE_FUNCTIONS_PCT" "$PR_FUNCTIONS_PCT")
FUNCTIONS_HIT_DELTA=$(delta_int "$BASE_FUNCTIONS_HIT" "$PR_FUNCTIONS_HIT")
BRANCHES_DELTA=$(delta_pct "$BASE_BRANCHES_PCT" "$PR_BRANCHES_PCT")
BRANCHES_HIT_DELTA=$(delta_int "$BASE_BRANCHES_HIT" "$PR_BRANCHES_HIT")

echo "Coverage delta summary:"
echo " Lines: ${BASE_LINES_PCT}% -> ${PR_LINES_PCT}% (${LINES_DELTA} pp, ${LINES_HIT_DELTA} lines)"
echo " Functions: ${BASE_FUNCTIONS_PCT}% -> ${PR_FUNCTIONS_PCT}% (${FUNCTIONS_DELTA} pp, ${FUNCTIONS_HIT_DELTA} fns)"
echo " Branches: ${BASE_BRANCHES_PCT}% -> ${PR_BRANCHES_PCT}% (${BRANCHES_DELTA} pp, ${BRANCHES_HIT_DELTA} branches)"

if [[ $(abs_lt "$LINES_DELTA" "$LINES_EPSILON") == 1 \
&& $(abs_lt "$LINES_HIT_DELTA" "$HIT_EPSILON") == 1 ]]; then
echo "Delta is negligible (eps=${LINES_EPSILON}pp, hit_eps=${HIT_EPSILON}); not posting."
exit 0
fi

: "${GH_TOKEN:?GH_TOKEN not set}"
: "${PR_NUMBER:?PR_NUMBER not set}"
: "${REPO:?REPO not set}"

BASE_SHORT=${BASE_SHA:-unknown}
HEAD_SHORT=${HEAD_SHA:-unknown}
BASE_SHORT=${BASE_SHORT:0:7}
HEAD_SHORT=${HEAD_SHORT:0:7}

format_row() {
local label=$1 base_pct=$2 pr_pct=$3 delta=$4 base_hit=$5 pr_hit=$6 total=$7 hit_delta=$8
echo "| ${label} | ${base_pct}% (${base_hit} / ${total}) | ${pr_pct}% (${pr_hit} / ${total}) | ${delta} pp (${hit_delta}) |"
}

BODY=$(cat <<EOF
${MARKER}
## 📊 Coverage report (v2 stack)

| Metric | Base (\`${BASE_SHORT}\`) | PR (\`${HEAD_SHORT}\`) | Δ |
|---|---|---|---|
$(format_row "Lines" "$BASE_LINES_PCT" "$PR_LINES_PCT" "$LINES_DELTA" "$BASE_LINES_HIT" "$PR_LINES_HIT" "$PR_LINES_TOTAL" "$LINES_HIT_DELTA")
$(format_row "Functions" "$BASE_FUNCTIONS_PCT" "$PR_FUNCTIONS_PCT" "$FUNCTIONS_DELTA" "$BASE_FUNCTIONS_HIT" "$PR_FUNCTIONS_HIT" "$PR_FUNCTIONS_TOTAL" "$FUNCTIONS_HIT_DELTA")
$(format_row "Branches" "$BASE_BRANCHES_PCT" "$PR_BRANCHES_PCT" "$BRANCHES_DELTA" "$BASE_BRANCHES_HIT" "$PR_BRANCHES_HIT" "$PR_BRANCHES_TOTAL" "$BRANCHES_HIT_DELTA")

<sub>Generated from \`make coverage-v2\`. Threshold for posting: line coverage must move by ≥ ${LINES_EPSILON} pp OR ≥ ${HIT_EPSILON} lines.</sub>
EOF
)

# Always surface in the job summary so fork PRs (read-only token) still
# get a visible report on the workflow run page.
if [[ -n "${GITHUB_STEP_SUMMARY:-}" ]]; then
printf '%s\n' "$BODY" >> "$GITHUB_STEP_SUMMARY"
fi

# Post the comment. A failed POST — 403 included — is a real error the
# check should surface loudly. The report is still in the job summary
# above so the data isn't lost, but a failed post means the pipeline
# isn't delivering on its contract and needs attention (grant the token
# pull-requests:write scope, or move this step into a workflow triggered
# by `workflow_run: completed` so it runs with base-branch permissions).
echo "Posting coverage comment on PR #${PR_NUMBER}"
if ! POST_ERR=$(gh api -X POST "repos/${REPO}/issues/${PR_NUMBER}/comments" \
-f body="$BODY" 2>&1 >/dev/null); then
if grep -q "Resource not accessible by integration" <<<"$POST_ERR"; then
echo "::error::PR comment POST returned 403 — GITHUB_TOKEN lacks pull-requests:write. Likely the fork-PR read-only-token case."
else
printf '%s\n' "$POST_ERR" >&2
fi
exit 1
fi
52 changes: 52 additions & 0 deletions .github/scripts/coverage-totals.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
#
# Print totals for an LCOV tracefile as shell-sourceable key=value lines:
#
# LINES_HIT=NNN
# LINES_TOTAL=NNN
# LINES_PCT=XX.XX
# FUNCTIONS_HIT=NNN
# FUNCTIONS_TOTAL=NNN
# FUNCTIONS_PCT=XX.XX
# BRANCHES_HIT=NNN
# BRANCHES_TOTAL=NNN
# BRANCHES_PCT=XX.XX
#
# Parses the LCOV trace directly rather than delegating to `lcov --summary`
# so the output is stable across lcov versions (1.14 / 1.16 / 2.x all
# format their summary text differently, and we want machine-readable
# fields for the PR-comment delta math).
#
# Usage: coverage-totals.sh path/to/file.lcov
set -euo pipefail

if [[ $# -ne 1 ]]; then
echo "usage: $0 <lcov-file>" >&2
exit 2
fi

LCOV=$1
if [[ ! -s "$LCOV" ]]; then
echo "lcov file missing or empty: $LCOV" >&2
exit 1
fi

awk -v out_prefix="" '
/^LH:/ { lh += substr($0, 4) }
/^LF:/ { lf += substr($0, 4) }
/^FNH:/ { fnh += substr($0, 5) }
/^FNF:/ { fnf += substr($0, 5) }
/^BRH:/ { brh += substr($0, 5) }
/^BRF:/ { brf += substr($0, 5) }
END {
printf "LINES_HIT=%d\n", lh
printf "LINES_TOTAL=%d\n", lf
printf "LINES_PCT=%.2f\n", (lf > 0) ? 100.0 * lh / lf : 0.0
printf "FUNCTIONS_HIT=%d\n", fnh
printf "FUNCTIONS_TOTAL=%d\n", fnf
printf "FUNCTIONS_PCT=%.2f\n", (fnf > 0) ? 100.0 * fnh / fnf : 0.0
printf "BRANCHES_HIT=%d\n", brh
printf "BRANCHES_TOTAL=%d\n", brf
printf "BRANCHES_PCT=%.2f\n", (brf > 0) ? 100.0 * brh / brf : 0.0
}
' "$LCOV"
183 changes: 183 additions & 0 deletions .github/workflows/coverage-diff.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
name: Coverage Diff

# `workflow_run` runs in the base branch's context (and with the base
# branch's permissions), independent of whether the triggering Tests
# workflow was launched by a same-repo PR or a fork PR. That's the
# mechanism GitHub provides for reacting to fork-PR workflows with
# write permissions — the `pull_request` trigger can't do that.
#
# Inputs come from the Tests workflow's uploaded artifacts:
# - `v2-coverage-lcov` — the PR's combined.lcov.
# - `v2-coverage-metadata` — a `KEY=value` file with PR number + SHAs.
# (The `workflow_run` event's
# `pull_requests` array is empty for fork
# PRs, so we can't read PR context off the
# event itself.)
on:
workflow_run:
workflows: [Tests]
types: [completed]

permissions:
contents: read
pull-requests: write
actions: read

env:
SOLANA_VERSION: "3.1.10"

jobs:
diff:
name: v2 coverage diff
# Only act on PR runs that produced artifacts. Push / workflow_dispatch
# Tests runs have no base to diff against, and failed runs may not
# have uploaded the artifacts we need.
if: >-
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
- name: Download PR coverage artifact
uses: actions/download-artifact@v4
with:
name: v2-coverage-lcov
path: pr-coverage
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Download PR metadata artifact
uses: actions/download-artifact@v4
with:
name: v2-coverage-metadata
path: pr-metadata
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

- name: Parse PR metadata
id: meta
run: |
set -e
cat pr-metadata/metadata.env
grep -E '^(PR_NUMBER|BASE_SHA|HEAD_SHA)=' pr-metadata/metadata.env \
>> "$GITHUB_OUTPUT"

# Only PRs targeting `anchor-next` carry the v2 stack the coverage
# pipeline measures. Look up the PR's base ref via the API (uniform
# across same-repo and fork PRs — the workflow_run event's
# `pull_requests` array is empty for fork-originated runs, so we
# can't pull this off `github.event`).
- name: Check PR base ref
id: base
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BASE_REF=$(gh api \
"repos/${{ github.repository }}/pulls/${{ steps.meta.outputs.PR_NUMBER }}" \
--jq '.base.ref')
echo "ref=${BASE_REF}" >> "$GITHUB_OUTPUT"
if [[ "$BASE_REF" != "anchor-next" ]]; then
echo "PR #${{ steps.meta.outputs.PR_NUMBER }} targets '${BASE_REF}', not anchor-next — skipping coverage diff."
fi

# Checkout base at the SHA the PR was opened against (stable across
# retriggered runs). `head.sha` is only used as a display label in
# the comment and for the tooling overlay below.
- name: Checkout base ref
if: steps.base.outputs.ref == 'anchor-next'
uses: actions/checkout@v4
with:
ref: ${{ steps.meta.outputs.BASE_SHA }}
fetch-depth: 0

- if: steps.base.outputs.ref == 'anchor-next'
uses: ./.github/actions/setup/
- if: steps.base.outputs.ref == 'anchor-next'
uses: ./.github/actions/setup-solana/

- if: steps.base.outputs.ref == 'anchor-next'
run: sudo apt-get install -y lcov
- name: Install cargo-llvm-cov
if: steps.base.outputs.ref == 'anchor-next'
run: |
if ! command -v cargo-llvm-cov >/dev/null; then
cargo install cargo-llvm-cov --locked
fi

# Share the cache keyspace with the pr-side `test-v2` job so the
# registry is hot even on a fresh runner.
- if: steps.base.outputs.ref == 'anchor-next'
uses: actions/cache@v4
name: Cache Cargo home
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: cargo-home-${{ runner.os }}-v2-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-home-${{ runner.os }}-v2-

- if: steps.base.outputs.ref == 'anchor-next'
uses: actions/cache@v4
name: Cache cargo target
with:
path: ./target/
# Separate target cache for coverage builds — instrumentation
# produces different artifacts than the regular `test-v2` build.
key: cargo-target-coverage-${{ runner.os }}-v2-${{ hashFiles('**/Cargo.lock') }}-${{ steps.meta.outputs.BASE_SHA }}
restore-keys: |
cargo-target-coverage-${{ runner.os }}-v2-${{ hashFiles('**/Cargo.lock') }}-
cargo-target-coverage-${{ runner.os }}-v2-

- if: steps.base.outputs.ref == 'anchor-next'
run: cargo build-sbf --tools-version v1.52 --install-only

# Overlay the PR's Makefile + coverage scripts onto the base
# checkout. The coverage pipeline is tooling, not source under
# test: if the PR fixes a Makefile bug, the base-side run should
# inherit the fix so both halves of the comparison use the same
# (correct) tooling.
- name: Fetch PR tooling
if: steps.base.outputs.ref == 'anchor-next'
uses: actions/checkout@v4
with:
ref: ${{ steps.meta.outputs.HEAD_SHA }}
path: pr-tooling
sparse-checkout: |
Makefile
.github/scripts/coverage-pr-comment.sh
.github/scripts/coverage-totals.sh
sparse-checkout-cone-mode: false
- name: Overlay PR tooling onto base checkout
if: steps.base.outputs.ref == 'anchor-next'
run: |
cp pr-tooling/Makefile Makefile
cp pr-tooling/.github/scripts/coverage-pr-comment.sh .github/scripts/
cp pr-tooling/.github/scripts/coverage-totals.sh .github/scripts/
chmod +x .github/scripts/coverage-pr-comment.sh
chmod +x .github/scripts/coverage-totals.sh

- name: Build base coverage
if: steps.base.outputs.ref == 'anchor-next'
run: make coverage-v2

- name: Stash base lcov
if: steps.base.outputs.ref == 'anchor-next'
run: |
mkdir -p /tmp/coverage-base
cp target/coverage/combined.lcov /tmp/coverage-base/combined.lcov

- name: Compute delta and comment
if: steps.base.outputs.ref == 'anchor-next'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.meta.outputs.PR_NUMBER }}
REPO: ${{ github.repository }}
BASE_SHA: ${{ steps.meta.outputs.BASE_SHA }}
HEAD_SHA: ${{ steps.meta.outputs.HEAD_SHA }}
run: |
bash .github/scripts/coverage-pr-comment.sh \
/tmp/coverage-base/combined.lcov \
pr-coverage/combined.lcov
Loading
Loading