diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 1dc664b..c3176b7 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -66,6 +66,12 @@ jobs: runs-on: ubuntu-latest needs: test if: always() + outputs: + line_coverage: ${{ steps.coverage-summary.outputs.line_coverage }} + covered_lines: ${{ steps.coverage-summary.outputs.covered_lines }} + total_lines: ${{ steps.coverage-summary.outputs.total_lines }} + coverage_report_outcome: ${{ steps.download-coverage.outcome }} + codecov_outcome: ${{ steps.codecov.outcome }} steps: - name: Checkout @@ -79,6 +85,42 @@ jobs: name: backend-coverage-report path: build/reports/jacoco/test + - name: Calculate line coverage + id: coverage-summary + if: steps.download-coverage.outcome == 'success' + shell: python + run: | + import os + from pathlib import Path + import xml.etree.ElementTree as ET + + report = Path("build/reports/jacoco/test/jacocoTestReport.xml") + output = Path(os.environ["GITHUB_OUTPUT"]) + + line_coverage = "N/A" + covered_lines = "0" + total_lines = "0" + + if report.exists(): + root = ET.parse(report).getroot() + counter = next( + (item for item in root.findall("counter") if item.get("type") == "LINE"), + None, + ) + if counter is not None: + covered = int(counter.get("covered", "0")) + missed = int(counter.get("missed", "0")) + total = covered + missed + covered_lines = str(covered) + total_lines = str(total) + if total > 0: + line_coverage = f"{covered / total * 100:.2f}%" + + with output.open("a", encoding="utf-8") as stream: + stream.write(f"line_coverage={line_coverage}\n") + stream.write(f"covered_lines={covered_lines}\n") + stream.write(f"total_lines={total_lines}\n") + - name: Upload coverage to Codecov id: codecov if: steps.download-coverage.outcome == 'success' @@ -102,3 +144,91 @@ jobs: echo "| Codecov upload | ${{ steps.codecov.outcome }} |" echo "| Commit | \`${{ github.sha }}\` |" } >> "$GITHUB_STEP_SUMMARY" + + pr-report: + name: PR report + runs-on: ubuntu-latest + needs: + - test + - coverage + if: >- + always() && + github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: read + pull-requests: write + + steps: + - name: Update PR CI report comment + uses: actions/github-script@v9 + env: + TEST_RESULT: ${{ needs.test.result }} + LINE_COVERAGE: ${{ needs.coverage.outputs.line_coverage }} + COVERED_LINES: ${{ needs.coverage.outputs.covered_lines }} + TOTAL_LINES: ${{ needs.coverage.outputs.total_lines }} + COVERAGE_REPORT_OUTCOME: ${{ needs.coverage.outputs.coverage_report_outcome }} + CODECOV_OUTCOME: ${{ needs.coverage.outputs.codecov_outcome }} + WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + CODECOV_PR_URL: https://app.codecov.io/gh/${{ github.repository }}/pull/${{ github.event.pull_request.number }} + with: + script: | + const marker = ''; + + const jobResult = (value) => ({ + success: '✅ Passed', + failure: '❌ Failed', + cancelled: '⚪ Cancelled', + skipped: '⚪ Skipped', + })[value] ?? '⚪ Unknown'; + + const stepResult = (value, successLabel) => ({ + success: `✅ ${successLabel}`, + failure: '⚠️ Failed', + cancelled: '⚪ Cancelled', + skipped: '⚪ Skipped', + })[value] ?? '⚪ Not available'; + + const lineCoverage = process.env.LINE_COVERAGE || 'N/A'; + const coveredLines = process.env.COVERED_LINES || '0'; + const totalLines = process.env.TOTAL_LINES || '0'; + + const body = [ + marker, + '## Backend CI Report', + '', + '| Item | Result |', + '| --- | --- |', + `| Tests | ${jobResult(process.env.TEST_RESULT)} |`, + `| JaCoCo line coverage | ${lineCoverage} (${coveredLines}/${totalLines} lines) |`, + `| Coverage report | ${stepResult(process.env.COVERAGE_REPORT_OUTCOME, 'Generated')} |`, + `| Codecov upload | ${stepResult(process.env.CODECOV_OUTCOME, 'Uploaded')} |`, + '', + `[View workflow run](${process.env.WORKFLOW_URL}) | [View coverage details](${process.env.CODECOV_PR_URL})`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + per_page: 100, + }); + const existing = comments.find((comment) => + comment.user?.login === 'github-actions[bot]' && comment.body?.includes(marker) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body, + }); + } diff --git a/codecov.yml b/codecov.yml index b4f2865..f4e05b4 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,6 +1,16 @@ +coverage: + status: + project: + default: + informational: true + patch: + default: + informational: true + comment: - layout: "condensed_header, diff, files" + layout: "condensed_header, condensed_files, condensed_footer" behavior: default require_changes: false require_base: false require_head: true + hide_project_coverage: false diff --git a/docs/status.md b/docs/status.md index 4a65b09..ec56a58 100644 --- a/docs/status.md +++ b/docs/status.md @@ -36,6 +36,7 @@ API 계약과 API별 상태는 `docs/api/README.md`와 `docs/api/` 하위 도메 | Issue | Scope | Status | Notes | |---|---|---|---| | [#16](https://github.com/Rewrite-Team/Rewrite-BE/issues/16) | Backend 문서 구조 및 개발 규칙 정리 | Open | 현재 문서 구조와 작업 규칙 정리 범위 | +| [#46](https://github.com/Rewrite-Team/Rewrite-BE/issues/46) | Codecov 및 PR CI 리포트 개선 | Open | Codecov informational status, JaCoCo 수치와 Codecov 상세 링크를 포함한 고정 CI 댓글 구성 | ## Current Recommended Next Work diff --git a/docs/testing.md b/docs/testing.md index ad72f9b..521d83d 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -59,3 +59,14 @@ Jacoco가 설정되어 있으며, 테스트 후 `jacocoTestReport`가 실행된 build/reports/jacoco/test/html/index.html build/reports/jacoco/test/jacocoTestReport.xml ``` + +## Pull Request CI Report + +- `test` job 실패는 기존과 동일하게 PR check를 실패 처리한다. +- Codecov project/patch status는 informational로 사용하며 커버리지 수치만 제공한다. +- 커버리지 감소나 Codecov 업로드 실패만으로 PR 병합을 차단하지 않는다. +- `coverage` job은 JaCoCo XML의 전체 line coverage와 Codecov 업로드 결과를 output으로 제공한다. +- `pr-report` job은 테스트 결과, line coverage, Codecov 업로드 상태와 Codecov PR 상세 링크를 고정 댓글로 표시한다. +- 고정 댓글은 새 workflow 실행마다 기존 `github-actions[bot]` 댓글을 갱신한다. +- PR 코드 실행 job에는 쓰기 권한을 주지 않고, checkout을 수행하지 않는 `pr-report` job에만 `pull-requests: write`를 부여한다. +- 외부 fork PR에서는 쓰기 토큰 제한을 고려해 고정 댓글 작성을 건너뛴다.