import { mkdir, readdir, readFile, unlink, writeFile } from "node:fs/promises"; import path from "node:path"; const repoRoot = process.cwd(); const badgeDir = path.join(repoRoot, "docs", "badges"); const manifestPath = path.join(badgeDir, "badges.json"); const resultStyles = { success: { message: "passing", color: "#2ea043" }, failure: { message: "failing", color: "#cf222e" }, cancelled: { message: "cancelled", color: "#bf8700" }, skipped: { message: "skipped", color: "#6e7781" }, unknown: { message: "unknown", color: "#6e7781" }, }; const badgeDefinitions = { "dev-sha": { label: "dev", message: "unknown", color: "#0969da" }, "code-check": { label: "code check", message: "unknown", color: "#6e7781" }, biome: { label: "biome", message: "unknown", color: "#6e7781" }, "backend-tests": { label: "backend", message: "unknown", color: "#6e7781", }, userfront: { label: "userfront", message: "unknown", color: "#6e7781", }, adminfront: { label: "adminfront", message: "unknown", color: "#6e7781", }, devfront: { label: "devfront", message: "unknown", color: "#6e7781", }, orgfront: { label: "orgfront", message: "unknown", color: "#6e7781", }, "userfront-chrome": { label: "chrome", message: "unknown", color: "#6e7781", }, "userfront-firefox": { label: "firefox", message: "unknown", color: "#6e7781", }, "userfront-safari": { label: "safari", message: "unknown", color: "#6e7781", }, }; const deprecatedBadgeKeys = [ "userfront-e2e-fast", "userfront-e2e-full", "adminfront-e2e", "devfront-e2e", "orgfront-e2e", "userfront-coverage", "adminfront-coverage", "devfront-coverage", "orgfront-coverage", ]; function normalizeResult(result) { return resultStyles[result] ? result : "unknown"; } function styleForResult(result) { return resultStyles[normalizeResult(result)]; } function compactResult(result) { const normalized = normalizeResult(result); return { success: "pass", failure: "fail", cancelled: "cancel", skipped: "skip", unknown: "unknown", }[normalized]; } function colorForParts(parts) { const normalized = parts.map(normalizeResult); if (normalized.includes("failure")) return resultStyles.failure.color; if (normalized.includes("cancelled")) return resultStyles.cancelled.color; if (normalized.every((part) => part === "success")) return resultStyles.success.color; return resultStyles.unknown.color; } function colorForCoverage(percent) { if (percent >= 80) return "#2ea043"; if (percent >= 35) return "#bf8700"; return "#cf222e"; } function escapeXml(value) { return String(value) .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); } function textWidth(text) { return Math.max(38, Math.ceil(String(text).length * 6.8 + 10)); } function renderBadge({ label, message, color }) { const labelWidth = textWidth(label); const messageWidth = textWidth(message); const width = labelWidth + messageWidth; const labelCenter = labelWidth / 2; const messageCenter = labelWidth + messageWidth / 2; return ` ${escapeXml(label)}: ${escapeXml(message)} ${escapeXml(label)} ${escapeXml(label)} ${escapeXml(message)} ${escapeXml(message)} `; } async function readJsonIfExists(filePath) { try { return JSON.parse(await readFile(filePath, "utf8")); } catch { return null; } } async function findCoverageSummaries(directory) { const entries = await readdir(directory, { withFileTypes: true }).catch( () => [], ); const results = []; for (const entry of entries) { const entryPath = path.join(directory, entry.name); if (entry.isFile() && entry.name === "vitest-coverage-summary.json") { results.push(entryPath); continue; } if (entry.isDirectory()) { results.push(...(await findCoverageSummaries(entryPath))); } } return results; } function updateResultBadge(manifest, key, result) { const style = styleForResult(result); manifest.badges[key] = { ...(manifest.badges[key] ?? badgeDefinitions[key]), message: style.message, color: style.color, }; } function updateCompactResultBadge(manifest, key, result) { const style = styleForResult(result); manifest.badges[key] = { ...(manifest.badges[key] ?? badgeDefinitions[key]), label: badgeDefinitions[key]?.label ?? manifest.badges[key]?.label, message: compactResult(result), color: style.color, }; } function coveragePart(result, statements) { const normalized = normalizeResult(result); if (normalized !== "success") { return { message: compactResult(normalized), color: styleForResult(normalized).color, result: normalized, }; } const value = Number(statements); if (!Number.isFinite(value)) { return { message: "unknown", color: resultStyles.unknown.color, result: "unknown", }; } return { message: `${value.toFixed(2)}%`, color: colorForCoverage(value), result: "success", }; } function updatePackageBadge(manifest, key, testResult, coverageResult, statements) { if (!badgeDefinitions[key]) return; const test = normalizeResult(testResult); const coverage = coveragePart(coverageResult, statements); manifest.badges[key] = { ...(manifest.badges[key] ?? badgeDefinitions[key]), label: badgeDefinitions[key].label, message: `${compactResult(test)} | ${coverage.message}`, color: test === "failure" || coverage.result === "failure" ? resultStyles.failure.color : test === "cancelled" || coverage.result === "cancelled" ? resultStyles.cancelled.color : coverage.result === "success" ? coverage.color : colorForParts([test, coverage.result]), }; } function updateBrowserBadge(manifest, key, desktopResult, mobileResult) { if (!badgeDefinitions[key]) return; const desktop = normalizeResult(desktopResult); const mobile = normalizeResult(mobileResult); manifest.badges[key] = { ...(manifest.badges[key] ?? badgeDefinitions[key]), label: badgeDefinitions[key].label, message: `${compactResult(desktop)} | ${compactResult(mobile)}`, color: colorForParts([desktop, mobile]), }; } function normalizeManifestForComparison(value) { return { ...value, updatedAt: null, source: { ...(value?.source ?? {}), runId: null, runNumber: null, }, }; } function manifestsMatchIgnoringRunMetadata(left, right) { return ( JSON.stringify(normalizeManifestForComparison(left)) === JSON.stringify(normalizeManifestForComparison(right)) ); } function shortSha(value) { return String(value ?? "") .trim() .slice(0, 12); } const existingManifest = process.env.RESET_BADGES === "true" ? null : await readJsonIfExists(manifestPath); const sourceSha = shortSha( process.env.BADGE_SOURCE_SHA || process.env.GITHUB_SHA, ); const manifest = { schemaVersion: 1, generatedBy: "scripts/update_code_check_badges.mjs", updatedAt: new Date().toISOString(), source: { branch: process.env.BADGE_SOURCE_BRANCH || "dev", sha: process.env.BADGE_SOURCE_SHA || process.env.GITHUB_SHA || null, shortSha: sourceSha || null, runId: process.env.GITHUB_RUN_ID || null, runNumber: process.env.GITHUB_RUN_NUMBER || null, }, badges: { ...badgeDefinitions, ...(existingManifest?.badges ?? {}), }, }; for (const key of deprecatedBadgeKeys) { delete manifest.badges[key]; } manifest.badges["dev-sha"] = { ...badgeDefinitions["dev-sha"], message: sourceSha || "unknown", }; const jobResults = { lint: process.env.LINT_RESULT, biome: process.env.BIOME_RESULT, backend: process.env.BACKEND_RESULT, userfront: process.env.USERFRONT_RESULT, userfrontE2e: process.env.USERFRONT_E2E_RESULT, adminfront: process.env.ADMINFRONT_RESULT, devfront: process.env.DEVFRONT_RESULT, orgfront: process.env.ORGFRONT_RESULT, }; const e2eWasFull = process.env.USERFRONT_E2E_FULL === "true"; const userfrontFastResult = e2eWasFull ? undefined : jobResults.userfrontE2e; const browserResults = { chrome: { desktop: process.env.USERFRONT_E2E_CHROMIUM_DESKTOP_RESULT, mobile: process.env.USERFRONT_E2E_CHROMIUM_MOBILE_RESULT, }, firefox: { desktop: process.env.USERFRONT_E2E_FIREFOX_DESKTOP_RESULT, mobile: process.env.USERFRONT_E2E_FIREFOX_MOBILE_RESULT, }, safari: { desktop: process.env.USERFRONT_E2E_WEBKIT_DESKTOP_RESULT, mobile: process.env.USERFRONT_E2E_WEBKIT_MOBILE_RESULT, }, }; const legacyCoverageResult = process.env.COVERAGE_RESULT; const coverageJobResults = { userfront: process.env.USERFRONT_COVERAGE_RESULT, adminfront: process.env.ADMINFRONT_COVERAGE_RESULT || legacyCoverageResult, devfront: process.env.DEVFRONT_COVERAGE_RESULT || legacyCoverageResult, orgfront: process.env.ORGFRONT_COVERAGE_RESULT || legacyCoverageResult, }; const overallResults = [ ...Object.values(jobResults), ...Object.values(coverageJobResults), ...Object.values(browserResults).flatMap((result) => [ result.desktop, result.mobile, ]), ].filter(Boolean); const hasFailure = overallResults.some((result) => ["failure", "cancelled"].includes(result), ); const allSkipped = overallResults.length > 0 && overallResults.every((result) => result === "skipped"); if (process.env.BADGE_UPDATE_CODE_CHECK !== "false") { updateResultBadge( manifest, "code-check", overallResults.length === 0 ? "unknown" : hasFailure ? "failure" : allSkipped ? "skipped" : "success", ); } updateResultBadge(manifest, "biome", jobResults.biome); updateCompactResultBadge(manifest, "backend-tests", jobResults.backend); const coverageSummaries = process.env.COVERAGE_SUMMARY_PATH ? [process.env.COVERAGE_SUMMARY_PATH] : await findCoverageSummaries(path.join(repoRoot, "badge-artifacts")); const coverageByPackage = new Map(); for (const summaryPath of coverageSummaries) { const coverageSummary = await readJsonIfExists(summaryPath); for (const row of coverageSummary?.packages ?? []) { coverageByPackage.set(row.package, row.statements); } } for (const [key, testResult, coverageResult] of [ ["userfront", userfrontFastResult, coverageJobResults.userfront], ["adminfront", jobResults.adminfront, coverageJobResults.adminfront], ["devfront", jobResults.devfront, coverageJobResults.devfront], ["orgfront", jobResults.orgfront, coverageJobResults.orgfront], ]) { if (testResult || coverageResult) { updatePackageBadge( manifest, key, testResult, coverageResult, coverageByPackage.get(key), ); } } for (const [browser, result] of Object.entries(browserResults)) { if (result.desktop || result.mobile) { updateBrowserBadge( manifest, `userfront-${browser}`, result.desktop, result.mobile, ); } } if ( existingManifest && manifestsMatchIgnoringRunMetadata(manifest, existingManifest) ) { manifest.updatedAt = existingManifest.updatedAt; manifest.source = existingManifest.source ?? manifest.source; } await mkdir(badgeDir, { recursive: true }); await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`); for (const [key, badge] of Object.entries(manifest.badges)) { await writeFile(path.join(badgeDir, `${key}.svg`), renderBadge(badge)); } for (const key of deprecatedBadgeKeys) { await unlink(path.join(badgeDir, `${key}.svg`)).catch(() => {}); } console.log(`Updated ${Object.keys(manifest.badges).length} badge files.`);