7 minute read

개요

GitHub Actions CI/CD 파이프라인에서 테스트 커버리지를 자동으로 계산하고, GitHub Gist와 Shields.io를 활용하여 README에 동적 커버리지 뱃지를 표시하는 방법을 소개합니다.

뱃지 예시

Coverage

커버리지 비율에 따라 자동으로 색상이 변경됩니다:

  • 🟢 초록색: 80% 이상
  • 🟡 노란색: 60-80%
  • 🟠 주황색: 40-60%
  • 🔴 빨간색: 40% 미만

구현 아키텍처

GitHub Actions
    ↓
테스트 실행 & 커버리지 계산
    ↓
coverage.json 생성
    ↓
GitHub Gist API로 업데이트
    ↓
Shields.io가 Gist에서 읽음
    ↓
README 뱃지 표시

1. GitHub Gist 생성

1.1 Gist 생성

  1. https://gist.github.com/으로 이동
  2. 새로운 Gist 생성:
    • 파일명: coverage.json
    • 내용:
      {
        "schemaVersion": 1,
        "label": "coverage",
        "message": "0%",
        "color": "red"
      }
      
  3. “Create public gist” 클릭

1.2 Gist ID 확인

생성된 Gist URL에서 ID를 복사합니다:

https://gist.github.com/USERNAME/a1b2c3d4e5f6g7h8i9j0
                                ^^^^^^^^^^^^^^^^^^^^
                                이 부분이 Gist ID

2. Personal Access Token 생성

2.1 토큰 생성

  1. https://github.com/settings/tokens로 이동
  2. “Generate new token (classic)” 클릭
  3. 설정:
    • Note: coverage-badge-gist (또는 원하는 이름)
    • Expiration: 1 year (권장)
    • Scopes: gist 체크 ✓

2.2 토큰 복사

생성된 토큰(ghp_로 시작)을 복사하여 안전하게 보관합니다.

3. Repository Secrets 설정

3.1 Secret 추가

Repository → Settings → Secrets and variables → Actions로 이동하여 다음 Secrets를 추가합니다:

Secret 1: GIST_SECRET

Name: GIST_SECRET
Value: ghp_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Secret 2: GIST_ID (선택사항)

Name: GIST_ID
Value: a1b2c3d4e5f6g7h8i9j0

참고: GIST_ID는 CI 스크립트에 직접 하드코딩해도 됩니다 (공개 Gist이므로 보안 문제 없음).

4. GitHub Actions 워크플로우 설정

4.1 CI 파일 수정

.github/workflows/ci.yml 파일의 테스트 단계 이후에 다음을 추가합니다:

      - name: Test
        run: go test -race -timeout=300s -parallel=4 -coverprofile=coverage.out -cover -v ./...

      - name: Update Coverage Badge
        if: github.ref == 'refs/heads/main' && success()
        env:
          GIST_SECRET: $
          GIST_ID: $
        run: |
          if [ ! -f coverage.out ]; then
            echo "⚠️ No coverage data available, skipping badge update"
            exit 0
          fi
          
          # Extract coverage percentage
          COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
          echo "Coverage: ${COVERAGE}%"
          
          # Determine badge color based on coverage
          COLOR=$(awk -v cov="$COVERAGE" 'BEGIN {
            if (cov >= 80) print "brightgreen"
            else if (cov >= 60) print "yellow"
            else if (cov >= 40) print "orange"
            else print "red"
          }')
          echo "Badge color: $COLOR"
          
          # Create JSON for Gist update
          JSON=$(jq -n \
            --arg coverage "${COVERAGE}%" \
            --arg color "${COLOR}" \
            '{
              "description": "Code coverage badge for common-library/go",
              "files": {
                "coverage.json": {
                  "content": "{\"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"\($coverage)\", \"color\": \"\($color)\"}"
                }
              }
            }')
          
          # Update Gist
          RESPONSE=$(curl -s -X PATCH \
            -H "Authorization: token ${GIST_SECRET}" \
            -H "Accept: application/vnd.github.v3+json" \
            -d "$JSON" \
            "https://api.github.com/gists/${GIST_ID}")
          
          if echo "$RESPONSE" | jq -e '.id' > /dev/null; then
            echo "✅ Coverage badge updated successfully"
            echo "Coverage: ${COVERAGE}% (${COLOR})"
          else
            echo "❌ Failed to update coverage badge"
            echo "$RESPONSE" | jq .
            exit 1
          fi

4.2 스크립트 주요 구성 요소

파일 존재 확인

if [ ! -f coverage.out ]; then
  echo "⚠️ No coverage data available, skipping badge update"
  exit 0
fi

테스트가 실패했거나 커버리지 파일이 생성되지 않은 경우를 안전하게 처리합니다.

커버리지 계산

COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')

go tool cover를 사용하여 전체 커버리지 비율을 추출하고 % 기호를 제거합니다.

색상 로직

COLOR=$(awk -v cov="$COVERAGE" 'BEGIN {
  if (cov >= 80) print "brightgreen"
  else if (cov >= 60) print "yellow"
  else if (cov >= 40) print "orange"
  else print "red"
}')

