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
6 changes: 6 additions & 0 deletions .github/badges/coverage.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"schemaVersion": 1,
"label": "coverage",
"message": "93.9%",
"color": "brightgreen"
}
263 changes: 263 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
name: CI

on:
push:
branches:
- '**'
pull_request:
branches:
- '**'

permissions:
contents: write # Needed to commit coverage badge on main branch
pull-requests: write

jobs:
test:
name: Test and Coverage
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true

- name: Install bc for floating-point math
run: sudo apt-get update && sudo apt-get install -y bc

- name: Run tests with coverage
run: |
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html

- name: Calculate coverage
id: coverage
run: |
# Calculate total coverage
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
echo "Total coverage: $COVERAGE%"

# Calculate coverage by package
echo "## Coverage by Package" > coverage_report.txt
echo "" >> coverage_report.txt
go tool cover -func=coverage.out | grep -v "total:" | awk '{print $1, $3}' | sort -t: -k1,1 -u | while read line; do
echo "- $line" >> coverage_report.txt
done

# Get coverage summary
echo "" >> coverage_report.txt
echo "## Summary" >> coverage_report.txt
echo "" >> coverage_report.txt
go tool cover -func=coverage.out | tail -1 >> coverage_report.txt

- name: Check coverage threshold
run: |
COVERAGE=${{ steps.coverage.outputs.coverage }}
THRESHOLD=80.0

echo "Coverage: $COVERAGE%"
echo "Threshold: $THRESHOLD%"

# Use bc for floating point comparison
if [ $(echo "$COVERAGE < $THRESHOLD" | bc -l) -eq 1 ]; then
echo "❌ Coverage $COVERAGE% is below threshold $THRESHOLD%"
exit 1
else
echo "βœ… Coverage $COVERAGE% meets threshold $THRESHOLD%"
fi

- name: Generate coverage badge data
if: github.event_name == 'pull_request'
id: badge
run: |
COVERAGE=${{ steps.coverage.outputs.coverage }}

# Determine badge color based on coverage
if [ $(echo "$COVERAGE >= 90" | bc -l) -eq 1 ]; then
COLOR="brightgreen"
elif [ $(echo "$COVERAGE >= 80" | bc -l) -eq 1 ]; then
COLOR="green"
elif [ $(echo "$COVERAGE >= 70" | bc -l) -eq 1 ]; then
COLOR="yellow"
elif [ $(echo "$COVERAGE >= 60" | bc -l) -eq 1 ]; then
COLOR="orange"
else
COLOR="red"
fi

echo "color=$COLOR" >> $GITHUB_OUTPUT

- name: Generate PR comment body
if: github.event_name == 'pull_request'
id: comment
env:
COVERAGE: ${{ steps.coverage.outputs.coverage }}
BADGE_COLOR: ${{ steps.badge.outputs.color }}
COMMIT_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Determine status
if [ $(echo "$COVERAGE >= 80" | bc -l) -eq 1 ]; then
STATUS="βœ… PASSED - Coverage meets minimum threshold"
STATUS_EMOJI="βœ…"
else
STATUS="❌ FAILED - Coverage below minimum threshold"
STATUS_EMOJI="❌"
fi

# Create comment body using heredoc
cat > comment_final.txt << EOF
## πŸ“Š Test Coverage Report

