Compare commits

...

2 Commits

Author SHA1 Message Date
huang47
4f4e6645fc ci: handle failed critical coverage summary 2026-07-01 10:03:45 -07:00
huang47
0228a18554 ci: add critical coverage PR summary 2026-07-01 00:01:49 -07:00
5 changed files with 180 additions and 4 deletions

View File

@@ -58,3 +58,11 @@ jobs:
- name: Enforce critical coverage gate
run: pnpm test:coverage:critical
- name: Upload critical coverage summary
if: always() && !cancelled() && hashFiles('coverage/coverage-summary.json') != ''
uses: actions/upload-artifact@v6
with:
name: critical-coverage-summary
path: coverage/coverage-summary.json
retention-days: 1

View File

@@ -2,7 +2,11 @@ name: 'PR: Unified Report'
on:
workflow_run:
workflows: ['CI: Size Data', 'CI: Performance Report', 'CI: E2E Coverage']
workflows:
- 'CI: Size Data'
- 'CI: Performance Report'
- 'CI: E2E Coverage'
- 'CI: Tests Unit'
types:
- completed
branches-ignore:
@@ -90,6 +94,25 @@ jobs:
path: temp/coverage
if_no_artifact_found: warn
- name: Find critical coverage workflow run
if: steps.pr-meta.outputs.skip != 'true'
id: find-critical-coverage
uses: ./.github/actions/find-workflow-run
with:
workflow-id: ci-tests-unit.yaml
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
not-found-status: skip
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download critical coverage summary
if: steps.pr-meta.outputs.skip != 'true' && (steps.find-critical-coverage.outputs.status == 'ready' || steps.find-critical-coverage.outputs.status == 'failed')
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: critical-coverage-summary
run_id: ${{ steps.find-critical-coverage.outputs.run-id }}
path: temp/critical-coverage
if_no_artifact_found: warn
- name: Download perf metrics (current)
if: steps.pr-meta.outputs.skip != 'true' && steps.find-perf.outputs.status == 'ready'
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
@@ -129,6 +152,7 @@ jobs:
--size-status=${{ steps.find-size.outputs.status }}
--perf-status=${{ steps.find-perf.outputs.status }}
--coverage-status=${{ steps.find-coverage.outputs.status }}
--critical-coverage-status=${{ steps.find-critical-coverage.outputs.status }}
> pr-report.md
- name: Remove legacy separate comments

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from 'vitest'
import {
formatCoverageMetric,
renderCriticalCoverageReport
} from './unified-report'
describe('formatCoverageMetric', () => {
it('formats covered counts and percent', () => {
expect(
formatCoverageMetric({
covered: 8,
total: 10,
pct: 80
})
).toBe('8/10 | 80.00%')
})
it('falls back when a metric is missing or invalid', () => {
expect(formatCoverageMetric()).toBe('N/A | N/A')
expect(
formatCoverageMetric({
covered: Number.NaN,
total: 10,
pct: 80
})
).toBe('N/A | N/A')
})
})
describe('renderCriticalCoverageReport', () => {
it('renders critical coverage rows from a summary', () => {
expect(
renderCriticalCoverageReport({
total: {
statements: { covered: 8, total: 10, pct: 80 },
functions: { covered: 3, total: 4, pct: 75 },
lines: { covered: 9, total: 10, pct: 90 }
}
})
).toBe(
[
'## Critical Unit Coverage',
'',
'| Metric | Covered | Coverage |',
'|---|---:|---:|',
'| Statements | 8/10 | 80.00% |',
'| Branches | N/A | N/A |',
'| Functions | 3/4 | 75.00% |',
'| Lines | 9/10 | 90.00% |'
].join('\n')
)
})
})

View File

@@ -1,5 +1,6 @@
import { execFileSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { existsSync, readFileSync } from 'node:fs'
import { pathToFileURL } from 'node:url'
const args: string[] = process.argv.slice(2)
@@ -12,6 +13,58 @@ function getArg(name: string): string | undefined {
const sizeStatus = getArg('size-status') ?? 'pending'
const perfStatus = getArg('perf-status') ?? 'pending'
const coverageStatus = getArg('coverage-status') ?? 'skip'
const criticalCoverageStatus = getArg('critical-coverage-status') ?? 'skip'
export type CoverageMetricName =
| 'statements'
| 'branches'
| 'functions'
| 'lines'
export type CoverageMetric = {
total: number
covered: number
pct: number
}
export type CoverageSummary = {
total?: Partial<Record<CoverageMetricName, CoverageMetric>>
}
export function formatCoverageMetric(metric?: CoverageMetric): string {
if (
!metric ||
!Number.isFinite(metric.covered) ||
!Number.isFinite(metric.total) ||
!Number.isFinite(metric.pct)
) {
return 'N/A | N/A'
}
return `${metric.covered}/${metric.total} | ${metric.pct.toFixed(2)}%`
}
export function renderCriticalCoverageReport(
summary: CoverageSummary = JSON.parse(
readFileSync('temp/critical-coverage/coverage-summary.json', 'utf-8')
) as CoverageSummary
): string {
const rows: Array<[string, CoverageMetricName]> = [
['Statements', 'statements'],
['Branches', 'branches'],
['Functions', 'functions'],
['Lines', 'lines']
]
return [
'## Critical Unit Coverage',
'',
'| Metric | Covered | Coverage |',
'|---|---:|---:|',
...rows.map(([label, key]) => {
const metric = summary.total?.[key]
return `| ${label} | ${formatCoverageMetric(metric)} |`
})
].join('\n')
}
const lines: string[] = []
@@ -97,4 +150,41 @@ if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
lines.push('> ⚠️ Coverage collection failed. Check the CI workflow logs.')
}
process.stdout.write(lines.join('\n') + '\n')
if (
(criticalCoverageStatus === 'ready' || criticalCoverageStatus === 'failed') &&
existsSync('temp/critical-coverage/coverage-summary.json')
) {
try {
lines.push('')
lines.push(renderCriticalCoverageReport())
} catch {
lines.push('')
lines.push('## Critical Unit Coverage')
lines.push('')
lines.push(
'> Failed to render critical coverage summary. Check the CI workflow logs.'
)
}
} else if (criticalCoverageStatus === 'ready') {
lines.push('')
lines.push('## Critical Unit Coverage')
lines.push('')
lines.push('> Critical coverage summary unavailable.')
} else if (criticalCoverageStatus === 'failed') {
lines.push('')
lines.push('## Critical Unit Coverage')
lines.push('')
lines.push('> Critical coverage gate failed. Check the CI workflow logs.')
} else if (criticalCoverageStatus === 'pending') {
lines.push('')
lines.push('## Critical Unit Coverage')
lines.push('')
lines.push('> Critical coverage gate is still running.')
}
if (
process.argv[1] &&
import.meta.url === pathToFileURL(process.argv[1]).href
) {
process.stdout.write(lines.join('\n') + '\n')
}

View File

@@ -715,7 +715,7 @@ export default defineConfig({
],
coverage: {
provider: 'v8',
reporter: ['text', 'json', 'html', 'lcov'],
reporter: ['text', 'json', 'json-summary', 'html', 'lcov'],
include: COVERAGE_CRITICAL
? CRITICAL_COVERAGE_INCLUDE
: ['src/**/*.{ts,vue}'],