awk의 부동소수점 비교를 사용하여 커버리지 비율에 따라 4단계 색상을 결정합니다:

  • 80% 이상: brightgreen
  • 60-80%: yellow
  • 40-60%: orange
  • 40% 미만: red

Gist 업데이트 JSON 생성

JSON=$(jq -n \
  --arg coverage "${COVERAGE}%" \
  --arg color "${COLOR}" \
  '{
    "description": "Code coverage badge for common-library/go",
    "files": {
      "coverage.json": {
        "content": "{\"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"\($coverage)\", \"color\": \"\($color)\"}"
      }
    }
  }')

jq를 사용하여 Gist API 요청에 필요한 JSON을 생성합니다. content 필드 안에 Shields.io 엔드포인트 형식의 JSON이 문자열로 포함됩니다.

Shields.io 엔드포인트 형식

최종적으로 Gist에 저장되는 coverage.json 내용:

{
  "schemaVersion": 1,
  "label": "coverage",
  "message": "85.3%",
  "color": "brightgreen"
}

Gist API 호출 및 응답 검증

RESPONSE=$(curl -s -X PATCH \
  -H "Authorization: token ${GIST_SECRET}" \
  -H "Accept: application/vnd.github.v3+json" \
  -d "$JSON" \
  "https://api.github.com/gists/${GIST_ID}")

if echo "$RESPONSE" | jq -e '.id' > /dev/null; then
  echo "✅ Coverage badge updated successfully"
  echo "Coverage: ${COVERAGE}% (${COLOR})"
else
  echo "❌ Failed to update coverage badge"
  echo "$RESPONSE" | jq .
  exit 1
fi
  • PATCH 메서드로 기존 Gist 파일 업데이트
  • Personal Access Token으로 인증
  • 응답 JSON에서 .id 필드 존재 여부로 성공 여부 확인
  • 실패 시 상세 에러 메시지 출력

5. README에 뱃지 추가

5.1 기본 뱃지

![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json)

5.2 클릭 가능한 뱃지

Gist 페이지로 링크하려면:

