CI: add PR labeler + label sync

This commit is contained in:
Shadow
2026-01-25 20:37:20 -06:00
parent 136f0d4d1d
commit 6b6284c69c
4 changed files with 317 additions and 0 deletions

150
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,150 @@
"channel: bluebubbles":
- "extensions/bluebubbles/**"
- "docs/channels/bluebubbles.md"
"channel: discord":
- "src/discord/**"
- "extensions/discord/**"
- "docs/channels/discord.md"
"channel: googlechat":
- "extensions/googlechat/**"
- "docs/channels/googlechat.md"
"channel: imessage":
- "src/imessage/**"
- "extensions/imessage/**"
- "docs/channels/imessage.md"
"channel: line":
- "extensions/line/**"
"channel: matrix":
- "extensions/matrix/**"
- "docs/channels/matrix.md"
"channel: mattermost":
- "extensions/mattermost/**"
- "docs/channels/mattermost.md"
"channel: msteams":
- "extensions/msteams/**"
- "docs/channels/msteams.md"
"channel: nextcloud-talk":
- "extensions/nextcloud-talk/**"
- "docs/channels/nextcloud-talk.md"
"channel: nostr":
- "extensions/nostr/**"
- "docs/channels/nostr.md"
"channel: signal":
- "src/signal/**"
- "extensions/signal/**"
- "docs/channels/signal.md"
"channel: slack":
- "src/slack/**"
- "extensions/slack/**"
- "docs/channels/slack.md"
"channel: telegram":
- "src/telegram/**"
- "extensions/telegram/**"
- "docs/channels/telegram.md"
"channel: tlon":
- "extensions/tlon/**"
- "docs/channels/tlon.md"
"channel: voice-call":
- "extensions/voice-call/**"
"channel: whatsapp-web":
- "src/web/**"
- "extensions/whatsapp/**"
- "docs/channels/whatsapp.md"
"channel: zalo":
- "extensions/zalo/**"
- "docs/channels/zalo.md"
"channel: zalouser":
- "extensions/zalouser/**"
- "docs/channels/zalouser.md"
"app: android":
- "apps/android/**"
- "docs/platforms/android.md"
"app: ios":
- "apps/ios/**"
- "docs/platforms/ios.md"
"app: macos":
- "apps/macos/**"
- "docs/platforms/macos.md"
- "docs/platforms/mac/**"
"app: web-ui":
- "ui/**"
- "src/gateway/control-ui.ts"
- "src/gateway/control-ui-shared.ts"
- "src/infra/control-ui-assets.ts"
"cli":
- "src/cli/**"
- "src/commands/**"
- "src/tui/**"
"gateway":
- "src/gateway/**"
- "src/daemon/**"
- "docs/gateway/**"
"docs":
- "docs/**"
- "docs.acp.md"
- "README.md"
- "README-header.png"
- "CHANGELOG.md"
- "CONTRIBUTING.md"
- "SECURITY.md"
"extensions: bluebubbles":
- "extensions/bluebubbles/**"
"extensions: copilot-proxy":
- "extensions/copilot-proxy/**"
"extensions: diagnostics-otel":
- "extensions/diagnostics-otel/**"
"extensions: discord":
- "extensions/discord/**"
"extensions: google-antigravity-auth":
- "extensions/google-antigravity-auth/**"
"extensions: google-gemini-cli-auth":
- "extensions/google-gemini-cli-auth/**"
"extensions: googlechat":
- "extensions/googlechat/**"
"extensions: imessage":
- "extensions/imessage/**"
"extensions: line":
- "extensions/line/**"
"extensions: llm-task":
- "extensions/llm-task/**"
"extensions: lobster":
- "extensions/lobster/**"
"extensions: matrix":
- "extensions/matrix/**"
"extensions: mattermost":
- "extensions/mattermost/**"
"extensions: memory-core":
- "extensions/memory-core/**"
"extensions: memory-lancedb":
- "extensions/memory-lancedb/**"
"extensions: msteams":
- "extensions/msteams/**"
"extensions: nextcloud-talk":
- "extensions/nextcloud-talk/**"
"extensions: nostr":
- "extensions/nostr/**"
"extensions: open-prose":
- "extensions/open-prose/**"
"extensions: qwen-portal-auth":
- "extensions/qwen-portal-auth/**"
"extensions: signal":
- "extensions/signal/**"
"extensions: slack":
- "extensions/slack/**"
"extensions: telegram":
- "extensions/telegram/**"
"extensions: tlon":
- "extensions/tlon/**"
"extensions: voice-call":
- "extensions/voice-call/**"
"extensions: whatsapp":
- "extensions/whatsapp/**"
"extensions: zalo":
- "extensions/zalo/**"
"extensions: zalouser":
- "extensions/zalouser/**"

59
.github/workflows/auto-response.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Auto response
on:
issues:
types: [labeled]
pull_request:
types: [labeled]
permissions:
issues: write
pull-requests: write
jobs:
auto-response:
runs-on: ubuntu-latest
steps:
- name: Handle labeled items
uses: actions/github-script@v7
with:
script: |
const rules = [
{
label: "skill-clawdhub",
close: true,
message:
"Thanks for the contribution! New skills should be published to Clawdhub for everyone to use. Were keeping the core lean on skills, so Im closing this out.",
},
];
const labelName = context.payload.label?.name;
if (!labelName) {
return;
}
const rule = rules.find((item) => item.label === labelName);
if (!rule) {
return;
}
const issueNumber = context.payload.issue?.number ?? context.payload.pull_request?.number;
if (!issueNumber) {
return;
}
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: rule.message,
});
if (rule.close) {
await github.rest.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
state: "closed",
});
}

17
.github/workflows/labeler.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Labeler
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
pull-requests: write
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
with:
configuration-path: .github/labeler.yml

91
scripts/sync-labels.ts Normal file
View File

@@ -0,0 +1,91 @@
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import yaml from "yaml";
type LabelConfig = Record<string, unknown>;
type RepoLabel = {
name: string;
color?: string;
};
const COLOR_BY_PREFIX = new Map<string, string>([
["channel", "1d76db"],
["app", "6f42c1"],
["extensions", "0e8a16"],
["docs", "0075ca"],
["cli", "f9d0c4"],
["gateway", "d4c5f9"],
]);
const configPath = resolve(".github/labeler.yml");
const config = yaml.parse(readFileSync(configPath, "utf8")) as LabelConfig;
if (!config || typeof config !== "object") {
throw new Error("labeler.yml must be a mapping of label names to globs.");
}
const labelNames = Object.keys(config).filter(Boolean);
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 color = pickColor(label);
execFileSync(
"gh",
[
"api",
"-X",
"POST",
`repos/${repo}/labels`,
"-f",
`name=${label}`,
"-f",
`color=${color}`,
],
{ stdio: "inherit" },
);
console.log(`Created label: ${label}`);
}
function pickColor(label: string): string {
const prefix = label.includes(":") ? label.split(":", 1)[0].trim() : label.trim();
return 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]));
}