diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06d8e34..9b791f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,6 +124,17 @@ jobs: path: coverage.out retention-days: 30 + - name: Coverage PR comment + # ci-pipeline-cleanup Phase 10 / frozen decision 0.9: self-hosted + # alternative to Codecov / Coveralls. Posts a per-package coverage + # delta as a PR comment; updates in place on subsequent pushes. + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.number }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: bash scripts/ci-guards/coverage-pr-comment.sh + # Bundle P / Strengthening #6 — QA-doc drift guards. Forces every PR # that adds a Part to docs/testing-guide.md OR a seed row to # migrations/seed_demo.sql to keep docs/qa-test-guide.md in sync. This diff --git a/scripts/ci-guards/coverage-pr-comment.sh b/scripts/ci-guards/coverage-pr-comment.sh new file mode 100755 index 0000000..c8ff4af --- /dev/null +++ b/scripts/ci-guards/coverage-pr-comment.sh @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# scripts/ci-guards/coverage-pr-comment.sh +# +# Post a per-package coverage table as a PR comment on every PR. +# Self-hosted alternative to Codecov / Coveralls (per ci-pipeline-cleanup +# bundle Phase 10 / frozen decision 0.9). +# +# Reads coverage.out from the Go Test step. Updates an existing comment +# in place if one already exists (avoids duplicate noise on subsequent +# pushes to the same PR). +# +# Required env: +# GH_TOKEN — secrets.GITHUB_TOKEN +# PR_NUMBER — github.event.number +# GITHUB_REPOSITORY — github.repository (owner/name) + +set -e + +if [ -z "$PR_NUMBER" ]; then + echo "PR_NUMBER not set — not a PR build, skipping coverage comment." + exit 0 +fi +if [ -z "$GH_TOKEN" ]; then + echo "::warning::GH_TOKEN not set — cannot post coverage comment" + exit 0 +fi +if [ ! -f coverage.out ]; then + echo "::warning::coverage.out not found — skipping coverage comment" + exit 0 +fi + +# Build per-package summary table (mirrors check-coverage-thresholds.sh logic). +table=$(go tool cover -func=coverage.out | awk ' + /internal\// { + pkg = $1 + sub(/\/[^\/]+\.go:.*$/, "", pkg) + cov = $NF + sub(/%/, "", cov) + sum[pkg] += cov + 0 + n[pkg]++ + } + END { + for (pkg in sum) printf "| `%s` | %.1f%% |\n", pkg, sum[pkg] / n[pkg] + } +' | sort) + +total=$(go tool cover -func=coverage.out | tail -1 | awk '{print $NF}') + +body="**Coverage report (HEAD)** + +| package | coverage | +|---|---:| +| **TOTAL** | **${total}** | +${table} + +_Per-package floors enforced by \`scripts/check-coverage-thresholds.sh\`._ +_Generated by \`scripts/ci-guards/coverage-pr-comment.sh\` (ci-pipeline-cleanup Phase 10)._" + +# Find existing comment created by this script (starts with the marker). +api="https://api.github.com/repos/${GITHUB_REPOSITORY}" +existing_id=$(curl -sS \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "$api/issues/$PR_NUMBER/comments?per_page=100" \ + | python3 -c " +import sys, json +comments = json.load(sys.stdin) +for c in comments: + if c['body'].startswith('**Coverage report'): + print(c['id']) + break +") + +if [ -n "$existing_id" ]; then + curl -sS -X PATCH \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "$api/issues/comments/$existing_id" \ + -d "$(python3 -c "import json,sys; print(json.dumps({'body': open('/dev/stdin').read()}))" <<< "$body")" \ + > /dev/null + echo "Updated existing coverage comment #$existing_id on PR #$PR_NUMBER" +else + curl -sS -X POST \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Accept: application/vnd.github+json" \ + "$api/issues/$PR_NUMBER/comments" \ + -d "$(python3 -c "import json,sys; print(json.dumps({'body': open('/dev/stdin').read()}))" <<< "$body")" \ + > /dev/null + echo "Created new coverage comment on PR #$PR_NUMBER" +fi