name: Codecov PR Comment on: workflow_run: workflows: - ci types: - completed permissions: contents: read pull-requests: write jobs: comment: if: >- github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' runs-on: ubuntu-24.04 steps: - name: Gather pull request context id: pr uses: actions/github-script@v7 with: script: | const run = context.payload.workflow_run; if (!run.pull_requests || run.pull_requests.length === 0) { core.info('No associated pull request. Skipping.'); return { shouldRun: false }; } const pr = run.pull_requests[0]; return { shouldRun: true, prNumber: pr.number, headSha: run.head_sha, }; - name: Fetch Codecov coverage id: coverage if: steps.pr.outputs.shouldRun == 'true' uses: actions/github-script@v7 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} COMMIT_SHA: ${{ steps.pr.outputs.headSha }} with: script: | const token = process.env.CODECOV_TOKEN; if (!token) { core.warning('CODECOV_TOKEN secret is not available; skipping comment.'); core.setOutput('shouldComment', 'false'); return; } const commitSha = process.env.COMMIT_SHA; const owner = context.repo.owner; const repo = context.repo.repo; const url = `https://codecov.io/api/v2/github/${owner}/repos/${repo}/commits/${commitSha}/report`; const maxAttempts = 10; const waitMs = 15000; const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); let data; for (let attempt = 1; attempt <= maxAttempts; attempt++) { core.info(`Fetching Codecov report (attempt ${attempt}/${maxAttempts})`); const response = await fetch(url, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', Accept: 'application/json', }, }); if (response.status === 404) { core.info('Report not ready yet (404). Waiting before retrying.'); await sleep(waitMs); continue; } if (!response.ok) { const text = await response.text(); throw new Error(`Codecov API returned ${response.status}: ${text}`); } data = await response.json(); if (data && Object.keys(data).length > 0) { break; } core.info('Report payload empty. Waiting before retrying.'); await sleep(waitMs); } if (!data) { core.warning('Unable to retrieve Codecov report after multiple attempts.'); core.setOutput('shouldComment', 'false'); return; } const totals = data.report?.totals ?? data.commit?.totals ?? data.totals; if (!totals) { core.warning('Codecov response does not contain coverage totals.'); core.setOutput('shouldComment', 'false'); return; } const compareTotals = data.report?.compare?.totals ?? data.compare?.totals; const flagsRaw = data.report?.totals_by_flag ?? data.report?.components ?? []; const toNumber = (value) => { if (value === null || value === undefined || value === '') { return undefined; } const num = Number(value); return Number.isFinite(num) ? num : undefined; }; const coverage = toNumber(totals.coverage); const baseCoverage = toNumber(compareTotals?.base_coverage ?? compareTotals?.base); const delta = toNumber( compareTotals?.coverage_change ?? compareTotals?.coverage_diff ?? totals.delta ?? totals.diff ?? totals.change, ); const formatPercent = (value) => { if (value === undefined) return '—'; return `${value.toFixed(2)}%`; }; const formatDelta = (value) => { if (value === undefined) return '—'; const sign = value >= 0 ? '+' : ''; return `${sign}${value.toFixed(2)}%`; }; const shortSha = commitSha.slice(0, 7); const lines = [ '', '**Codecov Coverage**', '', `- Head \`${shortSha}\`: ${formatPercent(coverage)}`, ]; if (baseCoverage !== undefined) { lines.push(`- Base: ${formatPercent(baseCoverage)}`); } if (delta !== undefined) { lines.push(`- Change: ${formatDelta(delta)}`); } const flagEntries = Array.isArray(flagsRaw) ? flagsRaw : Object.entries(flagsRaw).map(([name, totals]) => ({ name, totals })); const flagRows = []; for (const entry of flagEntries) { const label = entry.flag ?? entry.name ?? entry.component ?? entry.id; const entryTotals = entry.totals ?? entry; const entryCoverage = toNumber(entryTotals?.coverage); const entryDelta = toNumber( entryTotals?.coverage_change ?? entryTotals?.coverage_diff ?? entryTotals?.delta ?? entryTotals?.diff, ); if (!label || entryCoverage === undefined) { continue; } flagRows.push(`| ${label} | ${formatPercent(entryCoverage)} | ${formatDelta(entryDelta)} |`); } if (flagRows.length) { lines.push(''); lines.push('| Flag | Coverage | Change |'); lines.push('| --- | --- | --- |'); lines.push(...flagRows); } const commentBody = lines.join('\n'); const shouldComment = coverage !== undefined; core.setOutput('shouldComment', shouldComment ? 'true' : 'false'); if (shouldComment) { core.setOutput('commentBody', commentBody); } - name: Upsert coverage comment if: steps.pr.outputs.shouldRun == 'true' && steps.coverage.outputs.shouldComment == 'true' uses: actions/github-script@v7 env: PR_NUMBER: ${{ steps.pr.outputs.prNumber }} COMMENT_BODY: ${{ steps.coverage.outputs.commentBody }} with: script: | const prNumber = Number(process.env.PR_NUMBER); const body = process.env.COMMENT_BODY; const marker = ''; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, per_page: 100, }); const existing = comments.find((comment) => comment.body?.includes(marker)); if (existing) { core.info(`Updating existing coverage comment (id: ${existing.id}).`); await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: existing.id, body, }); } else { core.info('Creating new coverage comment.'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body, }); }