fix: remove docs/tests/comments, extract find-workflow-run action, consolidate scripts

- Remove feasibility doc (one-time analysis, not reference)
- Remove unit tests for CI scripts (change-detector tests)
- Remove organizational comments from scripts
- Extract find-workflow-run composite action from pr-report.yaml
- Remove exports from coverage-slack-notify.ts (were only for tests)
- Remove VITEST guard from coverage-slack-notify.ts
- Fix toLocaleString() in coverage-report.js (locale-dependent in CI)
This commit is contained in:
bymyself
2026-04-08 18:30:45 -07:00
parent c27759f801
commit 48d6c71898
7 changed files with 90 additions and 533 deletions

View File

@@ -0,0 +1,61 @@
name: Find Workflow Run
description: Finds a workflow run for a given commit SHA and outputs its status and run ID.
inputs:
workflow-id:
description: The workflow filename (e.g., 'ci-size-data.yaml')
required: true
head-sha:
description: The commit SHA to find runs for
required: true
not-found-status:
description: Status to output when no run exists
required: false
default: pending
token:
description: GitHub token for API access
required: true
outputs:
status:
description: One of 'ready', 'pending', 'failed', or the not-found-status value
value: ${{ steps.find.outputs.status }}
run-id:
description: The workflow run ID (only set when status is 'ready')
value: ${{ steps.find.outputs.run-id }}
runs:
using: composite
steps:
- name: Find workflow run
id: find
uses: actions/github-script@v8
with:
github-token: ${{ inputs.token }}
script: |
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: '${{ inputs.workflow-id }}',
head_sha: '${{ inputs.head-sha }}',
per_page: 1,
});
const run = runs.workflow_runs[0];
if (!run) {
core.setOutput('status', '${{ inputs.not-found-status }}');
return;
}
if (run.status !== 'completed') {
core.setOutput('status', 'pending');
return;
}
if (run.conclusion !== 'success') {
core.setOutput('status', 'failed');
return;
}
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));

View File

@@ -67,73 +67,23 @@ jobs:
core.setOutput('base', livePr.base.ref);
core.setOutput('head-sha', livePr.head.sha);
- name: Find size workflow run for this commit
- name: Find size workflow run
if: steps.pr-meta.outputs.skip != 'true'
id: find-size
uses: actions/github-script@v8
uses: ./.github/actions/find-workflow-run
with:
script: |
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'ci-size-data.yaml',
head_sha: headSha,
per_page: 1,
});
workflow-id: ci-size-data.yaml
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
token: ${{ secrets.GITHUB_TOKEN }}
const run = runs.workflow_runs[0];
if (!run) {
core.setOutput('status', 'pending');
return;
}
if (run.status !== 'completed') {
core.setOutput('status', 'pending');
return;
}
if (run.conclusion !== 'success') {
core.setOutput('status', 'failed');
return;
}
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));
- name: Find perf workflow run for this commit
- name: Find perf workflow run
if: steps.pr-meta.outputs.skip != 'true'
id: find-perf
uses: actions/github-script@v8
uses: ./.github/actions/find-workflow-run
with:
script: |
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'ci-perf-report.yaml',
head_sha: headSha,
per_page: 1,
});
const run = runs.workflow_runs[0];
if (!run) {
core.setOutput('status', 'pending');
return;
}
if (run.status !== 'completed') {
core.setOutput('status', 'pending');
return;
}
if (run.conclusion !== 'success') {
core.setOutput('status', 'failed');
return;
}
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));
workflow-id: ci-perf-report.yaml
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download size data (current)
if: steps.pr-meta.outputs.skip != 'true' && steps.find-size.outputs.status == 'ready'
@@ -154,39 +104,15 @@ jobs:
path: temp/size-prev
if_no_artifact_found: warn
- name: Find coverage workflow run for this commit
- name: Find coverage workflow run
if: steps.pr-meta.outputs.skip != 'true'
id: find-coverage
uses: actions/github-script@v8
uses: ./.github/actions/find-workflow-run
with:
script: |
const headSha = '${{ steps.pr-meta.outputs.head-sha }}';
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'ci-tests-e2e-coverage.yaml',
head_sha: headSha,
per_page: 1,
});
const run = runs.workflow_runs[0];
if (!run) {
core.setOutput('status', 'skip');
return;
}
if (run.status !== 'completed') {
core.setOutput('status', 'pending');
return;
}
if (run.conclusion !== 'success') {
core.setOutput('status', 'failed');
return;
}
core.setOutput('status', 'ready');
core.setOutput('run-id', String(run.id));
workflow-id: ci-tests-e2e-coverage.yaml
head-sha: ${{ steps.pr-meta.outputs.head-sha }}
not-found-status: skip
token: ${{ secrets.GITHUB_TOKEN }}
- name: Download coverage data
if: steps.pr-meta.outputs.skip != 'true' && steps.find-coverage.outputs.status == 'ready'