[![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json)](https://gist.github.com/USERNAME/GIST_ID)

5.3 스타일 커스터마이징

Shields.io 스타일 옵션:

<!-- flat-square 스타일 -->
![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json&style=flat-square)

<!-- for-the-badge 스타일 -->
![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json&style=for-the-badge)

<!-- plastic 스타일 -->
![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json&style=plastic)

6. 테스트 및 검증

6.1 로컬 테스트

로컬에서 스크립트를 테스트하려면 다음 파일을 생성합니다:

test_coverage_update.sh:

#!/bin/bash
set -e

if [ ! -f coverage.out ]; then
  echo "⚠️ No coverage data available, skipping badge update"
  exit 0
fi

COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
echo "Coverage: ${COVERAGE}%"

COLOR=$(awk -v cov="$COVERAGE" 'BEGIN {
  if (cov >= 80) print "brightgreen"
  else if (cov >= 60) print "yellow"
  else if (cov >= 40) print "orange"
  else print "red"
}')
echo "Badge color: $COLOR"

JSON=$(jq -n \
  --arg coverage "${COVERAGE}%" \
  --arg color "${COLOR}" \
  '{
    "description": "Code coverage badge",
    "files": {
      "coverage.json": {
        "content": "{\"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"\($coverage)\", \"color\": \"\($color)\"}"
      }
    }
  }')

RESPONSE=$(curl -s -X PATCH \
  -H "Authorization: token ${GIST_SECRET}" \
  -H "Accept: application/vnd.github.v3+json" \
  -d "$JSON" \
  "https://api.github.com/gists/${GIST_ID}")

if echo "$RESPONSE" | jq -e '.id' > /dev/null; then
  echo "✅ Coverage badge updated successfully"
  echo "Coverage: ${COVERAGE}% (${COLOR})"
else
  echo "❌ Failed to update coverage badge"
  echo "$RESPONSE" | jq .
  exit 1
fi

실행 방법:

# 환경 변수 설정
export GIST_SECRET="ghp_your_token"
export GIST_ID="your_gist_id"

# 커버리지 생성
go test -coverprofile=coverage.out ./...

# 스크립트 실행
chmod +x test_coverage_update.sh
./test_coverage_update.sh

6.2 CI 동작 확인

  1. 코드 푸시: main 브랜치에 푸시
  2. Actions 확인: GitHub Actions 탭에서 워크플로우 실행 확인
  3. 로그 검증:
    Coverage: 85.3%
    Badge color: brightgreen
    {
      "description": "Code coverage badge for common-library/go",
      "files": {
        "coverage.json": {
          "content": "{\"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"85.3%\", \"color\": \"brightgreen\"}"
        }
      }
    }
    ✅ Coverage badge updated successfully
    Coverage: 85.3% (brightgreen)
    

6.3 뱃지 미리보기

브라우저에서 직접 확인:

https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json

7. 트러블슈팅

7.1 Gist 업데이트 실패 (HTTP 404)

원인: GIST_ID가 잘못되었거나 Gist가 삭제됨

해결:

# Gist ID 확인
echo $GIST_ID

# Gist 존재 여부 확인
curl https://gist.github.com/USERNAME/$GIST_ID

7.2 권한 오류 (HTTP 401, 403)

원인: Personal Access Token이 만료되었거나 권한이 부족함

해결:

  1. Token 재생성 (gist scope 확인)
  2. Repository Secrets에서 GIST_SECRET 업데이트

7.3 뱃지가 표시되지 않음

원인: Gist URL이 잘못되었거나 캐시 문제

해결:

<!-- 캐시 무시 (테스트용) -->
![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USERNAME/GIST_ID/raw/coverage.json?timestamp=123)

7.4 커버리지가 0%로 표시

원인: coverage.out 파일이 생성되지 않았거나 비어있음

해결:

# 테스트 단계에서 커버리지 파일 생성 확인
- name: Test
  run: |
    go test -coverprofile=coverage.out ./...
    if [ ! -f coverage.out ]; then
      echo "❌ coverage.out 파일이 생성되지 않았습니다"
      exit 1
    fi
    echo "✅ coverage.out 파일 생성됨"

8. 고급 활용

8.1 다중 뱃지

여러 메트릭을 별도 Gist로 관리:

![Coverage](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USER/ID1/raw/coverage.json)
![Tests](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USER/ID2/raw/tests.json)
![Performance](https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/USER/ID3/raw/perf.json)

8.2 브랜치별 뱃지

# 브랜치 이름을 파일명에 포함
BRANCH_NAME=$(echo ${GITHUB_REF#refs/heads/} | sed 's/\//-/g')
FILE_NAME="coverage-${BRANCH_NAME}.json"

# Gist 업데이트 시 동적 파일명 사용
-d "{\"files\":{\"${FILE_NAME}\":{\"content\":${JSON_CONTENT}}}}"

8.3 Python 프로젝트 적용

- name: Test with coverage
  run: |
    pytest --cov=src --cov-report=term-missing | tee coverage.txt
    COVERAGE=$(grep "TOTAL" coverage.txt | awk '{print $4}' | sed 's/%//')
    
    # 색상 결정
    COLOR=$(awk -v cov="$COVERAGE" 'BEGIN {
      if (cov >= 80) print "brightgreen"
      else if (cov >= 60) print "yellow"
      else if (cov >= 40) print "orange"
      else print "red"
    }')
    
    # Gist 업데이트
    JSON=$(jq -n \
      --arg coverage "${COVERAGE}%" \
      --arg color "${COLOR}" \
      '{
        "files": {
          "coverage.json": {
            "content": "{\"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"\($coverage)\", \"color\": \"\($color)\"}"
          }
        }
      }')
    
    curl -s -X PATCH \
      -H "Authorization: token ${GIST_SECRET}" \
      -H "Accept: application/vnd.github.v3+json" \
      -d "$JSON" \
      "https://api.github.com/gists/${GIST_ID}"

8.4 JavaScript/TypeScript 프로젝트

- name: Test with coverage
  run: |
    npm test -- --coverage --coverageReporters=text-summary | tee coverage.txt
    COVERAGE=$(grep "Statements" coverage.txt | awk '{print $3}' | sed 's/%//')
    
    # 색상 결정
    COLOR=$(awk -v cov="$COVERAGE" 'BEGIN {
      if (cov >= 80) print "brightgreen"
      else if (cov >= 60) print "yellow"
      else if (cov >= 40) print "orange"
      else print "red"
    }')
    
    # Gist 업데이트
    JSON=$(jq -n \
      --arg coverage "${COVERAGE}%" \
      --arg color "${COLOR}" \
      '{
        "files": {
          "coverage.json": {
            "content": "{\"schemaVersion\": 1, \"label\": \"coverage\", \"message\": \"\($coverage)\", \"color\": \"\($color)\"}"
          }
        }
      }')
    
    curl -s -X PATCH \
      -H "Authorization: token ${GIST_SECRET}" \
      -H "Accept: application/vnd.github.v3+json" \
      -d "$JSON" \
      "https://api.github.com/gists/${GIST_ID}"

9. 장점

9.1 무료 솔루션

  • GitHub Actions 포함 (월 2,000분 무료)
  • GitHub Gist 무료
  • Shields.io 무료

9.2 실시간 업데이트

  • main 브랜치 푸시 시 자동 업데이트
  • 별도 서비스 배포 불필요

9.3 유연성

  • JSON 형식으로 완전한 제어
  • 색상, 스타일, 로고 커스터마이징 가능

9.4 간단한 구성

  • 외부 의존성 최소화
  • GitHub 인프라만 활용

10. 대안 비교

솔루션 비용 설정 난이도 커스터마이징
Gist + Shields.io 무료 쉬움 높음
Codecov 무료/유료 중간 중간
Coveralls 무료/유료 중간 낮음
정적 뱃지 무료 매우 쉬움 낮음 (수동)

참고 자료