diff --git a/scripts/sync-labels.ts b/scripts/sync-labels.ts index f3412f25945..88c1973641b 100644 --- a/scripts/sync-labels.ts +++ b/scripts/sync-labels.ts @@ -9,13 +9,17 @@ type RepoLabel = { }; const COLOR_BY_PREFIX = new Map([ - ["channel", "1d76db"], - ["app", "6f42c1"], - ["extensions", "0e8a16"], - ["docs", "0075ca"], - ["cli", "f9d0c4"], - ["gateway", "d4c5f9"], - ["size", "fbca04"], + ["channel", "DDEBFA"], + ["app", "EADFF8"], + ["extensions", "EDEDED"], + ["plugin", "EDEDED"], + ["docs", "CFE3F8"], + ["cli", "CFE3F8"], + ["gateway", "D9CCF5"], + ["commands", "CFE3F8"], + ["scripts", "D9CCF5"], + ["docker", "DDF4E4"], + ["size", "E8C4CB"], ]); const EXTRA_LABEL_METADATA = new Map< diff --git a/scripts/sync-openclaw-label-colors.mjs b/scripts/sync-openclaw-label-colors.mjs new file mode 100644 index 00000000000..d68a1c0573c --- /dev/null +++ b/scripts/sync-openclaw-label-colors.mjs @@ -0,0 +1,288 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; + +const REPO = "openclaw/openclaw"; +const APPLY = process.argv.includes("--apply"); + +const COLORS = { + saturatedRed: "B60205", + saturatedBugRed: "D73A4A", + saturatedOrangeRed: "D93F0B", + saturatedAmber: "FBCA04", + softerAmber: "F9D65C", + paleYellow: "F7E7A1", + saturatedGreen: "0E8A16", + mutedGreen: "B8E0B0", + paleGreen: "DDF4E4", + proofGreen: "C2E0C6", + mutedProofGreen: "9BD3A0", + overrideGreen: "DDECCF", + saturatedBlue: "0F2CCE", + paleBlue: "CFE3F8", + channelBlue: "DDEBFA", + dedupeBlue: "BFD4F2", + triageBlue: "D8E8F8", + saturatedPurple: "7057FF", + mutedPurple: "D9CCF5", + appPurple: "EADFF8", + neutralGray: "EDEDED", + duplicateGray: "CFD3D7", + darkGray: "8C8C8C", + mutedRose: "E8C4CB", + mutedRed: "E99695", + black: "000000", + white: "FFFFFF", + clawsweeperOrange: "F97316", + helpGreen: "008672", + maintainerYellow: "FFD700", +}; + +const EXACT_COLORS = new Map( + Object.entries({ + P0: COLORS.saturatedRed, + P1: COLORS.saturatedOrangeRed, + P2: COLORS.saturatedAmber, + P3: COLORS.mutedGreen, + "impact:data-loss": COLORS.saturatedRed, + "impact:security": COLORS.saturatedRed, + "impact:crash-loop": COLORS.saturatedOrangeRed, + "impact:message-loss": COLORS.saturatedOrangeRed, + "impact:auth-provider": COLORS.softerAmber, + "impact:session-state": COLORS.softerAmber, + bug: COLORS.saturatedBugRed, + "bug:crash": COLORS.saturatedRed, + "bug:behavior": COLORS.saturatedBugRed, + regression: COLORS.saturatedOrangeRed, + security: COLORS.saturatedRed, + "beta-blocker": COLORS.saturatedOrangeRed, + "proof: supplied": COLORS.proofGreen, + "proof: sufficient": COLORS.mutedProofGreen, + "proof: override": COLORS.overrideGreen, + "triage:blocked": COLORS.saturatedAmber, + "triage:bug": COLORS.saturatedBugRed, + "triage:done": COLORS.mutedGreen, + "triage:needs-review": COLORS.paleBlue, + "triage:started": COLORS.mutedPurple, + agents: COLORS.mutedPurple, + docs: COLORS.paleBlue, + cli: COLORS.paleBlue, + commands: COLORS.paleBlue, + scripts: COLORS.mutedPurple, + gateway: COLORS.mutedPurple, + codex: COLORS.neutralGray, + docker: COLORS.paleGreen, + tui: COLORS.paleGreen, + "extensions: NEW": COLORS.channelBlue, + clawsweeper: COLORS.clawsweeperOrange, + "clawsweeper:human-review": COLORS.saturatedRed, + "clawsweeper:needs-security-review": COLORS.saturatedRed, + "clawsweeper:needs-live-repro": COLORS.saturatedAmber, + "clawsweeper:needs-maintainer-review": COLORS.saturatedAmber, + "clawsweeper:needs-product-decision": COLORS.saturatedAmber, + "clawsweeper:automerge": COLORS.saturatedGreen, + "clawsweeper:queueable-fix": COLORS.saturatedGreen, + "clawsweeper:fix-shape-clear": COLORS.mutedProofGreen, + "clawsweeper:autofix": COLORS.paleBlue, + "clawsweeper:commit-finding": COLORS.paleBlue, + "clawsweeper:autogenerated": COLORS.mutedPurple, + "clawsweeper:linked-pr-open": COLORS.mutedPurple, + "clawsweeper:merge-ready": COLORS.mutedPurple, + "clawsweeper:current-main-repro": COLORS.paleBlue, + "clawsweeper:source-repro": COLORS.paleBlue, + "clawsweeper:not-repro-on-main": COLORS.proofGreen, + "clawsweeper:no-new-fix-pr": COLORS.neutralGray, + "clawsweeper:needs-info": COLORS.appPurple, + "close:spam": COLORS.black, + "close:duplicate": COLORS.mutedRed, + "close:already-fixed": COLORS.paleBlue, + "close:cannot-repro": COLORS.paleYellow, + "close:invalid": COLORS.neutralGray, + "close:not-planned": COLORS.darkGray, + "close:superseded": COLORS.mutedPurple, + duplicate: COLORS.duplicateGray, + invalid: COLORS.paleYellow, + wontfix: COLORS.white, + "r: spam": COLORS.saturatedRed, + "r: moltbook": COLORS.saturatedRed, + "r: too-many-prs": COLORS.saturatedOrangeRed, + "r: no-ci-pr": COLORS.saturatedOrangeRed, + "r: bluebubbles": COLORS.saturatedOrangeRed, + "r: testflight": COLORS.saturatedOrangeRed, + "r: false-positive": COLORS.saturatedOrangeRed, + "r: skill": COLORS.mutedPurple, + "r: third-party-extension": COLORS.mutedPurple, + "r: support": COLORS.mutedGreen, + "r: too-many-prs-override": COLORS.overrideGreen, + maintainer: COLORS.maintainerYellow, + "trusted-contributor": COLORS.neutralGray, + "help wanted": COLORS.helpGreen, + "good first issue": COLORS.saturatedPurple, + "In Progress": COLORS.saturatedBlue, + "no-stale": COLORS.mutedGreen, + stale: COLORS.paleYellow, + "trigger-response": COLORS.saturatedAmber, + "bad-barnacle": COLORS.mutedRed, + clownfish: COLORS.neutralGray, + "mantis: telegram-visible-proof": COLORS.mutedPurple, + "dedupe:parent": COLORS.dedupeBlue, + "dedupe:child": COLORS.paleBlue, + dependencies: COLORS.neutralGray, + "dependencies-changed": COLORS.paleYellow, + github_actions: COLORS.neutralGray, + go: COLORS.neutralGray, + java: COLORS.neutralGray, + swift_package_manager: COLORS.neutralGray, + "cannot-reproduce": COLORS.dedupeBlue, + question: COLORS.appPurple, + enhancement: COLORS.channelBlue, + "pi-issue": COLORS.saturatedOrangeRed, + aardvark: COLORS.neutralGray, + "cohort-4": COLORS.mutedPurple, + dirty: COLORS.saturatedRed, + }), +); + +const FAMILY_RULES = [ + { + family: "channel", + match: (name) => name.startsWith("channel: "), + color: COLORS.channelBlue, + reason: "routine channel taxonomy stays visually quiet", + }, + { + family: "app", + match: (name) => name.startsWith("app: "), + color: COLORS.appPurple, + reason: "app platform taxonomy stays muted", + }, + { + family: "extension", + match: (name) => name.startsWith("extensions: "), + color: COLORS.neutralGray, + reason: "plugin implementation taxonomy should not compete with priority", + }, + { + family: "plugin", + match: (name) => name.startsWith("plugin: "), + color: COLORS.neutralGray, + reason: "plugin taxonomy stays neutral unless it becomes an action gate", + }, + { + family: "size", + match: (name) => name.startsWith("size: "), + color: COLORS.mutedRose, + reason: "size text carries the value, so one muted family color is enough", + }, + { + family: "triage-candidate", + match: (name) => name.startsWith("triage:"), + color: COLORS.triageBlue, + reason: "routine triage candidates stay softer than blockers and priorities", + }, +]; + +function wantedPolicy(name) { + if (EXACT_COLORS.has(name)) { + return { color: EXACT_COLORS.get(name), family: exactFamily(name), source: "exact" }; + } + const rule = FAMILY_RULES.find((candidate) => candidate.match(name)); + if (rule) { + return { color: rule.color, family: rule.family, source: "family" }; + } + return null; +} + +function exactFamily(name) { + if (/^P[0-3]$/.test(name)) { + return "priority"; + } + if (name.startsWith("impact:")) { + return "impact"; + } + if (name.startsWith("clawsweeper")) { + return "clawsweeper"; + } + if (name.startsWith("close:") || name.startsWith("r:")) { + return "resolution"; + } + if (name.startsWith("proof:")) { + return "proof"; + } + if (name.startsWith("triage:")) { + return "triage-status"; + } + if (name.startsWith("bug") || name === "security" || name === "regression") { + return "risk"; + } + return "specific"; +} + +function ghJson(args) { + return JSON.parse(execFileSync("gh", args, { encoding: "utf8" })); +} + +function gh(args) { + execFileSync("gh", args, { encoding: "utf8", stdio: APPLY ? "inherit" : "pipe" }); +} + +function fetchLabels() { + const labels = []; + let cursor = null; + for (;;) { + const query = `query($cursor:String) { + repository(owner:"openclaw", name:"openclaw") { + labels(first:100, after:$cursor, orderBy:{field:NAME,direction:ASC}) { + nodes { name color description } + pageInfo { hasNextPage endCursor } + } + } + }`; + const response = ghJson([ + "api", + "graphql", + "-f", + `query=${query}`, + "-f", + `cursor=${cursor ?? ""}`, + ]); + const page = response.data.repository.labels; + labels.push(...page.nodes); + if (!page.pageInfo.hasNextPage) { + return labels; + } + cursor = page.pageInfo.endCursor; + } +} + +const changes = fetchLabels() + .map((label) => ({ + name: label.name, + current: label.color.toUpperCase(), + policy: wantedPolicy(label.name), + })) + .filter((label) => label.policy && label.current !== label.policy.color) + .toSorted((a, b) => a.name.localeCompare(b.name)); + +if (changes.length === 0) { + console.log("No label color changes needed."); + process.exit(0); +} + +const familyCounts = new Map(); +for (const change of changes) { + familyCounts.set(change.policy.family, (familyCounts.get(change.policy.family) ?? 0) + 1); + console.log( + `${APPLY ? "update" : "would update"} [${change.policy.family}] ${change.name}: ${change.current} -> ${change.policy.color}`, + ); + if (APPLY) { + gh(["label", "edit", change.name, "--repo", REPO, "--color", change.policy.color]); + } +} + +console.log("Family summary:"); +for (const [family, count] of [...familyCounts.entries()].toSorted(([a], [b]) => + a.localeCompare(b), +)) { + console.log(`- ${family}: ${count}`); +} +console.log(`${APPLY ? "Updated" : "Would update"} ${changes.length} labels.`);