From 03c998f22f829d22b02ab091262f3c715c2448b5 Mon Sep 17 00:00:00 2001 From: Kaushik Gnanaskandan Date: Mon, 23 Mar 2026 18:19:31 -0700 Subject: [PATCH 1/3] feat: dynamic coverage badge backed by GitHub Gist Replace the static 80%+ coverage badge with a real aggregate computed from per-package Vitest json-summary reports. CI pushes the result to a Gist on every main-branch push via schneegans/dynamic-badges-action. --- .github/workflows/ci.yml | 18 +++++ README.md | 2 +- package.json | 7 ++ packages/domscribe-core/vite.config.ts | 1 + packages/domscribe-manifest/vite.config.ts | 1 + packages/domscribe-next/vite.config.ts | 1 + packages/domscribe-nuxt/vite.config.ts | 1 + packages/domscribe-overlay/vite.config.ts | 1 + packages/domscribe-react/vite.config.ts | 1 + packages/domscribe-relay/vite.config.ts | 1 + packages/domscribe-runtime/vite.config.ts | 1 + packages/domscribe-transform/vite.config.ts | 1 + packages/domscribe-vue/vite.config.ts | 1 + scripts/aggregate-coverage.mjs | 89 +++++++++++++++++++++ 14 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 scripts/aggregate-coverage.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d54642..9ac5450 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,24 @@ jobs: - run: npx nx run-many -t lint test build typecheck --exclude domscribe-test-fixtures + - name: Aggregate coverage + id: coverage + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + run: | + npx nx run domscribe:coverage-badge + echo "json=$(cat coverage/shields.json)" >> "$GITHUB_OUTPUT" + + - name: Update coverage badge + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + uses: schneegans/dynamic-badges-action@v1.7.0 + with: + auth: ${{ secrets.GIST_SECRET }} + gistID: ${{ vars.COVERAGE_GIST_ID }} + filename: domscribe-coverage.json + label: ${{ fromJSON(steps.coverage.outputs.json).label }} + message: ${{ fromJSON(steps.coverage.outputs.json).message }} + color: ${{ fromJSON(steps.coverage.outputs.json).color }} + build-registry: runs-on: ubuntu-latest needs: checks diff --git a/README.md b/README.md index 90980a6..caf2b92 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@

