From d1076bfc1b70140dd0e9b08cbd7152a594f65b55 Mon Sep 17 00:00:00 2001 From: coo1estguy <70365929+coolestcodeguy@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:56:36 +0530 Subject: [PATCH] ci: add GitHub Actions workflow and dynamic coverage badge generation - Add .github/workflows/ci.yml with typecheck, lint, build, test, and badge publishing - Add scripts/coverage-badge.mjs to generate shields.io endpoint JSON from coverage summary - Update vitest.config.ts with json-summary reporter and include package sources - Update README.md coverage badge to read from badges branch endpoint - Ignore badge directory and ignore .next in eslint.config.js --- .github/workflows/ci.yml | 90 ++++++++++++++++++++++++++++++++++++++ .gitignore | 3 +- README.md | 2 +- eslint.config.js | 1 + scripts/coverage-badge.mjs | 61 ++++++++++++++++++++++++++ vitest.config.ts | 4 +- 6 files changed, 157 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 scripts/coverage-badge.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a6da92a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +# Only the badge job writes; everything else is read-only. +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test & coverage + runs-on: ubuntu-latest + # The badge step pushes to the `badges` branch on main. + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - run: npm ci + + - name: Typecheck + run: npm run typecheck + + - name: Typecheck workspace packages + run: npm run typecheck --workspaces --if-present + + - name: Lint + # Not `npm run lint` — that script carries --fix, which would paper over + # violations instead of reporting them. + run: npx eslint . + + - name: Build + run: npm run build + + # ubuntu-latest ships Docker, so the testcontainers suites run here even + # though they cannot locally. This is the only place the real coverage + # number exists. + - name: Test + run: npm test + + - name: Generate coverage badge + # Runs even when the suite fails its thresholds, so the badge reports + # what coverage actually is rather than going stale on a red build. + if: always() + id: badge + run: node scripts/coverage-badge.mjs --out badge/coverage.json + + - name: Publish badge + # steps.badge.outcome guards the case where the suite crashed outright + # and never wrote a summary — no badge is better than a misleading one. + if: always() && steps.badge.outcome == 'success' && github.event_name == 'push' && github.ref == 'refs/heads/main' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PCT: ${{ steps.badge.outputs.pct }} + run: | + set -euo pipefail + tmp="$(mktemp -d)" + cp badge/coverage.json "$tmp/coverage.json" + cd "$tmp" + git init -q -b badges + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add coverage.json + git commit -qm "chore(badge): coverage ${PCT}%" + # A single-commit orphan branch — the badge has no history worth + # keeping, and force-pushing stops it growing without bound. + git push -q --force \ + "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" badges + + - name: Upload coverage report + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage + path: coverage/ + retention-days: 14 diff --git a/.gitignore b/.gitignore index 2f5a9e8..fae9e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,9 @@ dist .env.* !.env.example coverage +badge/ .DS_Store *.tsbuildinfo -.env +.env .codegraph/ .agents/ diff --git a/README.md b/README.md index 8b45905..bfb552a 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Self-hosted notification infrastructure for product notifications. One API call handles email, SMS, push, and webhooks — with preferences, quiet hours, retries, fallback, scheduling, workflows, and delivery logs built in. -[![npm version](https://img.shields.io/npm/v/notifkit.svg?style=flat-square&color=6366f1)](https://www.npmjs.com/package/notifkit) [![npm downloads](https://img.shields.io/npm/dm/notifkit.svg?style=flat-square&color=6366f1)](https://www.npmjs.com/package/notifkit) [![Coverage](https://img.shields.io/badge/coverage-85%25-brightgreen.svg?style=flat-square)](https://github.com/devkitshq/notifkit) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178c6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Node.js](https://img.shields.io/badge/node-%3E%3D22.0.0-339933.svg?style=flat-square)](https://nodejs.org) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](./LICENSE) +[![npm version](https://img.shields.io/npm/v/notifkit.svg?style=flat-square&color=6366f1)](https://www.npmjs.com/package/notifkit) [![npm downloads](https://img.shields.io/npm/dm/notifkit.svg?style=flat-square&color=6366f1)](https://www.npmjs.com/package/notifkit) [![Coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/devkitshq/notifkit/badges/coverage.json&style=flat-square)](https://github.com/devkitshq/notifkit/actions/workflows/ci.yml) [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-3178c6.svg?style=flat-square)](https://www.typescriptlang.org/) [![Node.js](https://img.shields.io/badge/node-%3E%3D22.0.0-339933.svg?style=flat-square)](https://nodejs.org) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](./LICENSE) [Documentation](https://notifkit.dev/docs/) · [Quickstart](https://notifkit.dev/docs/quickstart.html) · [Examples](https://notifkit.dev/docs/examples.html) · [notifkit.dev](https://notifkit.dev) diff --git a/eslint.config.js b/eslint.config.js index 5f14e94..63cd87c 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,6 +7,7 @@ export default [ ignores: [ "**/dist/**", "**/node_modules/**", + "**/.next/**", "**/*.js", "**/*.cjs", "**/*.mjs", diff --git a/scripts/coverage-badge.mjs b/scripts/coverage-badge.mjs new file mode 100644 index 0000000..7176120 --- /dev/null +++ b/scripts/coverage-badge.mjs @@ -0,0 +1,61 @@ +#!/usr/bin/env node +/** + * Turns the coverage summary vitest writes into a shields.io endpoint payload. + * + * CI publishes the result to the orphan `badges` branch, which the README's + * badge reads over raw.githubusercontent. Keeping the number in the repo means + * no third-party coverage service and no secret beyond the built-in token. + * + * Usage: node scripts/coverage-badge.mjs [--out ] [--metric lines] + */ +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; + +const SUMMARY = "coverage/coverage-summary.json"; + +/** Shields renders whatever colour we hand it, so the thresholds live here. */ +const colorFor = (pct) => + pct >= 90 ? "brightgreen" : pct >= 80 ? "green" : pct >= 70 ? "yellowgreen" : pct >= 60 ? "yellow" : pct >= 50 ? "orange" : "red"; + +const arg = (flag, fallback) => { + const i = process.argv.indexOf(flag); + return i !== -1 && process.argv[i + 1] ? process.argv[i + 1] : fallback; +}; + +const out = resolve(arg("--out", "coverage-badge.json")); +const metric = arg("--metric", "lines"); + +let summary; +try { + summary = JSON.parse(await readFile(SUMMARY, "utf8")); +} catch (err) { + // Better to fail loudly than publish a stale or invented number. + console.error( + `coverage-badge: could not read ${SUMMARY} (${err.code ?? err.message}).\n` + + `Run the suite with coverage first, and make sure "json-summary" is in\n` + + `coverage.reporter in vitest.config.ts.`, + ); + process.exit(1); +} + +const total = summary.total?.[metric]?.pct; +if (typeof total !== "number" || Number.isNaN(total)) { + console.error(`coverage-badge: no numeric total.${metric}.pct in ${SUMMARY}`); + process.exit(1); +} + +// One decimal keeps 74.59 from rounding up to a friendlier-looking 75. +const pct = Math.round(total * 10) / 10; + +await mkdir(dirname(out), { recursive: true }); +await writeFile( + out, + JSON.stringify({ schemaVersion: 1, label: "coverage", message: `${pct}%`, color: colorFor(pct) }, null, 2) + "\n", +); + +// The workflow reads this line to label the badge commit. +console.log(`coverage-badge: ${pct}% (${metric}) -> ${out}`); +if (process.env.GITHUB_OUTPUT) { + const { appendFile } = await import("node:fs/promises"); + await appendFile(process.env.GITHUB_OUTPUT, `pct=${pct}\n`); +} diff --git a/vitest.config.ts b/vitest.config.ts index 4f0aa9b..d19e86a 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,8 +13,8 @@ export default defineConfig({ hookTimeout: 1200_000, coverage: { provider: "v8", - reporter: ["text", "json", "html"], - include: ["src/**"], + reporter: ["text", "json", "json-summary", "html"], + include: ["src/**", "packages/*/src/**"], exclude: ["test-project/**", "tests/**", "vitest.config.ts"], thresholds: { lines: 80,