mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-26 09:25:13 +00:00
chore(labels): add label color sync policy (#83357)
* chore(labels): add label color sync script * chore(labels): align future label colors
This commit is contained in:
@@ -9,13 +9,17 @@ type RepoLabel = {
|
||||
};
|
||||
|
||||
const COLOR_BY_PREFIX = new Map<string, string>([
|
||||
["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<
|
||||
|
||||
288
scripts/sync-openclaw-label-colors.mjs
Normal file
288
scripts/sync-openclaw-label-colors.mjs
Normal file
@@ -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.`);
|
||||
Reference in New Issue
Block a user