mirror of
https://github.com/devkitshq/notifkit.git
synced 2026-10-19 18:24:52 +02:00
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
This commit is contained in:
@@ -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
|
||||
+2
-1
@@ -6,8 +6,9 @@ dist
|
||||
.env.*
|
||||
!.env.example
|
||||
coverage
|
||||
badge/
|
||||
.DS_Store
|
||||
*.tsbuildinfo
|
||||
.env
|
||||
.env
|
||||
.codegraph/
|
||||
.agents/
|
||||
|
||||
@@ -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.
|
||||
|
||||
[](https://www.npmjs.com/package/notifkit) [](https://www.npmjs.com/package/notifkit) [](https://github.com/devkitshq/notifkit) [](https://www.typescriptlang.org/) [](https://nodejs.org) [](./LICENSE)
|
||||
[](https://www.npmjs.com/package/notifkit) [](https://www.npmjs.com/package/notifkit) [](https://github.com/devkitshq/notifkit/actions/workflows/ci.yml) [](https://www.typescriptlang.org/) [](https://nodejs.org) [](./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)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export default [
|
||||
ignores: [
|
||||
"**/dist/**",
|
||||
"**/node_modules/**",
|
||||
"**/.next/**",
|
||||
"**/*.js",
|
||||
"**/*.cjs",
|
||||
"**/*.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 <path>] [--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`);
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user