Skip to content

Commit c48d376

Browse files
clairevnextclaude
andcommitted
refactor(badges): replace self-hosted JSON with direct shields.io integrations
Mirrors the refactor applied to the bold-minds/oss template. Deletes the custom endpoint-badge infrastructure that required CI to write JSON files back to the repo, replaces it with direct shields.io integrations that read from public GitHub APIs. Badge block reduced from 8 to 3: - Kept: Go Reference (pkg.go.dev), Build status, Go Version - Dropped: License, Latest Release, Last Updated (GitHub's sidebar already shows all three) - Deferred: Coverage, golangci-lint issue count, Dependabot alerts (come back later via schneegans/dynamic-badges-action + gist once that infrastructure is in place) Workflow simplified: - .github/workflows/test.yaml: ~140 → 56 lines - Removed: badge generation step, badge commit step, GitHub App token bypass infrastructure, $REPO env var plumbing - Permissions: read-only throughout (was: contents: write for badge push) - No more branch-protection conflicts, no more CI auto-commits polluting git history, no more race with CodeQL required checks validate.sh simplified: - Removed generate_badges function (~120 lines) - Removed 'Badge Generation' step from main pipeline - Validation goes 9 → 8 steps, faster and cleaner Deleted: - .github/badges/ directory (6 JSON files per repo) This eliminates ~200 lines of infrastructure per repo that existed solely to self-host badge data. shields.io reads the same data (and more) directly from public APIs with zero hosting. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 35414ec commit c48d376

9 files changed

Lines changed: 16 additions & 302 deletions

File tree

.github/badges/coverage.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/badges/dependabot.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/badges/go-version.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/badges/golangci-lint.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/badges/last-updated.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/badges/lint-results.json

Lines changed: 0 additions & 1 deletion
This file was deleted.

.github/workflows/test.yaml

Lines changed: 14 additions & 151 deletions
Original file line numberDiff line numberDiff line change
@@ -6,188 +6,51 @@ on:
66
pull_request:
77
branches: [ main ]
88

9+
# Read-only by default. This workflow never writes back to the repo —
10+
# badges are served directly by shields.io from public GitHub APIs and
11+
# pkg.go.dev, so there is no self-hosted JSON to commit.
912
permissions:
1013
contents: read
1114

12-
env:
13-
GO_VERSION: '1.26'
14-
GOLANGCI_LINT_VERSION: 'v2.0.2'
15-
# Pin govulncheck — never install @latest in CI. Dependabot (see
16-
# .github/dependabot.yml) will open PRs to bump this version.
17-
GOVULNCHECK_VERSION: 'v1.1.3'
18-
19-
# Badges job pushes to main. If two merges land close together, two badge
20-
# jobs could race on `git push origin main`. Serialize them. We do NOT
21-
# cancel in progress because each run commits genuinely different state.
22-
concurrency:
23-
group: tests-${{ github.ref }}
24-
cancel-in-progress: false
25-
2615
jobs:
2716
test:
28-
permissions:
29-
contents: read
3017
strategy:
3118
fail-fast: false
3219
matrix:
3320
os: [ ubuntu-latest, windows-latest, macos-latest ]
3421
runs-on: ${{ matrix.os }}
3522
steps:
3623
- uses: actions/checkout@v4
24+
with:
25+
persist-credentials: false
3726

3827
- name: Setup Go
3928
uses: actions/setup-go@v5
4029
with:
41-
go-version: ${{ env.GO_VERSION }}
30+
go-version: '1.26'
31+
32+
- name: Install dependencies
33+
run: go mod download
4234

4335
# Build validation (cross-platform).
44-
# Pure-stdlib libraries have no go.sum, so we only diff go.mod.
36+
# Conditionally diffs go.sum only if present — pure-stdlib children have none.
4537
- name: Build validation
38+
shell: bash
4639
run: |
4740
go build ./...
4841
go mod tidy
4942
git diff --exit-code go.mod
43+
if [ -f go.sum ]; then
44+
git diff --exit-code go.sum
45+
fi
5046
5147
- name: Run tests (Unix)
5248
if: runner.os != 'Windows'
5349
run: go test -v -race -coverprofile=coverage.out ./...
5450

55-
# Windows omits -race deliberately: the Go race detector requires
56-
# CGO, and the MSVC/MinGW toolchain on windows-latest runners is
57-
# flaky enough that enabling -race here causes spurious CI failures
58-
# unrelated to the code under test. Linux + macOS still run -race,
59-
# so race bugs are caught — this is belt, not braces.
6051
- name: Run tests (Windows)
6152
if: runner.os == 'Windows'
6253
run: go test -v -coverprofile="coverage.out" ./...
6354

6455
- name: Run benchmarks
6556
run: go test -bench=. -benchmem ./...
66-
67-
- name: govulncheck
68-
if: runner.os == 'Linux'
69-
run: |
70-
go install "golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION}"
71-
govulncheck ./...
72-
73-
- name: Upload coverage artifact
74-
if: matrix.os == 'ubuntu-latest'
75-
uses: actions/upload-artifact@v4
76-
with:
77-
name: coverage-out
78-
path: coverage.out
79-
retention-days: 1
80-
81-
# Badge generation runs only on main, and is the only job that needs
82-
# contents: write. Separating it from the test matrix keeps every test
83-
# job running with read-only credentials (least privilege).
84-
#
85-
# Why badges live in the repo instead of shields.io endpoints:
86-
# shields.io cannot authenticate to our GitHub API for dependabot /
87-
# code-scanning counts. We generate JSON files here, commit them to
88-
# main, and point shields.io at the raw file URL. The `[skip ci]`
89-
# marker in the commit message breaks the CI→commit→CI loop — do not
90-
# "clean up" that marker without replacing the loop-breaker.
91-
badges:
92-
needs: test
93-
if: github.ref == 'refs/heads/main'
94-
runs-on: ubuntu-latest
95-
permissions:
96-
contents: write
97-
pull-requests: read
98-
security-events: read
99-
steps:
100-
- uses: actions/checkout@v4
101-
102-
- name: Setup Go
103-
uses: actions/setup-go@v5
104-
with:
105-
go-version: ${{ env.GO_VERSION }}
106-
107-
- name: Download coverage artifact
108-
uses: actions/download-artifact@v4
109-
with:
110-
name: coverage-out
111-
112-
- name: Generate badge data
113-
env:
114-
GH_TOKEN: ${{ github.token }}
115-
REPO: ${{ github.repository }}
116-
run: |
117-
mkdir -p .github/badges
118-
119-
# Install pinned golangci-lint (never @latest in CI).
120-
go install "github.com/golangci/golangci-lint/v2/cmd/golangci-lint@${GOLANGCI_LINT_VERSION}"
121-
122-
# Coverage badge
123-
if [[ -f "coverage.out" ]]; then
124-
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
125-
if (( $(echo "$COVERAGE >= 80" | bc -l) )); then
126-
COLOR="brightgreen"
127-
elif (( $(echo "$COVERAGE >= 60" | bc -l) )); then
128-
COLOR="yellow"
129-
else
130-
COLOR="red"
131-
fi
132-
echo '{"schemaVersion":1,"label":"coverage","message":"'$COVERAGE'%","color":"'$COLOR'"}' > .github/badges/coverage.json
133-
fi
134-
135-
# Go version badge
136-
GO_VERSION=$(go version | grep -oE 'go[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)
137-
echo '{"schemaVersion":1,"label":"Go","message":"'$GO_VERSION'","color":"00ADD8"}' > .github/badges/go-version.json
138-
139-
# Last updated badge
140-
LAST_COMMIT_DATE=$(git log -1 --format=%cd --date=short)
141-
echo '{"schemaVersion":1,"label":"last updated","message":"'$LAST_COMMIT_DATE'","color":"teal"}' > .github/badges/last-updated.json
142-
143-
# golangci-lint badge.
144-
# Note: `grep -c` exits 1 when there are zero matches, which
145-
# trips `set -e` and also prints 0. Using `|| true` (not
146-
# `|| echo "0"`) ensures ISSUES gets exactly one numeric value
147-
# — the earlier `|| echo "0"` produced "0\n0" on the no-match
148-
# path, which then failed the `[[ -eq ]]` numeric comparison.
149-
if golangci-lint run; then
150-
echo '{"schemaVersion":1,"label":"golangci-lint","message":"0 issues","color":"brightgreen"}' > .github/badges/golangci-lint.json
151-
else
152-
ISSUES=$(golangci-lint run 2>&1 | grep -c "^.*\.go:" || true)
153-
ISSUES=${ISSUES:-0}
154-
if [[ $ISSUES -eq 0 ]]; then
155-
echo '{"schemaVersion":1,"label":"golangci-lint","message":"passing","color":"brightgreen"}' > .github/badges/golangci-lint.json
156-
else
157-
echo '{"schemaVersion":1,"label":"golangci-lint","message":"'$ISSUES' issues","color":"red"}' > .github/badges/golangci-lint.json
158-
fi
159-
fi
160-
161-
# Security/dependabot badge — queries THIS repo via $REPO env var
162-
DEPENDABOT_ALERTS=$(gh api "repos/$REPO/dependabot/alerts" --jq 'length' 2>/dev/null || echo "0")
163-
CODE_SCANNING_ALERTS=$(gh api "repos/$REPO/code-scanning/alerts" --jq '[.[] | select(.state == "open")] | length' 2>/dev/null || echo "0")
164-
TOTAL_ALERTS=$((DEPENDABOT_ALERTS + CODE_SCANNING_ALERTS))
165-
OPEN_PRS=$(gh pr list --author "app/dependabot" --state open --json number --jq 'length' 2>/dev/null || echo "0")
166-
167-
if [[ $TOTAL_ALERTS -gt 0 ]]; then
168-
if [[ $DEPENDABOT_ALERTS -gt 0 && $CODE_SCANNING_ALERTS -gt 0 ]]; then
169-
echo '{"schemaVersion":1,"label":"security","message":"'$TOTAL_ALERTS' alerts","color":"red"}' > .github/badges/dependabot.json
170-
elif [[ $DEPENDABOT_ALERTS -gt 0 ]]; then
171-
echo '{"schemaVersion":1,"label":"security","message":"'$DEPENDABOT_ALERTS' dependency alerts","color":"red"}' > .github/badges/dependabot.json
172-
else
173-
echo '{"schemaVersion":1,"label":"security","message":"'$CODE_SCANNING_ALERTS' code alerts","color":"red"}' > .github/badges/dependabot.json
174-
fi
175-
elif [[ $OPEN_PRS -gt 0 ]]; then
176-
echo '{"schemaVersion":1,"label":"dependabot","message":"'$OPEN_PRS' updates","color":"blue"}' > .github/badges/dependabot.json
177-
else
178-
echo '{"schemaVersion":1,"label":"security","message":"all clear","color":"brightgreen"}' > .github/badges/dependabot.json
179-
fi
180-
181-
- name: Commit badges to main
182-
env:
183-
RUN_NUMBER: ${{ github.run_number }}
184-
run: |
185-
git config --global user.name "github-actions[bot]"
186-
git config --global user.email "github-actions[bot]@users.noreply.github.com"
187-
git add .github/badges/
188-
if git diff --staged --quiet; then
189-
echo "No badge changes to commit"
190-
else
191-
git commit -m "Update badges from CI run $RUN_NUMBER [skip ci]"
192-
git push origin main
193-
fi

README.md

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,8 @@
11
# dig
22

3-
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
43
[![Go Reference](https://pkg.go.dev/badge/github.com/bold-minds/dig.svg)](https://pkg.go.dev/github.com/bold-minds/dig)
5-
[![Go Version](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/bold-minds/dig/main/.github/badges/go-version.json)](https://golang.org/doc/go1.26)
6-
[![Latest Release](https://img.shields.io/github/v/release/bold-minds/dig?logo=github&color=blueviolet)](https://github.com/bold-minds/dig/releases)
7-
[![Last Updated](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/bold-minds/dig/main/.github/badges/last-updated.json)](https://github.com/bold-minds/dig/commits)
8-
[![golangci-lint](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/bold-minds/dig/main/.github/badges/golangci-lint.json)](https://github.com/bold-minds/dig/actions/workflows/test.yaml)
9-
[![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/bold-minds/dig/main/.github/badges/coverage.json)](https://github.com/bold-minds/dig/actions/workflows/test.yaml)
10-
[![Dependabot](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/bold-minds/dig/main/.github/badges/dependabot.json)](https://github.com/bold-minds/dig/security/dependabot)
4+
[![Build](https://img.shields.io/github/actions/workflow/status/bold-minds/dig/test.yaml?branch=main&label=tests)](https://github.com/bold-minds/dig/actions/workflows/test.yaml)
5+
[![Go Version](https://img.shields.io/github/go-mod/go-version/bold-minds/dig)](go.mod)
116

127
**Nested data navigation for Go, one line.**
138

scripts/validate.sh

Lines changed: 0 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -348,143 +348,6 @@ final_validation() {
348348
return 0
349349
}
350350

351-
# 🏷️ Generate badge JSON files for debugging and CI compatibility
352-
generate_badges() {
353-
print_info "Generating badge JSON files..."
354-
355-
# Create badges directory in .github
356-
mkdir -p .github/badges
357-
358-
# Add GOPATH/bin to PATH if not already there (for golangci-lint).
359-
# Declare and assign separately so `set -e` still sees a failing `go env`.
360-
local gopath_bin
361-
gopath_bin="$(go env GOPATH)/bin"
362-
if [[ ":$PATH:" != *":$gopath_bin:"* ]]; then
363-
export PATH="$gopath_bin:$PATH"
364-
fi
365-
366-
# Generate golangci-lint badge
367-
if command -v golangci-lint >/dev/null 2>&1; then
368-
print_info "Running golangci-lint with JSON output for badge generation..."
369-
370-
# Run golangci-lint with JSON output (allow failure to capture issues)
371-
local lint_json_output
372-
lint_json_output=$(golangci-lint run --out-format json ./... 2>/dev/null || echo '{"Issues":null}')
373-
374-
# Save raw output for debugging
375-
echo "$lint_json_output" > .github/badges/lint-results.json
376-
377-
# Count issues (handle both null and empty array cases)
378-
local issues_count
379-
if command -v jq >/dev/null 2>&1; then
380-
issues_count=$(echo "$lint_json_output" | jq '.Issues | length // 0' 2>/dev/null || echo "0")
381-
else
382-
# Fallback without jq - count occurrences of issue objects
383-
if [[ "$lint_json_output" == *'"Issues":null'* ]] || [[ "$lint_json_output" == *'"Issues":[]'* ]]; then
384-
issues_count=0
385-
else
386-
# Simple count of issue entries (rough approximation)
387-
issues_count=$(echo "$lint_json_output" | grep -o '"Pos":' | wc -l || echo "0")
388-
fi
389-
fi
390-
391-
# Generate golangci-lint badge JSON
392-
if [[ "$issues_count" -eq 0 ]]; then
393-
echo '{"schemaVersion":1,"label":"golangci-lint","message":"0 issues","color":"brightgreen"}' > .github/badges/golangci-lint.json
394-
print_info "✅ golangci-lint badge: 0 issues (green)"
395-
else
396-
echo '{"schemaVersion":1,"label":"golangci-lint","message":"'$issues_count' issues","color":"red"}' > .github/badges/golangci-lint.json
397-
print_info "❌ golangci-lint badge: $issues_count issues (red)"
398-
fi
399-
else
400-
# Fallback if golangci-lint not available
401-
echo '{"schemaVersion":1,"label":"golangci-lint","message":"not available","color":"lightgrey"}' > .github/badges/golangci-lint.json
402-
print_warning "golangci-lint not available, generated fallback badge"
403-
fi
404-
405-
# Generate coverage badge (if coverage file exists)
406-
if [[ -f "coverage.out" ]]; then
407-
# Read the total directly from the profile (same approach as
408-
# validate_coverage). No re-running `go test`.
409-
local coverage_percent
410-
coverage_percent=$(go tool cover -func=coverage.out 2>/dev/null | awk '/^total:/ {gsub("%","",$3); print $3}')
411-
if [[ -z "$coverage_percent" ]]; then
412-
coverage_percent="0"
413-
fi
414-
415-
# Determine color based on coverage
416-
local coverage_color="red"
417-
if (( $(echo "$coverage_percent >= 80" | bc -l 2>/dev/null || echo "0") )); then
418-
coverage_color="brightgreen"
419-
elif (( $(echo "$coverage_percent >= 60" | bc -l 2>/dev/null || echo "0") )); then
420-
coverage_color="yellow"
421-
fi
422-
423-
echo '{"schemaVersion":1,"label":"coverage","message":"'$coverage_percent'%","color":"'$coverage_color'"}' > .github/badges/coverage.json
424-
print_info "📊 Coverage badge: $coverage_percent% ($coverage_color)"
425-
else
426-
echo '{"schemaVersion":1,"label":"coverage","message":"no data","color":"lightgrey"}' > .github/badges/coverage.json
427-
print_info "📊 Coverage badge: no data available"
428-
fi
429-
430-
# Generate Go version badge
431-
local go_version
432-
go_version=$(go version | grep -oE 'go[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)
433-
echo '{"schemaVersion":1,"label":"Go","message":"'$go_version'","color":"00ADD8"}' > .github/badges/go-version.json
434-
print_info "🐹 Go version badge: $go_version (Go blue)"
435-
436-
# Generate last updated badge (shows when validation last ran)
437-
LAST_COMMIT_DATE=$(git log -1 --format=%cd --date=short)
438-
echo '{"schemaVersion":1,"label":"last updated","message":"'$LAST_COMMIT_DATE'","color":"teal"}' > .github/badges/last-updated.json
439-
440-
# Comprehensive security badge (Dependabot + Code Scanning).
441-
# Derive owner/repo from `git remote get-url origin` so a fork
442-
# running `./scripts/validate.sh` queries its own alerts, not
443-
# upstream's. Previously hardcoded to bold-minds/dig — forks would
444-
# silently read upstream numbers and get the wrong answer.
445-
local repo_slug=""
446-
if command -v gh >/dev/null 2>&1; then
447-
repo_slug=$(git remote get-url origin 2>/dev/null \
448-
| sed -E 's#(^git@github\.com:|^https://github\.com/)##; s#\.git$##' \
449-
|| echo "")
450-
fi
451-
if [[ -z "$repo_slug" ]]; then
452-
# Either gh is not installed, or no git remote is configured.
453-
# Emit a neutral badge and move on — the rest of the function
454-
# still needs to produce go-version and last-updated badges.
455-
echo '{"schemaVersion":1,"label":"security","message":"unknown","color":"lightgrey"}' > .github/badges/dependabot.json
456-
print_warning "Security badge: gh or git remote missing; emitted neutral badge"
457-
else
458-
DEPENDABOT_ALERTS=$(gh api "repos/$repo_slug/dependabot/alerts" --jq 'length' 2>/dev/null || echo "0")
459-
CODE_SCANNING_ALERTS=$(gh api "repos/$repo_slug/code-scanning/alerts" --jq '[.[] | select(.state == "open")] | length' 2>/dev/null || echo "0")
460-
TOTAL_ALERTS=$((DEPENDABOT_ALERTS + CODE_SCANNING_ALERTS))
461-
OPEN_PRS=$(gh pr list --author "app/dependabot" --state open --json number --jq 'length' 2>/dev/null || echo "0")
462-
463-
if [[ $TOTAL_ALERTS -gt 0 ]]; then
464-
if [[ $DEPENDABOT_ALERTS -gt 0 && $CODE_SCANNING_ALERTS -gt 0 ]]; then
465-
echo '{"schemaVersion":1,"label":"security","message":"'$TOTAL_ALERTS' alerts","color":"red"}' > .github/badges/dependabot.json
466-
print_info "🔴 Security badge: $TOTAL_ALERTS total alerts ($DEPENDABOT_ALERTS dependency + $CODE_SCANNING_ALERTS code scanning)"
467-
elif [[ $DEPENDABOT_ALERTS -gt 0 ]]; then
468-
echo '{"schemaVersion":1,"label":"security","message":"'$DEPENDABOT_ALERTS' dependency alerts","color":"red"}' > .github/badges/dependabot.json
469-
print_info "🔴 Security badge: $DEPENDABOT_ALERTS dependency alerts (red)"
470-
else
471-
echo '{"schemaVersion":1,"label":"security","message":"'$CODE_SCANNING_ALERTS' code alerts","color":"red"}' > .github/badges/dependabot.json
472-
print_info "🔴 Security badge: $CODE_SCANNING_ALERTS code scanning alerts (red)"
473-
fi
474-
elif [[ $OPEN_PRS -gt 0 ]]; then
475-
echo '{"schemaVersion":1,"label":"dependabot","message":"'$OPEN_PRS' updates","color":"blue"}' > .github/badges/dependabot.json
476-
print_info "🔵 Security badge: $OPEN_PRS pending updates (blue)"
477-
else
478-
echo '{"schemaVersion":1,"label":"security","message":"all clear","color":"brightgreen"}' > .github/badges/dependabot.json
479-
print_info "🟢 Security badge: all clear (green)"
480-
fi
481-
fi
482-
483-
print_info "Badge JSON files generated in ./.github/badges/ directory 🏷️"
484-
print_info "Files created: golangci-lint.json, coverage.json, go-version.json, last-updated.json, dependabot.json, lint-results.json"
485-
486-
return 0
487-
}
488351

489352
# 📈 Performance summary
490353
print_summary() {
@@ -539,7 +402,6 @@ main() {
539402
run_step "Coverage Check" "validate_coverage" "📊" || exit 1
540403
run_step "Documentation" "validate_documentation" "📚" || exit 1
541404
run_step "Final Validation" "final_validation" "🧹" || exit 1
542-
run_step "Badge Generation" "generate_badges" "🏷️" || exit 1
543405

544406
print_summary
545407

0 commit comments

Comments
 (0)