1
0
forked from baron/baron-sso
Files
baron-sso/scripts/update_code_check_badges.mjs

460 lines
12 KiB
JavaScript

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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
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 `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="20" role="img" aria-label="${escapeXml(label)}: ${escapeXml(message)}">
<title>${escapeXml(label)}: ${escapeXml(message)}</title>
<linearGradient id="s" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="1" stop-opacity=".1"/>
</linearGradient>
<clipPath id="r"><rect width="${width}" height="20" rx="3" fill="#fff"/></clipPath>
<g clip-path="url(#r)">
<rect width="${labelWidth}" height="20" fill="#555"/>
<rect x="${labelWidth}" width="${messageWidth}" height="20" fill="${color}"/>
<rect width="${width}" height="20" fill="url(#s)"/>
</g>
<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="11">
<text x="${labelCenter}" y="15" fill="#010101" fill-opacity=".3">${escapeXml(label)}</text>
<text x="${labelCenter}" y="14">${escapeXml(label)}</text>
<text x="${messageCenter}" y="15" fill="#010101" fill-opacity=".3">${escapeXml(message)}</text>
<text x="${messageCenter}" y="14">${escapeXml(message)}</text>
</g>
</svg>
`;
}
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() &&
[
"backend-coverage-summary.json",
"package-coverage-summary.json",
"vitest-coverage-summary.json",
].includes(entry.name)
) {
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 = {
backend: process.env.BACKEND_COVERAGE_RESULT,
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);
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);
}
if (coverageSummary?.package) {
coverageByPackage.set(coverageSummary.package, coverageSummary.statements);
}
}
if (coverageJobResults.backend) {
updatePackageBadge(
manifest,
"backend-tests",
jobResults.backend,
coverageJobResults.backend,
coverageByPackage.get("backend"),
);
} else {
updateCompactResultBadge(manifest, "backend-tests", jobResults.backend);
}
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.`);