Skip to content

Commit 5dc1fa9

Browse files
authored
Merge pull request #3 from CodeYogiCo/Self-add
chore: add pull covergae step to itself
2 parents fa8a49f + f25dc6a commit 5dc1fa9

4 files changed

Lines changed: 240 additions & 76 deletions

File tree

.github/workflows/pr-coverage.yml

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: pr-code-coverage
2+
3+
# Dogfoods this plugin on its own PRs: builds the plugin image from the PR
4+
# source, computes coverage for only the lines changed in the PR, and posts a
5+
# summary comment on the PR.
6+
7+
on:
8+
pull_request:
9+
10+
permissions:
11+
contents: read
12+
pull-requests: write # required so the plugin can post the coverage comment
13+
14+
jobs:
15+
coverage:
16+
runs-on: ubuntu-latest
17+
# Fork PRs only get a read-only GITHUB_TOKEN (can't comment) and no secrets,
18+
# so restrict to same-repo PRs to avoid a guaranteed failure on forks.
19+
if: github.event.pull_request.head.repo.full_name == github.repository
20+
steps:
21+
- name: Check out the repo
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 0 # need the base branch present to diff against it
25+
26+
- name: Set up Go
27+
uses: actions/setup-go@v5
28+
with:
29+
go-version: 1.26.3
30+
31+
- name: Generate coverage profile
32+
run: go test -coverpkg=./... -coverprofile=coverage.txt ./...
33+
34+
- name: Convert coverage to cobertura
35+
# go run leaves no installed binary; boumenot emits <source> as the
36+
# absolute build dir (== github.workspace) and class filenames relative
37+
# to the module root, which is what PARAMETER_SOURCE_DIRS matches against.
38+
run: go run github.com/boumenot/gocover-cobertura@v1.5.0 < coverage.txt > coverage.xml
39+
40+
- name: Build plugin image
41+
# The published :latest image predates the public-github.com base-URL fix,
42+
# so build from the PR source to test the exact code under review.
43+
run: docker build -t pr-code-coverage:ci .
44+
45+
- name: Report coverage on changed lines
46+
env:
47+
PARAMETER_COVERAGE_TYPE: cobertura
48+
PARAMETER_COVERAGE_FILE: coverage.xml
49+
# Must equal the cobertura <source> path (the dir go test ran in).
50+
PARAMETER_SOURCE_DIRS: ${{ github.workspace }}
51+
# Omit PARAMETER_GH_API_BASE_URL -> defaults to https://api.github.com.
52+
PARAMETER_GH_API_KEY: ${{ secrets.GITHUB_TOKEN }}
53+
BUILD_PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number }}
54+
REPOSITORY_ORG: ${{ github.repository_owner }}
55+
REPOSITORY_NAME: ${{ github.event.repository.name }}
56+
run: |
57+
git fetch --no-tags origin "${{ github.base_ref }}"
58+
git --no-pager diff --unified=0 "origin/${{ github.base_ref }}" -- '*.go' \
59+
| docker run --rm -i \
60+
-e PARAMETER_COVERAGE_TYPE \
61+
-e PARAMETER_COVERAGE_FILE \
62+
-e PARAMETER_SOURCE_DIRS \
63+
-e PARAMETER_GH_API_KEY \
64+
-e BUILD_PULL_REQUEST_NUMBER \
65+
-e REPOSITORY_ORG \
66+
-e REPOSITORY_NAME \
67+
-v "${{ github.workspace }}:${{ github.workspace }}" \
68+
-w "${{ github.workspace }}" \
69+
--entrypoint /plugin \
70+
pr-code-coverage:ci