**Overall Coverage:** ${COVERAGE}% ![Coverage](https://img.shields.io/badge/coverage-${COVERAGE}%25-${BADGE_COLOR})

**Threshold:** 80% ${STATUS_EMOJI}

<details>
<summary>Coverage by Package</summary>

\`\`\`
$(cat coverage_report.txt)
\`\`\`

</details>

---
πŸ“ˆ **Coverage Status:** ${STATUS}

<sub>Updated for commit ${COMMIT_SHA}</sub>
EOF

- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
COMMENT_BODY: ${{ steps.comment.outputs.comment_body }}
with:
script: |
const fs = require('fs');
const commentBody = fs.readFileSync('comment_final.txt', 'utf8');

// Find existing comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});

const botComment = comments.find(comment =>
comment.user.type === 'Bot' && comment.body.includes('πŸ“Š Test Coverage Report')
);

if (botComment) {
// Update existing comment
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: commentBody
});
} else {
// Create new comment
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: commentBody
});
}

- name: Generate coverage badge for main branch
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
COVERAGE: ${{ steps.coverage.outputs.coverage }}
run: |
# Determine badge color
if [ $(echo "$COVERAGE >= 90" | bc -l) -eq 1 ]; then
COLOR="brightgreen"
elif [ $(echo "$COVERAGE >= 80" | bc -l) -eq 1 ]; then
COLOR="green"
elif [ $(echo "$COVERAGE >= 70" | bc -l) -eq 1 ]; then
COLOR="yellow"
elif [ $(echo "$COVERAGE >= 60" | bc -l) -eq 1 ]; then
COLOR="orange"
else
COLOR="red"
fi

# Create badge JSON for shields.io endpoint schema
mkdir -p .github/badges
cat > .github/badges/coverage.json << EOF
{
"schemaVersion": 1,
"label": "coverage",
"message": "${COVERAGE}%",
"color": "${COLOR}"
}
EOF

echo "Generated coverage badge: ${COVERAGE}% (${COLOR})"

- name: Commit coverage badge to main
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add .github/badges/coverage.json
git diff --staged --quiet || git commit -m "chore: update coverage badge [skip ci]"
git push

- name: Upload coverage artifacts
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: |
coverage.out
coverage.html
coverage_report.txt
retention-days: 30

lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m

build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true

- name: Build
run: go build -v ./...

- name: Build CLI binary
run: go build -o pup .

- name: Verify binary
run: ./pup --version
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ coverage.html

# Output of the go coverage tool
*.out
coverage_report.txt
comment_body.txt
comment_final.txt

# Go workspace file
go.work
Expand Down
55 changes: 55 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,61 @@ See the [Automated Development Workflow](#automated-development-workflow-for-cla
- Aim for >80% code coverage
- Include integration tests for critical paths

### CI/CD and Code Coverage

**Coverage Requirements:**
- **Minimum threshold: 80%** - PRs that drop coverage below 80% will fail CI
- Coverage is automatically calculated and reported on every PR and branch
- Coverage reports are uploaded as artifacts for 30 days
- Coverage badge is automatically updated on the main branch

**CI Workflow:**
The project uses GitHub Actions with three parallel jobs that run on all branches:

1. **Test and Coverage**:
- Runs all tests with race detection
- Generates coverage reports (text, HTML)
- Checks coverage meets 80% threshold
- Comments on PR with detailed coverage breakdown
- Uploads coverage artifacts
- On main branch: Updates coverage badge in README.md

2. **Lint**:
- Runs `golangci-lint` with 5-minute timeout
- Enforces Go style and best practices

3. **Build**:
- Verifies the project builds successfully
- Builds the CLI binary
- Validates binary execution

**Coverage Badge:**
The README.md displays a live coverage badge that updates automatically on each push to main:
- Badge color indicates coverage level (green 80%+, yellow 70-80%, red <70%)
- Badge data stored in `.github/badges/coverage.json`
- Uses shields.io endpoint for dynamic display

**PR Coverage Comments:**
Every PR receives an automated comment showing:
- Overall coverage percentage with color-coded badge
- Pass/fail status against 80% threshold
- Detailed coverage breakdown by package
- Commit SHA for tracking

**Running Coverage Locally:**
```bash
# Run tests with coverage
go test -v -race -coverprofile=coverage.out -covermode=atomic ./...

# View coverage in terminal
go tool cover -func=coverage.out

# Generate HTML coverage report
go tool cover -html=coverage.out -o coverage.html
open coverage.html # macOS
xdg-open coverage.html # Linux
```

### Configuration Precedence

Configuration values are resolved in the following order (highest to lowest priority):
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Pup - Datadog API CLI Wrapper

[![CI](https://github.com/DataDog/pup/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DataDog/pup/actions/workflows/ci.yml)
[![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)
[![Go Version](https://img.shields.io/badge/go-1.21+-00ADD8?logo=go)](https://go.dev/)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)

A Go-based command-line wrapper for easy interaction with Datadog APIs.

## Features
Expand Down
2 changes: 1 addition & 1 deletion cmd/api_keys_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func TestAPIKeysCmd_Subcommands(t *testing.T) {

commandMap := make(map[string]bool)
for _, cmd := range commands {
commandMap[cmd.Use] = true
commandMap[cmd.Name()] = true
}

for _, expected := range expectedCommands {
Expand Down
5 changes: 4 additions & 1 deletion cmd/audit_logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,10 @@ func runAuditLogsSearch(cmd *cobra.Command, args []string) error {
page.SetLimit(auditLogsLimit)
body.SetPage(page)

resp, r, err := api.SearchAuditLogs(client.Context(), datadogV2.SearchAuditLogsOptionalParameters{}.WithBody(body))
opts := datadogV2.SearchAuditLogsOptionalParameters{
Body: &body,
}
resp, r, err := api.SearchAuditLogs(client.Context(), opts)
if err != nil {
if r != nil {
return fmt.Errorf("failed to search audit logs: %w (status: %d)", err, r.StatusCode)
Expand Down
Loading