Files
openclaw/scripts/sync-openclaw-label-colors.mjs
Tak Hoffman 816fbe0cf0 chore(labels): cool label palette (#83374)
* chore(labels): cool label palette

* chore(labels): soften taxonomy colors

* chore(labels): finalize label palette

* chore(labels): harden final palette
2026-05-17 21:12:10 -05:00

301 lines
9.5 KiB
JavaScript

#!/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: "8C959F",
paleGreen: "D6E3DA",
proofGreen: "2DA44E",
mutedProofGreen: "1A7F37",
overrideGreen: "2DA44E",
saturatedBlue: "0F2CCE",
paleBlue: "0A3069",
channelBlue: "0969DA",
dedupeBlue: "57606A",
triageBlue: "0969DA",
saturatedPurple: "7057FF",
mutedPurple: "57606A",
taxonomyGray: "6E7781",
taxonomySteel: "57606A",
appPurple: "6E7781",
neutralGray: "E5E7EB",
duplicateGray: "D1D5DB",
darkGray: "8C8C8C",
mutedRose: "8C959F",
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,
"rating: 🦀 challenger crab": "1F883D",
"rating: 🦞 diamond lobster": "0969DA",
"rating: 🐚 platinum hermit": "0F766E",
"rating: 🦐 gold shrimp": "B7791F",
"rating: 🦪 silver shellfish": "7A828E",
"rating: 🧂 unranked krab": "8C2F39",
"rating: 🌊 off-meta tidepool": "6E7781",
"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.taxonomySteel,
docs: COLORS.paleBlue,
cli: COLORS.paleBlue,
commands: COLORS.paleBlue,
scripts: COLORS.mutedPurple,
gateway: COLORS.mutedPurple,
codex: COLORS.taxonomySteel,
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.taxonomyGray,
reason: "plugin implementation taxonomy should not compete with priority",
},
{
family: "plugin",
match: (name) => name.startsWith("plugin: "),
color: COLORS.taxonomyGray,
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("rating:")) {
return "rating";
}
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.`);