diff --git a/.github/workflows/coverage-comment.yml b/.github/workflows/coverage-comment.yml new file mode 100644 index 00000000..3069c689 --- /dev/null +++ b/.github/workflows/coverage-comment.yml @@ -0,0 +1,119 @@ +name: Post Coverage Comment + +on: + workflow_run: + workflows: ["Run Unit Tests"] + types: + - completed + +permissions: + pull-requests: write + actions: read + +jobs: + comment: + runs-on: ubuntu-latest + if: > + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: Download Coverage Artifacts + uses: actions/download-artifact@v4 + with: + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + name: coverage-data + + - name: Upload Coverage Report + id: upload-report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: coverage/ + retention-days: 14 + + - name: Post Comment + uses: actions/github-script@v6 + env: + ARTIFACT_URL: ${{ steps.upload-report.outputs.artifact-url }} + with: + script: | + const fs = require('fs'); + + const loadSummary = (path) => { + try { + return JSON.parse(fs.readFileSync(path, 'utf8')); + } catch (e) { + console.log(`Could not read ${path}: ${e}`); + return null; + } + }; + + const mainSummary = loadSummary('./coverage-main-summary.json'); + const prSummary = loadSummary('./coverage-pr-summary.json'); + + if (!mainSummary || !prSummary) { + console.log("Missing coverage data, skipping comment."); + return; + } + + let markdown = `### 🧪 Code Coverage\n\n`; + + markdown += `[⬇️ **Download Full Report**](${process.env.ARTIFACT_URL})\n\n`; + markdown += `| Metric | Main | PR | Delta |\n`; + markdown += `| :--- | :---: | :---: | :---: |\n`; + + const metrics = ['lines', 'statements', 'functions', 'branches']; + const getPct = (summary, metric) => summary.total[metric].pct; + + metrics.forEach(metric => { + const oldPct = getPct(mainSummary, metric); + const newPct = getPct(prSummary, metric); + const diff = (newPct - oldPct).toFixed(2); + + let icon = ''; + if (diff > 0) icon = '🟢'; + else if (diff < 0) icon = '🔴'; + else icon = '⚪️'; + + const diffStr = diff > 0 ? `+${diff}%` : `${diff}%`; + markdown += `| **${metric}** | ${oldPct}% | ${newPct}% | ${icon} ${diffStr} |\n`; + }); + + markdown += `\n_Generated by [coverage-comment.yml](https://github.com/a2aproject/a2a-js/actions/workflows/coverage-comment.yml)_`; + + const prNumber = fs.readFileSync('./PR_NUMBER', 'utf8').trim(); + + if (!prNumber) { + console.log("No PR number found."); + return; + } + + const { owner, repo } = context.repo; + + const comments = await github.rest.issues.listComments({ + owner, + repo, + issue_number: prNumber, + }); + + const existingComment = comments.data.find(c => + c.body.includes('Generated by [coverage-comment.yml]') && + c.user.type === 'Bot' + ); + + if (existingComment) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existingComment.id, + body: markdown + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: prNumber, + body: markdown + }); + } \ No newline at end of file diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a486991d..0f06e241 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -85,83 +85,22 @@ jobs: run: | npm ci npm run coverage + mv coverage/coverage-summary.json coverage-pr-summary.json + mv ${{ runner.temp }}/coverage-main-summary.json coverage-main-summary.json - - name: Upload Artifact (PR) - id: upload-artifact-pr + - name: Save PR number + run: | + echo ${{ github.event.number }} > ./PR_NUMBER + + - name: Upload Coverage Artifacts uses: actions/upload-artifact@v4 if: github.event_name == 'pull_request' with: - name: coverage-report - path: coverage - retention-days: 14 - - - name: Post Comment - uses: actions/github-script@v6 - if: github.event_name == 'pull_request' - env: - ARTIFACT_URL: ${{ steps.upload-artifact-pr.outputs.artifact-url }} - with: - script: | - const fs = require('fs'); - - const loadSummary = (path) => { - return JSON.parse(fs.readFileSync(path, 'utf8')); - }; - - let markdown = `### 🧪 Code Coverage\n\n`; - markdown += `[⬇️ **Download HTML Report**](${process.env.ARTIFACT_URL})\n\n`; - markdown += `| Metric | Main | PR | Delta |\n`; - markdown += `| :--- | :---: | :---: | :---: |\n`; - - const metrics = ['lines', 'statements', 'functions', 'branches']; - const getPct = (summary, metric) => summary.total[metric].pct; - - const mainSummary = loadSummary('${{ runner.temp }}/coverage-main-summary.json'); - const prSummary = loadSummary('./coverage/coverage-summary.json'); - - metrics.forEach(metric => { - const oldPct = getPct(mainSummary, metric); - const newPct = getPct(prSummary, metric); - const diff = (newPct - oldPct).toFixed(2); - - let icon = ''; - if (diff > 0) icon = '🟢'; - else if (diff < 0) icon = '🔴'; - else icon = '⚪️'; - - const diffStr = diff > 0 ? `+${diff}%` : `${diff}%`; - - markdown += `| **${metric}** | ${oldPct}% | ${newPct}% | ${icon} ${diffStr} |\n`; - }); - - markdown += `\n_Generated by [unit-tests.yml](https://github.com/a2aproject/a2a-js/actions/workflows/unit-tests.yml)_`; - - const { owner, repo } = context.repo; - const issue_number = context.issue.number; - - const comments = await github.rest.issues.listComments({ - owner, - repo, - issue_number, - }); - - const existingComment = comments.data.find(c => - c.body.includes('Generated by [unit-tests.yml]') && - c.user.type === 'Bot' - ); + name: coverage-data + path: | + coverage-main-summary.json + coverage-pr-summary.json + coverage/ + PR_NUMBER + retention-days: 1 - if (existingComment) { - await github.rest.issues.updateComment({ - owner, - repo, - comment_id: existingComment.id, - body: markdown - }); - } else { - await github.rest.issues.createComment({ - owner, - repo, - issue_number, - body: markdown - }); - }