diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml new file mode 100644 index 00000000..32754371 --- /dev/null +++ b/.github/workflows/test-coverage.yml @@ -0,0 +1,156 @@ +name: Test Coverage + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + coverage: + name: Test Coverage Report + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build project + run: npm run build + + - name: Run tests with coverage + run: npm run test:coverage + + - name: Generate coverage summary + id: coverage + run: | + # Read the coverage summary + COVERAGE_JSON=$(cat coverage/coverage-summary.json) + + # Extract metrics using jq + LINES_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.lines.pct') + STATEMENTS_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.statements.pct') + FUNCTIONS_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.functions.pct') + BRANCHES_PCT=$(echo "$COVERAGE_JSON" | jq -r '.total.branches.pct') + + LINES_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.lines.covered') + LINES_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.lines.total') + STATEMENTS_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.statements.covered') + STATEMENTS_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.statements.total') + FUNCTIONS_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.functions.covered') + FUNCTIONS_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.functions.total') + BRANCHES_COVERED=$(echo "$COVERAGE_JSON" | jq -r '.total.branches.covered') + BRANCHES_TOTAL=$(echo "$COVERAGE_JSON" | jq -r '.total.branches.total') + + # Create summary for GitHub Actions Summary + echo "## Test Coverage Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Coverage | Covered/Total |" >> $GITHUB_STEP_SUMMARY + echo "|--------|----------|---------------|" >> $GITHUB_STEP_SUMMARY + echo "| **Lines** | ${LINES_PCT}% | ${LINES_COVERED}/${LINES_TOTAL} |" >> $GITHUB_STEP_SUMMARY + echo "| **Statements** | ${STATEMENTS_PCT}% | ${STATEMENTS_COVERED}/${STATEMENTS_TOTAL} |" >> $GITHUB_STEP_SUMMARY + echo "| **Functions** | ${FUNCTIONS_PCT}% | ${FUNCTIONS_COVERED}/${FUNCTIONS_TOTAL} |" >> $GITHUB_STEP_SUMMARY + echo "| **Branches** | ${BRANCHES_PCT}% | ${BRANCHES_COVERED}/${BRANCHES_TOTAL} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + # Create PR comment body + COMMENT_BODY="## Test Coverage Report + + | Metric | Coverage | Covered/Total | + |--------|----------|---------------| + | **Lines** | ${LINES_PCT}% | ${LINES_COVERED}/${LINES_TOTAL} | + | **Statements** | ${STATEMENTS_PCT}% | ${STATEMENTS_COVERED}/${STATEMENTS_TOTAL} | + | **Functions** | ${FUNCTIONS_PCT}% | ${FUNCTIONS_COVERED}/${FUNCTIONS_TOTAL} | + | **Branches** | ${BRANCHES_PCT}% | ${BRANCHES_COVERED}/${BRANCHES_TOTAL} | + +
+ Coverage Thresholds + + The project has the following coverage thresholds configured: + - Lines: 38% + - Statements: 38% + - Functions: 35% + - Branches: 30% + +
+ + --- + *Coverage report generated by \\\`npm run test:coverage\\\`*" + + # Save for next step (escape newlines for GitHub Actions) + echo "COMMENT_BODY<> $GITHUB_ENV + echo "$COMMENT_BODY" >> $GITHUB_ENV + echo "EOF" >> $GITHUB_ENV + + # Also save individual metrics as outputs + echo "lines_pct=${LINES_PCT}" >> $GITHUB_OUTPUT + echo "statements_pct=${STATEMENTS_PCT}" >> $GITHUB_OUTPUT + echo "functions_pct=${FUNCTIONS_PCT}" >> $GITHUB_OUTPUT + echo "branches_pct=${BRANCHES_PCT}" >> $GITHUB_OUTPUT + + - name: Comment PR with coverage report + if: github.event_name == 'pull_request' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const commentBody = process.env.COMMENT_BODY; + + // Find existing coverage 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: Upload coverage reports + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: coverage-report + path: | + coverage/ + retention-days: 30 + + - name: Check coverage thresholds + run: | + echo "Checking if coverage meets minimum thresholds..." + # Jest will fail if coverage is below thresholds defined in jest.config.js + # This step is informational since the test:coverage command already checks diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d5eb3f0f..f70e5d0a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -109,10 +109,12 @@ logger.success('Operation completed successfully'); - Clear description of what the PR does - Reference any related issues - Include tests for new functionality - - Ensure CI passes + - Ensure CI passes (including test coverage checks) + - Review the automated coverage report posted as a PR comment 3. **Review process:** - Maintainers will review your PR + - The coverage report bot will automatically comment with test coverage metrics - Address any feedback - Once approved, your PR will be merged diff --git a/TESTING.md b/TESTING.md index 4844c27e..dcbfbfa4 100644 --- a/TESTING.md +++ b/TESTING.md @@ -53,6 +53,24 @@ After running `npm run test:coverage`, coverage reports are available in the `co - **LCOV**: `coverage/lcov.info` for integration with CI/CD tools - **JSON**: `coverage/coverage-summary.json` for programmatic access +### CI/CD Coverage Reporting + +The project includes automated test coverage reporting via GitHub Actions (`.github/workflows/test-coverage.yml`): + +- **Automatic PR Comments**: Coverage reports are automatically posted as comments on pull requests +- **GitHub Actions Summary**: Each workflow run includes a coverage summary in the job output +- **Coverage Artifacts**: Full coverage reports are uploaded as artifacts for 30 days +- **Update Strategy**: Existing coverage comments are updated on subsequent pushes to avoid comment spam + +The coverage workflow runs on: +- All pull requests to `main` +- All pushes to `main` + +Required permissions: +- `contents: read` - To checkout the repository +- `pull-requests: write` - To post/update PR comments +- `checks: write` - To update check status + ### Coverage Thresholds The project maintains the following minimum coverage thresholds (configured in `jest.config.js`):