Files
openclaw/scripts/sync-labels.ts
Tak Hoffman 9f0305420a docs: add beta blocker contributor guidance (#55199)
* docs: add beta blocker contributor guidance

* fix: tighten beta blocker labeling and flaky config test
2026-03-26 09:31:59 -05:00

138 lines
3.4 KiB
TypeScript

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
type RepoLabel = {
name: string;
color?: string;
description?: string;
};
const COLOR_BY_PREFIX = new Map<string, string>([
["channel", "1d76db"],
["app", "6f42c1"],
["extensions", "0e8a16"],
["docs", "0075ca"],
["cli", "f9d0c4"],
["gateway", "d4c5f9"],
["size", "fbca04"],
]);
const EXTRA_LABEL_METADATA = new Map<
string,
{
color: string;
description?: string;
}
>([
[
"beta-blocker",
{
color: "D93F0B",
description: "Plugin beta-release blocker pending stable cutoff triage",
},
],
]);
const configPath = resolve(".github/labeler.yml");
const EXTRA_LABELS = [
"size: XS",
"size: S",
"size: M",
"size: L",
"size: XL",
"beta-blocker",
] as const;
const labelNames = [
...new Set([...extractLabelNames(readFileSync(configPath, "utf8")), ...EXTRA_LABELS]),
];
if (!labelNames.length) {
throw new Error("labeler.yml must declare at least one label.");
}
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" });
console.log(`Created label: ${label}`);
}
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;
}
const prefix = label.includes(":") ? label.split(":", 1)[0].trim() : label.trim();
return { color: COLOR_BY_PREFIX.get(prefix) ?? "ededed" };
}
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> {
const raw = execFileSync("gh", ["api", `repos/${repo}/labels?per_page=100`, "--paginate"], {
encoding: "utf8",
});
const labels = JSON.parse(raw) as RepoLabel[];
return new Map(labels.map((label) => [label.name, label]));
}