From 6b2eb81fd48196780c52492a4a1454b044e3956b Mon Sep 17 00:00:00 2001 From: Mohammed Al Sahaf Date: Tue, 9 Jun 2026 00:40:56 +0300 Subject: [PATCH] add draft for coverage report generation --- .github/workflows/ci.yml | 141 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0cdbe4eac..574001d1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,6 +138,19 @@ jobs: go test -v -coverprofile="cover-profile.out" -short -race ./... # echo "status=$?" >> $GITHUB_OUTPUT + # Only upload from linux: mac and windows runs would produce overlapping + # profiles for the same packages, and the coverage-report job's simple + # concat-merge can't deduplicate. The artifact name must match the + # `caddy-*-coverage-*` pattern consumed by coverage-report. + - name: Publish Coverage Profile + if: always() && matrix.os == 'linux' + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: caddy-unit-coverage-${{ steps.vars.outputs.short_sha }} + path: cover-profile.out + retention-days: 30 + compression-level: 6 + # Relevant step if we reinvestigate publishing test/coverage reports # - name: Prepare coverage reports # run: | @@ -357,6 +370,134 @@ jobs: retention-days: 30 compression-level: 6 + # coverage-report: + # name: "Coverage report" + # # Always runs once the producer jobs finish, even if some failed: each + # # producer publishes its coverage artifact under `if: always()`, so a + # # partial failure still yields a usable profile to summarize. + # if: ${{ always() }} + # runs-on: ubuntu-latest + # needs: + # - test + # - spec-test + # # TODO: once `s390x-test` publishes its own coverage artifact + # # matching `caddy-*-coverage-*`, add it here so this job merges + # # all sources before reporting. + # permissions: + # contents: read + # pull-requests: write + # steps: + # - name: Harden the runner (Audit all outbound calls) + # uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + # with: + # egress-policy: audit + + # # `go tool cover -func` needs the source tree to map profile line + # # ranges back to function names, so the repo must be checked out + # # even though this job doesn't run any tests itself. + # - name: Checkout code + # uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + # - name: Install Go + # uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + # with: + # go-version: '~1.25.0' + # check-latest: true + + # # Pulls every artifact whose name matches the pattern into a flat + # # directory. Today this just grabs spec-test's `caddy-spec-coverage-*`; + # # additional producers will be picked up automatically. + # - name: Download coverage artifacts + # uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + # with: + # path: cov-in + # pattern: caddy-*-coverage-* + # merge-multiple: true + + # - name: Merge coverage profiles + # id: merge + # run: | + # set -euo pipefail + # shopt -s nullglob + # # Producers use different extensions: spec-test publishes + # # `*.txt` (from `go tool covdata textfmt`), the unit `test` job + # # publishes `cover-profile.out` (from `go test -coverprofile`). + # # Both are Go text-format profiles. Exclude `.html` reports. + # profiles=(cov-in/*.txt cov-in/*.out) + # if [[ ${#profiles[@]} -eq 0 ]]; then + # echo "No coverage profiles found; nothing to report." + # exit 0 + # fi + # # Concatenate Go text-format profiles: keep the first `mode:` + # # line and append every other line from every profile. Safe + # # because all producers use the same coverage mode. + # head -n 1 "${profiles[0]}" > merged.txt + # for f in "${profiles[@]}"; do + # tail -n +2 "$f" >> merged.txt + # done + # echo "profile=merged.txt" >> "$GITHUB_OUTPUT" + # echo "sources=${#profiles[@]}" >> "$GITHUB_OUTPUT" + + # - name: Build coverage summary + # if: steps.merge.outputs.profile != '' + # run: | + # set -euo pipefail + # COVER_FILE=merged.txt + # TOTAL=$(go tool cover -func="$COVER_FILE" | awk '/^total:/ {print $NF}') + + # # Per-package aggregation: average coverage across all functions in a package, lowest first. + # go tool cover -func="$COVER_FILE" \ + # | awk -v root='github.com/caddyserver/caddy/v2/' ' + # /^total:/ {next} + # { + # file=$1; sub(/:.*/, "", file); sub(root, "", file) + # n=split(file, parts, "/") + # pkg=(n>1) ? substr(file, 1, length(file)-length(parts[n])-1) : "(root)" + # pct=$NF; sub(/%/, "", pct) + # sum[pkg]+=pct; cnt[pkg]++ + # } + # END { for (p in sum) printf "%s\t%.1f\n", p, sum[p]/cnt[p] } + # ' | sort -t$'\t' -k2,2n > /tmp/pkg-cov.tsv + + # { + # echo "## Coverage report" + # echo + # echo "**Total coverage:** \`$TOTAL\`  ยท  merged from ${{ steps.merge.outputs.sources }} profile(s)" + # echo + # echo "
Lowest-covered packages (up to 15)" + # echo + # echo "| Package | Coverage |" + # echo "|---|---:|" + # head -n 15 /tmp/pkg-cov.tsv \ + # | awk -F'\t' '{printf "| `%s` | %.1f%% |\n", $1, $2}' + # echo + # echo "
" + # echo + # echo "
Lowest-covered functions (up to 30)" + # echo + # echo '```' + # # `awk` does the "head + cut" atomically so we don't trigger + # # SIGPIPE on `sort` when `head -n 30` closes its input early + # # (which `set -o pipefail` would then surface as an error). + # go tool cover -func="$COVER_FILE" \ + # | grep -v '^total:' \ + # | awk '{ pct=$NF; sub(/%/,"",pct); if (pct+0 < 100) print pct"\t"$0 }' \ + # | sort -n \ + # | awk -F'\t' 'NR<=30 { print $2 }' + # echo '```' + # echo + # echo "
" + # } > coverage-summary.md + + # cat coverage-summary.md >> "$GITHUB_STEP_SUMMARY" + + # - name: Post coverage summary as sticky PR comment + # if: github.event_name == 'pull_request' && hashFiles('coverage-summary.md') != '' + # uses: marocchino/sticky-pull-request-comment@0ea0beb66eb9baf113663a64ec522f60e49231c0 # v3.0.4 + # with: + # header: coverage-report + # path: coverage-summary.md + s390x-test: name: test (s390x on IBM Z) permissions: