Skip to content

Commit a311a9b

Browse files
Merge pull request #2 from DataDog/feat/add-ci-coverage
feat(ci): add comprehensive test coverage enforcement and PR reporting
2 parents 8861837 + 3024f0a commit a311a9b

26 files changed

Lines changed: 552 additions & 532 deletions

.github/badges/coverage.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"schemaVersion": 1,
3+
"label": "coverage",
4+
"message": "93.9%",
5+
"color": "brightgreen"
6+
}

.github/workflows/ci.yml

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- '**'
7+
pull_request:
8+
branches:
9+
- '**'
10+
11+
permissions:
12+
contents: write # Needed to commit coverage badge on main branch
13+
pull-requests: write
14+
15+
jobs:
16+
test:
17+
name: Test and Coverage
18+
runs-on: ubuntu-latest
19+
steps:
20+
- name: Checkout code
21+
uses: actions/checkout@v4
22+
23+
- name: Set up Go
24+
uses: actions/setup-go@v5
25+
with:
26+
go-version: '1.21'
27+
cache: true
28+
29+
- name: Install bc for floating-point math
30+
run: sudo apt-get update && sudo apt-get install -y bc
31+
32+
- name: Run tests with coverage
33+
run: |
34+
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
35+
go tool cover -html=coverage.out -o coverage.html
36+
37+
- name: Calculate coverage
38+
id: coverage
39+
run: |
40+
# Calculate total coverage
41+
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
42+
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
43+
echo "Total coverage: $COVERAGE%"
44+
45+
# Calculate coverage by package
46+
echo "## Coverage by Package" > coverage_report.txt
47+
echo "" >> coverage_report.txt
48+
go tool cover -func=coverage.out | grep -v "total:" | awk '{print $1, $3}' | sort -t: -k1,1 -u | while read line; do
49+
echo "- $line" >> coverage_report.txt
50+
done
51+
52+
# Get coverage summary
53+
echo "" >> coverage_report.txt
54+
echo "## Summary" >> coverage_report.txt
55+
echo "" >> coverage_report.txt
56+
go tool cover -func=coverage.out | tail -1 >> coverage_report.txt
57+
58+
- name: Check coverage threshold
59+
run: |
60+
COVERAGE=${{ steps.coverage.outputs.coverage }}
61+
THRESHOLD=80.0
62+
63+
echo "Coverage: $COVERAGE%"
64+
echo "Threshold: $THRESHOLD%"
65+
66+
# Use bc for floating point comparison
67+
if [ $(echo "$COVERAGE < $THRESHOLD" | bc -l) -eq 1 ]; then
68+
echo "❌ Coverage $COVERAGE% is below threshold $THRESHOLD%"
69+
exit 1
70+
else
71+
echo "✅ Coverage $COVERAGE% meets threshold $THRESHOLD%"
72+
fi
73+
74+
- name: Generate coverage badge data
75+
if: github.event_name == 'pull_request'
76+
id: badge
77+
run: |
78+
COVERAGE=${{ steps.coverage.outputs.coverage }}
79+
80+
# Determine badge color based on coverage
81+
if [ $(echo "$COVERAGE >= 90" | bc -l) -eq 1 ]; then
82+
COLOR="brightgreen"
83+
elif [ $(echo "$COVERAGE >= 80" | bc -l) -eq 1 ]; then
84+
COLOR="green"
85+
elif [ $(echo "$COVERAGE >= 70" | bc -l) -eq 1 ]; then
86+
COLOR="yellow"
87+
elif [ $(echo "$COVERAGE >= 60" | bc -l) -eq 1 ]; then
88+
COLOR="orange"
89+
else
90+
COLOR="red"
91+
fi
92+
93+
echo "color=$COLOR" >> $GITHUB_OUTPUT
94+
95+
- name: Generate PR comment body
96+
if: github.event_name == 'pull_request'
97+
id: comment
98+
env:
99+
COVERAGE: ${{ steps.coverage.outputs.coverage }}
100+
BADGE_COLOR: ${{ steps.badge.outputs.color }}
101+
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
102+
run: |
103+
# Determine status
104+
if [ $(echo "$COVERAGE >= 80" | bc -l) -eq 1 ]; then
105+
STATUS="✅ PASSED - Coverage meets minimum threshold"
106+
STATUS_EMOJI="✅"
107+
else
108+
STATUS="❌ FAILED - Coverage below minimum threshold"
109+
STATUS_EMOJI="❌"
110+
fi
111+
112+
# Create comment body using heredoc
113+
cat > comment_final.txt << EOF
114+
## 📊 Test Coverage Report
115+
116+
**Overall Coverage:** ${COVERAGE}% ![Coverage](https://img.shields.io/badge/coverage-${COVERAGE}%25-${BADGE_COLOR})
117+
118+
**Threshold:** 80% ${STATUS_EMOJI}
119+
120+
<details>
121+
<summary>Coverage by Package</summary>
122+
123+
\`\`\`
124+
$(cat coverage_report.txt)
125+
\`\`\`
126+
127+
</details>
128+
129+
---
130+
📈 **Coverage Status:** ${STATUS}
131+
132+
<sub>Updated for commit ${COMMIT_SHA}</sub>
133+
EOF
134+
135+
- name: Comment on PR
136+
if: github.event_name == 'pull_request'
137+
uses: actions/github-script@v7
138+
env:
139+
COMMENT_BODY: ${{ steps.comment.outputs.comment_body }}
140+
with:
141+
script: |
142+
const fs = require('fs');
143+
const commentBody = fs.readFileSync('comment_final.txt', 'utf8');
144+
145+
// Find existing comment
146+
const { data: comments } = await github.rest.issues.listComments({
147+
owner: context.repo.owner,
148+
repo: context.repo.repo,
149+
issue_number: context.issue.number,
150+
});
151+
152+
const botComment = comments.find(comment =>
153+
comment.user.type === 'Bot' && comment.body.includes('📊 Test Coverage Report')
154+
);
155+
156+
if (botComment) {
157+
// Update existing comment
158+
await github.rest.issues.updateComment({
159+
owner: context.repo.owner,
160+
repo: context.repo.repo,
161+
comment_id: botComment.id,
162+
body: commentBody
163+
});
164+
} else {
165+
// Create new comment
166+
await github.rest.issues.createComment({
167+
owner: context.repo.owner,
168+
repo: context.repo.repo,
169+
issue_number: context.issue.number,
170+
body: commentBody
171+
});
172+
}
173+
174+
- name: Generate coverage badge for main branch
175+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
176+
env:
177+
COVERAGE: ${{ steps.coverage.outputs.coverage }}
178+
run: |
179+
# Determine badge color
180+
if [ $(echo "$COVERAGE >= 90" | bc -l) -eq 1 ]; then
181+
COLOR="brightgreen"
182+
elif [ $(echo "$COVERAGE >= 80" | bc -l) -eq 1 ]; then
183+
COLOR="green"
184+
elif [ $(echo "$COVERAGE >= 70" | bc -l) -eq 1 ]; then
185+
COLOR="yellow"
186+
elif [ $(echo "$COVERAGE >= 60" | bc -l) -eq 1 ]; then
187+
COLOR="orange"
188+
else
189+
COLOR="red"
190+
fi
191+
192+
# Create badge JSON for shields.io endpoint schema
193+
mkdir -p .github/badges
194+
cat > .github/badges/coverage.json << EOF
195+
{
196+
"schemaVersion": 1,
197+
"label": "coverage",
198+
"message": "${COVERAGE}%",
199+
"color": "${COLOR}"
200+
}
201+
EOF
202+
203+
echo "Generated coverage badge: ${COVERAGE}% (${COLOR})"
204+
205+
- name: Commit coverage badge to main
206+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
207+
run: |
208+
git config --local user.email "github-actions[bot]@users.noreply.github.com"
209+
git config --local user.name "github-actions[bot]"
210+
git add .github/badges/coverage.json
211+
git diff --staged --quiet || git commit -m "chore: update coverage badge [skip ci]"
212+
git push
213+
214+
- name: Upload coverage artifacts
215+
uses: actions/upload-artifact@v4
216+
with:
217+
name: coverage-report
218+
path: |
219+
coverage.out
220+
coverage.html
221+
coverage_report.txt
222+
retention-days: 30
223+
224+
lint:
225+
name: Lint
226+
runs-on: ubuntu-latest
227+
steps:
228+
- name: Checkout code
229+
uses: actions/checkout@v4
230+
231+
- name: Set up Go
232+
uses: actions/setup-go@v5
233+
with:
234+
go-version: '1.21'
235+
cache: true
236+
237+
- name: golangci-lint
238+
uses: golangci/golangci-lint-action@v6
239+
with:
240+
version: latest
241+
args: --timeout=5m
242+
243+
build:
244+
name: Build
245+
runs-on: ubuntu-latest
246+
steps:
247+
- name: Checkout code
248+
uses: actions/checkout@v4
249+
250+
- name: Set up Go
251+
uses: actions/setup-go@v5
252+
with:
253+
go-version: '1.21'
254+
cache: true
255+
256+
- name: Build
257+
run: go build -v ./...
258+
259+
- name: Build CLI binary
260+
run: go build -o pup .
261+
262+
- name: Verify binary
263+
run: ./pup --version

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ coverage.html
1212

1313
# Output of the go coverage tool
1414
*.out
15+
coverage_report.txt
16+
comment_body.txt
17+
comment_final.txt
1518

1619
# Go workspace file
1720
go.work

CLAUDE.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,61 @@ See the [Automated Development Workflow](#automated-development-workflow-for-cla
346346
- Aim for >80% code coverage
347347
- Include integration tests for critical paths
348348

349+
### CI/CD and Code Coverage
350+
351+
**Coverage Requirements:**
352+
- **Minimum threshold: 80%** - PRs that drop coverage below 80% will fail CI
353+
- Coverage is automatically calculated and reported on every PR and branch
354+
- Coverage reports are uploaded as artifacts for 30 days
355+
- Coverage badge is automatically updated on the main branch
356+
357+
**CI Workflow:**
358+
The project uses GitHub Actions with three parallel jobs that run on all branches:
359+
360+
1. **Test and Coverage**:
361+
- Runs all tests with race detection
362+
- Generates coverage reports (text, HTML)
363+
- Checks coverage meets 80% threshold
364+
- Comments on PR with detailed coverage breakdown
365+
- Uploads coverage artifacts
366+
- On main branch: Updates coverage badge in README.md
367+
368+
2. **Lint**:
369+
- Runs `golangci-lint` with 5-minute timeout
370+
- Enforces Go style and best practices
371+
372+
3. **Build**:
373+
- Verifies the project builds successfully
374+
- Builds the CLI binary
375+
- Validates binary execution
376+
377+
**Coverage Badge:**
378+
The README.md displays a live coverage badge that updates automatically on each push to main:
379+
- Badge color indicates coverage level (green 80%+, yellow 70-80%, red <70%)
380+
- Badge data stored in `.github/badges/coverage.json`
381+
- Uses shields.io endpoint for dynamic display
382+
383+
**PR Coverage Comments:**
384+
Every PR receives an automated comment showing:
385+
- Overall coverage percentage with color-coded badge
386+
- Pass/fail status against 80% threshold
387+
- Detailed coverage breakdown by package
388+
- Commit SHA for tracking
389+
390+
**Running Coverage Locally:**
391+
```bash
392+
# Run tests with coverage
393+
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
394+
395+
# View coverage in terminal
396+
go tool cover -func=coverage.out
397+
398+
# Generate HTML coverage report
399+
go tool cover -html=coverage.out -o coverage.html
400+
open coverage.html # macOS
401+
xdg-open coverage.html # Linux
402+
```
403+
349404
### Configuration Precedence
350405

351406
Configuration values are resolved in the following order (highest to lowest priority):

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Pup - Datadog API CLI Wrapper
22

3+
[![CI](https://github.com/DataDog/pup/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DataDog/pup/actions/workflows/ci.yml)
4+
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/DataDog/pup/main/.github/badges/coverage.json)](https://github.com/DataDog/pup/actions/workflows/ci.yml)
5+
[![Go Version](https://img.shields.io/badge/go-1.21+-00ADD8?logo=go)](https://go.dev/)
6+
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
7+
38
A Go-based command-line wrapper for easy interaction with Datadog APIs.
49

510
## Features

cmd/api_keys_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ func TestAPIKeysCmd_Subcommands(t *testing.T) {
3434

3535
commandMap := make(map[string]bool)
3636
for _, cmd := range commands {
37-
commandMap[cmd.Use] = true
37+
commandMap[cmd.Name()] = true
3838
}
3939

4040
for _, expected := range expectedCommands {

cmd/audit_logs.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,10 @@ func runAuditLogsSearch(cmd *cobra.Command, args []string) error {
127127
page.SetLimit(auditLogsLimit)
128128
body.SetPage(page)
129129

130-
resp, r, err := api.SearchAuditLogs(client.Context(), datadogV2.SearchAuditLogsOptionalParameters{}.WithBody(body))
130+
opts := datadogV2.SearchAuditLogsOptionalParameters{
131+
Body: &body,
132+
}
133+
resp, r, err := api.SearchAuditLogs(client.Context(), opts)
131134
if err != nil {
132135
if r != nil {
133136
return fmt.Errorf("failed to search audit logs: %w (status: %d)", err, r.StatusCode)

0 commit comments

Comments
 (0)