View File

@@ -1,234 +0,0 @@
# Playwright Code Coverage Feasibility Summary
## Objective
Evaluate approaches for collecting code coverage data from Playwright E2E tests in the ComfyUI frontend project, enabling the team to identify untested code paths exercised during browser-level integration tests.
## Current Project Context
| Aspect | Details |
| -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| **Bundler** | Vite (via `vitest/config`'s `defineConfig`) |
| **Unit test coverage** | `@vitest/coverage-v8` already in devDependencies |
| **Playwright tests** | 59 spec files in `browser_tests/tests/` |
| **Playwright fixture** | Custom `comfyPageFixture` extending `@playwright/test`'s `base.extend` |
| **Browser targets** | Chromium-only (Desktop Chrome, Mobile Chrome, 2x/0.5x scale variants). Firefox/WebKit are commented out in `playwright.config.ts` |
| **Test runner** | `pnpm test:browser` / `pnpm test:browser:local` |
| **Source maps** | Enabled by default (`GENERATE_SOURCEMAP !== 'false'`) |
| **Build configuration** | Complex — conditional plugins for Sentry, DevTools, font exclusion, meta injection, etc. |
| **Global setup/teardown** | Backs up/restores ComfyUI user data; writes perf reports on teardown |
| **Vitest coverage config** | `coverage: { reporter: ['text', 'json', 'html'] }` in `vite.config.mts` |
## Approach 1: V8 Coverage via `page.coverage` API
### How It Works
1. Call `page.coverage.startJSCoverage()` before each test navigates to the app.
2. Call `page.coverage.stopJSCoverage()` after each test, receiving raw V8 coverage entries (byte offsets, function ranges).
3. Convert V8 format to Istanbul/lcov using the `v8-to-istanbul` npm package.
4. Merge per-test coverage files and generate reports with `nyc report` or `istanbul`.
### Wrapper Package: `@bgotink/playwright-coverage`
This package wraps the above workflow into a Playwright fixture and reporter:
- Provides a custom `test.extend()` fixture that auto-starts/stops coverage.
- Includes a Playwright reporter that merges and outputs Istanbul-format `.json` files.
- Handles source map resolution for mapping bundled code back to source files.
### Integration Point
The `comfyPageFixture` already extends `base.extend<{ comfyPage: ComfyPage }>`. A coverage fixture would either:
- Wrap `comfyPageFixture` with an additional fixture layer, or
- Be added as a separate fixture composed alongside `comfyPage`.
### Pros
- **No build modifications** — works with the existing production build.
- **Lower runtime overhead** — V8 coverage is built into the engine; no instrumentation step.
- **Simpler setup** — no conditional Vite plugin configuration.
- **Familiar tooling** — team already uses `@vitest/coverage-v8` (same V8 engine).
### Cons
- **Chromium-only** — `page.coverage` is a CDP (Chrome DevTools Protocol) API. If Firefox/WebKit projects are ever enabled, coverage won't work there.
- **Source map accuracy** — V8 reports coverage against bundled code. Source map resolution can produce imprecise mappings, especially with heavily transformed code (Vue SFCs, TypeScript, Tailwind).
- **Bundle-level granularity** — coverage is per-bundle-chunk, not per-source-file. Vendor chunks and code-split modules may produce noisy data.
- **Vue SFC blind spots** — template compilation and `<script setup>` transforms can cause missed or phantom coverage lines.
### Estimated Effort
**12 days** to integrate `@bgotink/playwright-coverage` into the existing fixture, configure source map resolution, and generate initial reports.
---
## Approach 2: Istanbul Instrumentation via `vite-plugin-istanbul`
### How It Works
1. Add `vite-plugin-istanbul` to the Vite plugin chain, gated behind an environment variable (e.g., `INSTRUMENT_COVERAGE=true`).
2. The plugin instruments source files at build/serve time with Istanbul counters, exposing `window.__coverage__` at runtime.
3. After each test, collect `window.__coverage__` via `page.evaluate(() => window.__coverage__)` and write it to `.nyc_output/`.
4. Run `nyc report --reporter=html --reporter=lcov` to generate coverage reports.
### Reference Implementation
[mxschmitt/playwright-test-coverage](https://github.com/mxschmitt/playwright-test-coverage) demonstrates this exact pattern with Vite + `vite-plugin-istanbul` + Playwright.
### Integration Point
Add a Playwright fixture that:
```typescript
// coverage-fixture.ts (simplified)
import { test as base } from '@playwright/test'
import fs from 'fs'
import path from 'path'
import crypto from 'crypto'
export const test = base.extend({
context: async ({ context }, use) => {
await context.addInitScript(() => {
// Reset coverage for each context if needed
})
await use(context)
},
page: async ({ page }, use) => {
await use(page)
// Collect coverage after test
const coverage = await page.evaluate(() => (window as any).__coverage__)
if (coverage) {
const outputDir = path.join(process.cwd(), '.nyc_output')
fs.mkdirSync(outputDir, { recursive: true })
const id = crypto.randomUUID()
fs.writeFileSync(
path.join(outputDir, `${id}.json`),
JSON.stringify(coverage)
)
}
}
})
```
### Vite Configuration Change
```typescript
// In vite.config.mts — add conditionally
import istanbul from 'vite-plugin-istanbul'
const INSTRUMENT_COVERAGE = process.env.INSTRUMENT_COVERAGE === 'true'
// Add to plugins array:
...(INSTRUMENT_COVERAGE
? [istanbul({
include: 'src/*',
exclude: ['node_modules', 'browser_tests', 'tests-ui'],
extension: ['.ts', '.vue'],
requireEnv: true,
forceBuildInstrument: true // Also instrument production builds
})]
: [])
```
### Pros
- **Cross-browser** — works on any browser Playwright supports (if Firefox/WebKit are ever re-enabled).
- **Source-level accuracy** — instrumentation happens before bundling, so coverage maps directly to original `.ts` and `.vue` files.
- **Vue SFC support** — instruments the compiled output of `<script setup>`, capturing template-driven code paths accurately.
- **Proven pattern** — well-documented reference implementations exist. Standard Istanbul tooling (`nyc`, `istanbul`) for reporting.
- **Mergeable with unit coverage** — both Istanbul and V8 can produce lcov format, enabling merged reports across Vitest unit tests and Playwright E2E tests.
### Cons
- **Build modification required** — adds a conditional Vite plugin, increasing config complexity in an already complex `vite.config.mts`.
- **Performance overhead** — Istanbul instrumentation adds ~1030% runtime overhead and 23× bundle size increase due to injected counter code.
- **Dev server impact** — if used with `pnpm dev`, instrumented code is slower. Must be gated behind an env var.
- **Maintenance burden** — `vite-plugin-istanbul` must stay compatible with Vite version upgrades.
- **CI build time** — instrumented builds take longer; may need a separate CI step or matrix entry.
### Estimated Effort
**23 days** to add `vite-plugin-istanbul` with conditional gating, create the coverage collection fixture, integrate with `nyc report`, and verify source mapping accuracy for Vue SFCs.
---
## Side-by-Side Comparison
| Criteria | V8 (`@bgotink/playwright-coverage`) | Istanbul (`vite-plugin-istanbul`) |
| ----------------------------- | ---------------------------------------- | ---------------------------------- |
| **Browser support** | Chromium only | All browsers |
| **Build changes** | None | Conditional Vite plugin |
| **Source map accuracy** | Good (post-bundle) | Excellent (pre-bundle) |
| **Vue SFC coverage** | Partial (template compilation artifacts) | Full (instruments compiled output) |
| **Runtime overhead** | Low (~5%) | Medium (~1030%) |
| **Bundle size impact** | None | 23× increase when instrumented |
| **Setup complexity** | Low | Medium |
| **Maintenance** | Low (Playwright built-in API) | Medium (plugin compatibility) |
| **Mergeable with Vitest** | Yes (via lcov) | Yes (via lcov) |
| **Existing team familiarity** | High (`@vitest/coverage-v8`) | Low (new tooling) |
| **Estimated effort** | 12 days | 23 days |
## Risk Assessment
### V8 Approach Risks
1. **Source map resolution failures** — the project uses multiple Vite transforms (Vue SFCs, TypeScript, Tailwind, custom plugins). V8 coverage relies on source maps being accurate after all transforms. Inaccurate maps → misleading coverage data.
2. **Code splitting noise** — the project uses aggressive code splitting (15+ vendor chunks via `rolldownOptions.output.codeSplitting`). Coverage for vendor code will pollute reports unless carefully filtered.
3. **Low risk of breakage**`page.coverage` is a stable Playwright/CDP API.
### Istanbul Approach Risks
1. **Build config complexity**`vite.config.mts` is already ~650 lines with many conditional plugins. Adding another conditional plugin increases the surface area for config bugs.
2. **Plugin compatibility** — the project recently migrated to Rolldown (`rolldownOptions`). `vite-plugin-istanbul` may not yet fully support Rolldown's output format. **This requires validation before committing to this approach.**
3. **CI pipeline impact** — instrumented builds will need separate caching and possibly a dedicated CI job to avoid slowing down the main test pipeline.
## Recommendation
### Primary: Istanbul via `vite-plugin-istanbul`
The Istanbul approach is recommended as the primary path for the following reasons:
1. **Source-level accuracy matters** — the project is a large Vue 3 + TypeScript codebase with complex SFC compilation. Pre-bundle instrumentation produces the most trustworthy coverage data.
2. **Vite-native integration** — as a Vite-based project, `vite-plugin-istanbul` integrates naturally with the existing build pipeline.
3. **Future-proof** — if Firefox/WebKit projects are ever re-enabled (they're commented out in `playwright.config.ts`), coverage will continue to work without changes.
4. **Merged reporting** — Istanbul output can be combined with Vitest's existing `@vitest/coverage-v8` output to produce a unified coverage view of unit + E2E tests.
### Fallback: V8 via `@bgotink/playwright-coverage`
If the Istanbul approach encounters blocking issues (e.g., `vite-plugin-istanbul` incompatibility with Rolldown), the V8 approach is a viable fallback:
- Zero build changes needed.
- Quick to prototype (12 days).
- Good enough for Chromium-only coverage (which is the current test target).
### Recommended Implementation Path
1. **Validate Rolldown compatibility** — install `vite-plugin-istanbul` and confirm it works with the project's Rolldown-based build. If it doesn't, fall back to V8.
2. **Add conditional instrumentation** — gate behind `INSTRUMENT_COVERAGE=true` env var in `vite.config.mts`.
3. **Create coverage fixture** — extend `comfyPageFixture` to collect `window.__coverage__` after each test and write to `.nyc_output/`.
4. **Add `nyc` report generation** — add a `pnpm test:browser:coverage` script that runs tests with instrumentation, then generates HTML/lcov reports.
5. **CI integration** — add an optional CI job (not on the critical path) that runs instrumented tests and uploads coverage reports.
6. **Merge with unit coverage (stretch goal)** — combine Playwright lcov output with Vitest lcov output for a unified coverage dashboard.
### Suggested Package Additions
```jsonc
// devDependencies
{
"vite-plugin-istanbul": "^6.0.2",
"nyc": "^17.1.0"
// OR for V8 fallback:
// "@bgotink/playwright-coverage": "^0.3.0",
// "v8-to-istanbul": "^9.3.0"
}
```
### Suggested Script Additions
```jsonc
// package.json scripts
{
"test:browser:coverage": "INSTRUMENT_COVERAGE=true pnpm test:browser && nyc report --reporter=html --reporter=lcov --temp-dir=.nyc_output --report-dir=coverage/playwright"
}
```

View File

@@ -1,11 +1,6 @@
// @ts-check
import { existsSync, readFileSync } from 'node:fs'
/**
* Generates a markdown coverage report from lcov data.
* Output format matches the unified PR report style (size + perf sections).
*/
const lcovPath = process.argv[2] || 'coverage/playwright/coverage.lcov'
if (!existsSync(lcovPath)) {
@@ -17,7 +12,6 @@ if (!existsSync(lcovPath)) {
const lcov = readFileSync(lcovPath, 'utf-8')
// Parse lcov summary
let totalLines = 0
let coveredLines = 0
let totalFunctions = 0
@@ -76,16 +70,15 @@ lines.push('')
lines.push('| Metric | Covered | Total | Pct | |')
lines.push('|---|--:|--:|--:|---|')
lines.push(
`| Lines | ${coveredLines.toLocaleString()} | ${totalLines.toLocaleString()} | ${pct(coveredLines, totalLines)} | ${bar(coveredLines, totalLines)} |`
`| Lines | ${coveredLines} | ${totalLines} | ${pct(coveredLines, totalLines)} | ${bar(coveredLines, totalLines)} |`
)
lines.push(
`| Functions | ${coveredFunctions.toLocaleString()} | ${totalFunctions.toLocaleString()} | ${pct(coveredFunctions, totalFunctions)} | ${bar(coveredFunctions, totalFunctions)} |`
`| Functions | ${coveredFunctions} | ${totalFunctions} | ${pct(coveredFunctions, totalFunctions)} | ${bar(coveredFunctions, totalFunctions)} |`
)
lines.push(
`| Branches | ${coveredBranches.toLocaleString()} | ${totalBranches.toLocaleString()} | ${pct(coveredBranches, totalBranches)} | ${bar(coveredBranches, totalBranches)} |`
`| Branches | ${coveredBranches} | ${totalBranches} | ${pct(coveredBranches, totalBranches)} | ${bar(coveredBranches, totalBranches)} |`
)
// Top uncovered files
const uncovered = [...fileStats.entries()]
.filter(([, s]) => s.lines > 0)
.map(([file, s]) => ({

View File

@@ -1,183 +0,0 @@
import { describe, expect, it } from 'vitest'
import {
buildMilestoneBlock,
crossedMilestone,
formatCoverageRow,
formatDelta,
formatPct,
parseArgs,
parseLcovContent,
progressBar
} from './coverage-slack-notify'
describe('parseLcovContent', () => {
it('parses valid lcov content', () => {
const content = [
'SF:src/foo.ts',
'LF:100',
'LH:75',
'end_of_record',
'SF:src/bar.ts',
'LF:200',
'LH:150',
'end_of_record'
].join('\n')
const result = parseLcovContent(content)
expect(result).toEqual({
totalLines: 300,
coveredLines: 225,
percentage: 75
})
})
it('returns null for empty content', () => {
expect(parseLcovContent('')).toBeNull()
})
it('returns null when total lines is zero', () => {
expect(parseLcovContent('SF:src/foo.ts\nend_of_record')).toBeNull()
})
it('handles malformed LF/LH values gracefully', () => {
const content = 'LF:abc\nLH:50\n'
expect(parseLcovContent(content)).toBeNull()
})
it('handles NaN in LH with valid LF', () => {
const content = 'LF:100\nLH:xyz\n'
const result = parseLcovContent(content)
expect(result).toEqual({
totalLines: 100,
coveredLines: 0,
percentage: 0
})
})
})
describe('progressBar', () => {
it('returns all filled for 100%', () => {
expect(progressBar(100)).toBe('████████████████████')
})
it('returns all empty for 0%', () => {
expect(progressBar(0)).toBe('░░░░░░░░░░░░░░░░░░░░')
})
it('returns half filled for 50%', () => {
const bar = progressBar(50)
expect(bar).toBe('██████████░░░░░░░░░░')
})
})
describe('formatPct', () => {
it('formats with one decimal place', () => {
expect(formatPct(75.123)).toBe('75.1%')
})
it('formats zero', () => {
expect(formatPct(0)).toBe('0.0%')
})
})
describe('formatDelta', () => {
it('adds + sign for positive delta', () => {
expect(formatDelta(2.5)).toBe('+2.5%')
})
it('adds - sign for negative delta', () => {
expect(formatDelta(-1.3)).toBe('-1.3%')
})
it('adds + sign for zero', () => {
expect(formatDelta(0)).toBe('+0.0%')
})
})
describe('crossedMilestone', () => {
it('detects crossing from 14.9 to 15.1', () => {
expect(crossedMilestone(14.9, 15.1)).toBe(15)
})
it('detects crossing from 79.9 to 80.1', () => {
expect(crossedMilestone(79.9, 80.1)).toBe(80)
})
it('returns null when no milestone crossed', () => {
expect(crossedMilestone(16, 18)).toBeNull()
})
it('returns highest milestone when crossing multiple', () => {
expect(crossedMilestone(14, 26)).toBe(25)
})
it('detects exact boundary crossing', () => {
expect(crossedMilestone(14.999, 15.0)).toBe(15)
})
it('returns null when staying in same bucket', () => {
expect(crossedMilestone(10.0, 14.9)).toBeNull()
})
})
describe('buildMilestoneBlock', () => {
it('returns goal-reached block at target', () => {
const block = buildMilestoneBlock('Unit test', 80)
expect(block).not.toBeNull()
expect(block!.text.text).toContain('GOAL REACHED')
})
it('returns milestone block below target', () => {
const block = buildMilestoneBlock('Unit test', 25)
expect(block).not.toBeNull()
expect(block!.text.text).toContain('MILESTONE')
expect(block!.text.text).toContain('55 percentage points to go')
})
it('uses singular for 1 percentage point', () => {
const block = buildMilestoneBlock('Unit test', 79)
expect(block!.text.text).toContain('1 percentage point to go')
})
})
describe('parseArgs', () => {
it('parses all arguments', () => {
const result = parseArgs([
'--pr-url=https://github.com/foo/bar/pull/1',
'--pr-number=42',
'--author=alice'
])
expect(result).toEqual({
prUrl: 'https://github.com/foo/bar/pull/1',
prNumber: '42',
author: 'alice'
})
})
it('returns empty strings for missing args', () => {
expect(parseArgs([])).toEqual({
prUrl: '',
prNumber: '',
author: ''
})
})
})
describe('formatCoverageRow', () => {
it('formats a coverage row with delta', () => {
const current = { percentage: 50, totalLines: 200, coveredLines: 100 }
const baseline = { percentage: 45, totalLines: 200, coveredLines: 90 }
const row = formatCoverageRow('Unit', current, baseline)
expect(row).toBe('*Unit:* 45.0% → 50.0% (+5.0%)')
})
it('formats negative delta', () => {
const current = { percentage: 40, totalLines: 200, coveredLines: 80 }
const baseline = { percentage: 45, totalLines: 200, coveredLines: 90 }
const row = formatCoverageRow('E2E', current, baseline)
expect(row).toContain('-5.0%')
})
})

View File

@@ -4,7 +4,7 @@ const TARGET = 80
const MILESTONE_STEP = 5
const BAR_WIDTH = 20
export interface CoverageData {
interface CoverageData {
percentage: number
totalLines: number
coveredLines: number
@@ -18,7 +18,7 @@ interface SlackBlock {
}
}
export function parseLcovContent(content: string): CoverageData | null {
function parseLcovContent(content: string): CoverageData | null {
let totalLines = 0
let coveredLines = 0
@@ -44,22 +44,22 @@ function parseLcov(filePath: string): CoverageData | null {
return parseLcovContent(readFileSync(filePath, 'utf-8'))
}
export function progressBar(percentage: number): string {
function progressBar(percentage: number): string {
const filled = Math.round((percentage / 100) * BAR_WIDTH)
const empty = BAR_WIDTH - filled
return '█'.repeat(filled) + '░'.repeat(empty)
}
export function formatPct(value: number): string {
function formatPct(value: number): string {
return value.toFixed(1) + '%'
}
export function formatDelta(delta: number): string {
function formatDelta(delta: number): string {
const sign = delta >= 0 ? '+' : ''
return sign + delta.toFixed(1) + '%'
}
export function crossedMilestone(prev: number, curr: number): number | null {
function crossedMilestone(prev: number, curr: number): number | null {
const prevBucket = Math.floor(prev / MILESTONE_STEP)
const currBucket = Math.floor(curr / MILESTONE_STEP)
@@ -69,7 +69,7 @@ export function crossedMilestone(prev: number, curr: number): number | null {
return null
}
export function buildMilestoneBlock(
function buildMilestoneBlock(
label: string,
milestone: number
): SlackBlock | null {
@@ -101,7 +101,7 @@ export function buildMilestoneBlock(
}
}
export function parseArgs(argv: string[]): {
function parseArgs(argv: string[]): {
prUrl: string
prNumber: string
author: string
@@ -120,7 +120,7 @@ export function parseArgs(argv: string[]): {
return { prUrl, prNumber, author }
}
export function formatCoverageRow(
function formatCoverageRow(
label: string,
current: CoverageData,
baseline: CoverageData
@@ -214,6 +214,4 @@ function main() {
process.stdout.write(JSON.stringify(payload))
}
if (process.env.VITEST !== 'true') {
main()
}
main()

View File

@@ -18,7 +18,6 @@ const coverageStatus = getArg('coverage-status') ?? 'skip'
/** @type {string[]} */
const lines = []
// --- Size section ---
if (sizeStatus === 'ready') {
try {
const sizeReport = execFileSync('node', ['scripts/size-report.js'], {
@@ -44,7 +43,6 @@ if (sizeStatus === 'ready') {
lines.push('')
// --- Perf section ---
if (perfStatus === 'ready' && existsSync('test-results/perf-metrics.json')) {
try {
const perfReport = execFileSync(
@@ -73,7 +71,6 @@ if (perfStatus === 'ready' && existsSync('test-results/perf-metrics.json')) {
lines.push('> ⏳ Performance tests in progress…')
}
// --- Coverage section ---
if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
try {
const coverageReport = execFileSync(
@@ -97,6 +94,5 @@ if (coverageStatus === 'ready' && existsSync('temp/coverage/coverage.lcov')) {
lines.push('')
lines.push('> ⚠️ Coverage collection failed. Check the CI workflow logs.')
}
// coverageStatus === 'skip' (default) — don't show section at all
process.stdout.write(lines.join('\n') + '\n')