internal/plugin/reporter/github_pr.go

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -89,47 +89,44 @@ func (s *GithubPullRequest) createCommentBody(changedLinesWithCoverage domain.So
8989

9090
modules := collectModules(changedLinesWithCoverage)
9191

92-
summaryLines := []string{}
93-
94-
if len(modules) > 0 {
95-
summaryLines = append(summaryLines, fmt.Sprintf("*Modules: %v*\n\n", strings.Join(modules, ", ")))
92+
bodyLines := []string{
93+
"## 📊 Code Coverage — Changed Lines\n",
94+
"\n",
95+
"> Coverage is measured **only for the lines this PR changes**, not the whole file or repo.\n",
96+
"\n",
9697
}
97-
var missedInstructions string
9898

99-
for _, r := range changedLinesWithCoverage {
100-
if r.MissedInstructionCount > 0 {
101-
missedInstructions += fmt.Sprintf("--- %v\n", lineDescription(r.SourceLine))
102-
missedInstructions += fmt.Sprintf("%v\n", r.LineValue)
103-
}
99+
if len(modules) > 0 {
100+
bodyLines = append(bodyLines, fmt.Sprintf("*Modules: %v*\n\n", strings.Join(modules, ", ")))
104101
}
105102

106-
summaryLines = append(summaryLines, generateSummaryLines(changedLinesWithCoverage, func(linesWithDataCount int, linesWithoutDataCount int, covered int, missed int) []string {
103+
bodyLines = append(bodyLines, generateSummaryLines(changedLinesWithCoverage, func(linesWithDataCount int, linesWithoutDataCount int, covered int, missed int) []string {
107104
totalLines := linesWithDataCount + linesWithoutDataCount
108105
totalInstructions := covered + missed
109106

110-
result := make([]string, 5)
111-
112-
result[0] = "Code Coverage Summary:\n\n"
113-
result[1] = fmt.Sprintf("Lines Without Coverage Data -> %.f%% (%d)\n", toPercent(safeDiv(float32(linesWithoutDataCount), float32(totalLines), 0)), linesWithoutDataCount)
114-
result[2] = fmt.Sprintf("Lines With Coverage Data -> %.f%% (%d)\n", toPercent(safeDiv(float32(linesWithDataCount), float32(totalLines), 1)), linesWithDataCount)
115-
result[3] = fmt.Sprintf("Covered Instructions -> **%.f%%** (%d)\n", toPercent(safeDiv(float32(covered), float32(totalInstructions), 1)), covered)
116-
result[4] = fmt.Sprintf("Missed Instructions -> %.f%% (%d)\n", toPercent(safeDiv(float32(missed), float32(totalInstructions), 0)), missed)
117-
118-
return result
107+
coveredPct := toPercent(safeDiv(float32(covered), float32(totalInstructions), 1))
108+
missedPct := toPercent(safeDiv(float32(missed), float32(totalInstructions), 0))
109+
withDataPct := toPercent(safeDiv(float32(linesWithDataCount), float32(totalLines), 1))
110+
withoutDataPct := toPercent(safeDiv(float32(linesWithoutDataCount), float32(totalLines), 0))
111+
112+
return []string{
113+
fmt.Sprintf("### %v Covered Instructions: %.f%% (%d)\n", coverageStatusEmoji(coveredPct), coveredPct, covered),
114+
"\n",
115+
"| Metric | Result | What it means |\n",
116+
"| :-- | :-: | :-- |\n",
117+
fmt.Sprintf("| 🟢 **Covered Instructions** | **%.f%%** (%d) | Changed code your tests executed. Higher is better. |\n", coveredPct, covered),
118+
fmt.Sprintf("| 🔴 **Missed Instructions** | %.f%% (%d) | Changed code your tests never ran. Lower is better. |\n", missedPct, missed),
119+
fmt.Sprintf("| 📈 Lines With Coverage Data | %.f%% (%d) | Changed lines the coverage tool could track. |\n", withDataPct, linesWithDataCount),
120+
fmt.Sprintf("| ⚪ Lines Without Coverage Data | %.f%% (%d) | Changed lines with no data: comments, blanks, declarations. |\n", withoutDataPct, linesWithoutDataCount),
121+
}
119122
})...)
120123

121-
var summary string
122-
if missedInstructions == "" {
123-
summary = strings.Join(summaryLines, "")
124-
} else {
125-
126-
summaryWithoutInstructions := strings.Join(summaryLines, "")
127-
summary = summaryWithoutInstructions + "\n<details><summary>Missed Instructions summary</summary>\n\n" + "```\n" + missedInstructions + "```" +
128-
"\n</details>"
129-
}
124+
body := strings.Join(bodyLines, "")
125+
body += missedInstructionsSection(changedLinesWithCoverage)
126+
body += "\n<sub>🤖 Generated by <a href=\"https://github.com/target/pull-request-code-coverage\">pull-request-code-coverage</a> — coverage for changed lines only.</sub>\n"
130127

131128
data := map[string]string{
132-
"body": summary,
129+
"body": body,
133130
}
134131

135132
dataBytes, marshalErr := s.jsonClient.Marshal(data)
@@ -141,6 +138,41 @@ func (s *GithubPullRequest) createCommentBody(changedLinesWithCoverage domain.So
141138
return bytes.NewBuffer(dataBytes), nil
142139
}
143140

141+
// missedInstructionsSection renders a collapsible block listing each changed
142+
// line that was not executed by tests. Returns "" when nothing was missed.
143+
func missedInstructionsSection(changedLinesWithCoverage domain.SourceLineCoverageReport) string {
144+
var missedInstructions string
145+
missedLineCount := 0
146+
147+
for _, r := range changedLinesWithCoverage {
148+
if r.MissedInstructionCount > 0 {
149+
missedLineCount++
150+
missedInstructions += fmt.Sprintf("--- %v\n", lineDescription(r.SourceLine))
151+
missedInstructions += fmt.Sprintf("%v\n", r.LineValue)
152+
}
153+
}
154+
155+
if missedInstructions == "" {
156+
return ""
157+
}
158+
159+
return fmt.Sprintf("\n<details><summary>🔍 Missed instructions (%d)</summary>\n\n", missedLineCount) +
160+
"```\n" + missedInstructions + "```" + "\n</details>\n"
161+
}
162+
163+
// coverageStatusEmoji maps a covered-instruction percentage to a traffic-light
164+
// status icon, so the headline reads at a glance.
165+
func coverageStatusEmoji(coveredPct float32) string {
166+
switch {
167+
case coveredPct >= 80:
168+
return "🟢"
169+
case coveredPct >= 50:
170+
return "🟡"
171+
default:
172+
return "🔴"
173+
}
174+
}
175+
144176
func collectModules(changedLinesWithCoverage domain.SourceLineCoverageReport) []string {
145177
collector := map[string]bool{}
146178

internal/plugin/runner.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ func NewRunner() *DefaultRunner {
3333
// nolint: gocyclo
3434
func (*DefaultRunner) Run(propertyGetter func(string) (string, bool), changedSourceLinesSource io.Reader, reportDefaultOut io.Writer) error {
3535

36+
logrus.Info("starting pull-request-code-coverage run")
37+
3638
rawSourceDirs, found := propertyGetter("PARAMETER_SOURCE_DIRS")
3739
if !found {
3840
return errors.New("Missing property PARAMETER_SOURCE_DIRS")

0 commit comments

Comments
 (0)