Files
openclaw/scripts/sync-labels.ts
T

138 lines
3.4 KiB
TypeScript
Raw Normal View History

2026-01-25 20:37:20 -06:00
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
type RepoLabel = {
name: string;
color?: string;
description?: string;
2026-01-25 20:37:20 -06:00
};
const COLOR_BY_PREFIX = new Map<string, string>([
["channel", "1d76db"],
["app", "6f42c1"],
["extensions", "0e8a16"],
["docs", "0075ca"],
["cli", "f9d0c4"],
["gateway", "d4c5f9"],
2026-02-11 21:12:27 -06:00
["size", "fbca04"],
2026-01-25 20:37:20 -06:00
]);
const EXTRA_LABEL_METADATA = new Map<
string,
{
color: string;
description?: string;
}
>([
[
"beta-blocker",
{
color: "D93F0B",
description: "Plugin beta-release blocker pending stable cutoff triage",
},
],
]);
2026-01-25 20:37:20 -06:00
const configPath = resolve(".github/labeler.yml");
const EXTRA_LABELS = [
"size: XS",
"size: S",
"size: M",
"size: L",
"size: XL",
"beta-blocker",
] as const;
2026-02-11 21:12:27 -06:00
const labelNames = [
...new Set([...extractLabelNames(readFileSync(configPath, "utf8")), ...EXTRA_LABELS]),
];
2026-01-25 20:37:20 -06:00
2026-01-25 20:38:44 -06:00
if (!labelNames.length) {
throw new Error("labeler.yml must declare at least one label.");
2026-01-25 20:37:20 -06:00
}
const repo = resolveRepo();
const existing = fetchExistingLabels(repo);
const missing = labelNames.filter((label) => !existing.has(label));
if (!missing.length) {
console.log("All labeler labels already exist.");
process.exit(0);
}
for (const label of missing) {
const metadata = resolveLabelMetadata(label);
const args = [
"api",
"-X",
"POST",
`repos/${repo}/labels`,
"-f",
`name=${label}`,
"-f",
`color=${metadata.color}`,
];
if (metadata.description) {
args.push("-f", `description=${metadata.description}`);
}
execFileSync("gh", args, { stdio: "inherit" });
2026-01-25 20:37:20 -06:00
console.log(`Created label: ${label}`);
}
2026-01-25 20:38:44 -06:00
function extractLabelNames(contents: string): string[] {
const labels: string[] = [];
for (const line of contents.split("\n")) {
if (!line.trim() || line.trimStart().startsWith("#")) {
continue;
}
if (/^\s/.test(line)) {
continue;
}
const match = line.match(/^(["'])(.+)\1\s*:/) ?? line.match(/^([^:]+):/);
if (match) {
const name = (match[2] ?? match[1] ?? "").trim();
if (name) {
labels.push(name);
}
}
}
return labels;
}
function resolveLabelMetadata(label: string): { color: string; description?: string } {
const extraMetadata = EXTRA_LABEL_METADATA.get(label);
if (extraMetadata) {
return extraMetadata;
}
2026-01-25 20:37:20 -06:00
const prefix = label.includes(":") ? label.split(":", 1)[0].trim() : label.trim();
return { color: COLOR_BY_PREFIX.get(prefix) ?? "ededed" };
2026-01-25 20:37:20 -06:00
}
function resolveRepo(): string {
const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
encoding: "utf8",
}).trim();
if (!remote) {
throw new Error("Unable to determine repository from git remote.");
}
if (remote.startsWith("git@github.com:")) {
return remote.replace("git@github.com:", "").replace(/\.git$/, "");
}
if (remote.startsWith("https://github.com/")) {
return remote.replace("https://github.com/", "").replace(/\.git$/, "");
}
throw new Error(`Unsupported GitHub remote: ${remote}`);
}
function fetchExistingLabels(repo: string): Map<string, RepoLabel> {
2026-01-31 21:21:09 +09:00
const raw = execFileSync("gh", ["api", `repos/${repo}/labels?per_page=100`, "--paginate"], {
encoding: "utf8",
});
2026-01-25 20:37:20 -06:00
const labels = JSON.parse(raw) as RepoLabel[];
return new Map(labels.map((label) => [label.name, label]));
}