npm version CI status - test coverage + test coverage MIT license TypeScript Node.js >= 18 diff --git a/package.json b/package.json index 2904496..5a0436d 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,13 @@ "command": "node scripts/sync-versions.mjs" } }, + "coverage-badge": { + "executor": "nx:run-commands", + "dependsOn": ["^test"], + "options": { + "command": "node scripts/aggregate-coverage.mjs" + } + }, "local-registry": { "executor": "@nx/js:verdaccio", "options": { diff --git a/packages/domscribe-core/vite.config.ts b/packages/domscribe-core/vite.config.ts index 188efa4..e3ab15f 100644 --- a/packages/domscribe-core/vite.config.ts +++ b/packages/domscribe-core/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-manifest/vite.config.ts b/packages/domscribe-manifest/vite.config.ts index 7cda7a0..32f30b9 100644 --- a/packages/domscribe-manifest/vite.config.ts +++ b/packages/domscribe-manifest/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-next/vite.config.ts b/packages/domscribe-next/vite.config.ts index 574faab..90b7650 100644 --- a/packages/domscribe-next/vite.config.ts +++ b/packages/domscribe-next/vite.config.ts @@ -23,6 +23,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-nuxt/vite.config.ts b/packages/domscribe-nuxt/vite.config.ts index 91deaec..d92c94b 100644 --- a/packages/domscribe-nuxt/vite.config.ts +++ b/packages/domscribe-nuxt/vite.config.ts @@ -20,6 +20,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-overlay/vite.config.ts b/packages/domscribe-overlay/vite.config.ts index a97af07..9541cf0 100644 --- a/packages/domscribe-overlay/vite.config.ts +++ b/packages/domscribe-overlay/vite.config.ts @@ -21,6 +21,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0, diff --git a/packages/domscribe-react/vite.config.ts b/packages/domscribe-react/vite.config.ts index 1a78cac..9ba1983 100644 --- a/packages/domscribe-react/vite.config.ts +++ b/packages/domscribe-react/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-relay/vite.config.ts b/packages/domscribe-relay/vite.config.ts index 67ca8f6..caf1641 100644 --- a/packages/domscribe-relay/vite.config.ts +++ b/packages/domscribe-relay/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', include: ['src/**/*.ts'], exclude: [ diff --git a/packages/domscribe-runtime/vite.config.ts b/packages/domscribe-runtime/vite.config.ts index 46ebee5..d46aa7f 100644 --- a/packages/domscribe-runtime/vite.config.ts +++ b/packages/domscribe-runtime/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-transform/vite.config.ts b/packages/domscribe-transform/vite.config.ts index 0d39fac..0c1144f 100644 --- a/packages/domscribe-transform/vite.config.ts +++ b/packages/domscribe-transform/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/packages/domscribe-vue/vite.config.ts b/packages/domscribe-vue/vite.config.ts index 804a893..84b1904 100644 --- a/packages/domscribe-vue/vite.config.ts +++ b/packages/domscribe-vue/vite.config.ts @@ -12,6 +12,7 @@ export default defineConfig({ coverage: { enabled: true, provider: 'v8', + reporter: ['text', 'json-summary'], reportsDirectory: './test-output/vitest/coverage', thresholds: { lines: 0.8, diff --git a/scripts/aggregate-coverage.mjs b/scripts/aggregate-coverage.mjs new file mode 100644 index 0000000..7deec60 --- /dev/null +++ b/scripts/aggregate-coverage.mjs @@ -0,0 +1,89 @@ +/** + * @module aggregate-coverage + * + * Reads coverage-summary.json from each package's test-output directory, + * computes a weighted-average line coverage percentage across all packages, + * and writes a shields.io-compatible JSON endpoint to coverage/shields.json. + */ + +import { + readFileSync, + writeFileSync, + mkdirSync, + existsSync, + readdirSync, +} from 'fs'; +import { resolve, dirname, join } from 'path'; + +const ROOT = resolve(dirname(new URL(import.meta.url).pathname), '..'); + +const summaryFiles = readdirSync(join(ROOT, 'packages')) + .filter((d) => d.startsWith('domscribe-')) + .map((d) => + join('packages', d, 'test-output/vitest/coverage/coverage-summary.json'), + ) + .filter((f) => existsSync(join(ROOT, f))); + +if (summaryFiles.length === 0) { + console.error( + 'No coverage-summary.json files found. Run tests with coverage first.', + ); + process.exit(1); +} + +let totalCovered = 0; +let totalStatements = 0; + +for (const file of summaryFiles) { + const abs = resolve(ROOT, file); + const summary = JSON.parse(readFileSync(abs, 'utf8')); + const totals = summary.total; + + if (!totals?.statements) { + console.warn(`Skipping ${file} — no total.statements`); + continue; + } + + totalCovered += totals.statements.covered; + totalStatements += totals.statements.total; + + const pkg = file.split('/')[1]; + const pct = totals.statements.pct; + console.log(` ${pkg}: ${pct}%`); +} + +if (totalStatements === 0) { + console.error('No statement data found across packages.'); + process.exit(1); +} + +const percentage = ((totalCovered / totalStatements) * 100).toFixed(1); + +const color = + percentage >= 90 + ? 'brightgreen' + : percentage >= 80 + ? 'green' + : percentage >= 70 + ? 'yellowgreen' + : percentage >= 60 + ? 'yellow' + : 'red'; + +const badge = { + schemaVersion: 1, + label: 'coverage', + message: `${percentage}%`, + color, +}; + +const outDir = resolve(ROOT, 'coverage'); +if (!existsSync(outDir)) { + mkdirSync(outDir, { recursive: true }); +} + +const outPath = resolve(outDir, 'shields.json'); +writeFileSync(outPath, JSON.stringify(badge, null, 2) + '\n'); + +console.log(`\nAggregate coverage: ${percentage}% (${summaryFiles.length} packages)`); +console.log(`Badge JSON written to ${outPath}`); From 892f819f3e706add809377a42655dc306a1ec161 Mon Sep 17 00:00:00 2001 From: Kaushik Gnanaskandan Date: Mon, 23 Mar 2026 18:20:58 -0700 Subject: [PATCH 2/3] fix(ci): avoid fromJSON on potentially-empty step output GitHub Actions evaluates expressions at parse time regardless of step if-conditions, so fromJSON on an empty string causes a workflow error. Use jq to extract individual outputs instead. --- .github/workflows/ci.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ac5450..8688700 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,10 @@ jobs: if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: | npx nx run domscribe:coverage-badge - echo "json=$(cat coverage/shields.json)" >> "$GITHUB_OUTPUT" + BADGE_JSON=$(cat coverage/shields.json) + echo "label=$(echo "$BADGE_JSON" | jq -r .label)" >> "$GITHUB_OUTPUT" + echo "message=$(echo "$BADGE_JSON" | jq -r .message)" >> "$GITHUB_OUTPUT" + echo "color=$(echo "$BADGE_JSON" | jq -r .color)" >> "$GITHUB_OUTPUT" - name: Update coverage badge if: github.ref == 'refs/heads/main' && github.event_name == 'push' @@ -44,9 +47,9 @@ jobs: auth: ${{ secrets.GIST_SECRET }} gistID: ${{ vars.COVERAGE_GIST_ID }} filename: domscribe-coverage.json - label: ${{ fromJSON(steps.coverage.outputs.json).label }} - message: ${{ fromJSON(steps.coverage.outputs.json).message }} - color: ${{ fromJSON(steps.coverage.outputs.json).color }} + label: ${{ steps.coverage.outputs.label }} + message: ${{ steps.coverage.outputs.message }} + color: ${{ steps.coverage.outputs.color }} build-registry: runs-on: ubuntu-latest From bf0b10cc8b010a235499ec7661e4ed80e99bade3 Mon Sep 17 00:00:00 2001 From: Kaushik Gnanaskandan Date: Mon, 23 Mar 2026 18:24:06 -0700 Subject: [PATCH 3/3] fix(ci): use gh api instead of disallowed third-party action The repo's action allowlist blocks schneegans/dynamic-badges-action. Replace with a direct gh api call to update the Gist. --- .github/workflows/ci.yml | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8688700..269fb9c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,26 +30,14 @@ jobs: - run: npx nx run-many -t lint test build typecheck --exclude domscribe-test-fixtures - - name: Aggregate coverage - id: coverage + - name: Update coverage badge if: github.ref == 'refs/heads/main' && github.event_name == 'push' + env: + GH_TOKEN: ${{ secrets.GIST_SECRET }} run: | npx nx run domscribe:coverage-badge - BADGE_JSON=$(cat coverage/shields.json) - echo "label=$(echo "$BADGE_JSON" | jq -r .label)" >> "$GITHUB_OUTPUT" - echo "message=$(echo "$BADGE_JSON" | jq -r .message)" >> "$GITHUB_OUTPUT" - echo "color=$(echo "$BADGE_JSON" | jq -r .color)" >> "$GITHUB_OUTPUT" - - - name: Update coverage badge - if: github.ref == 'refs/heads/main' && github.event_name == 'push' - uses: schneegans/dynamic-badges-action@v1.7.0 - with: - auth: ${{ secrets.GIST_SECRET }} - gistID: ${{ vars.COVERAGE_GIST_ID }} - filename: domscribe-coverage.json - label: ${{ steps.coverage.outputs.label }} - message: ${{ steps.coverage.outputs.message }} - color: ${{ steps.coverage.outputs.color }} + gh api --method PATCH "gists/${{ vars.COVERAGE_GIST_ID }}" \ + -f "files[domscribe-coverage.json][content]=$(cat coverage/shields.json)" build-registry: runs-on: ubuntu-latest