diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000..0f3344acc --- /dev/null +++ b/.github/labeler.yml @@ -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/**" diff --git a/.github/workflows/auto-response.yml b/.github/workflows/auto-response.yml new file mode 100644 index 000000000..7f242a094 --- /dev/null +++ b/.github/workflows/auto-response.yml @@ -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. We’re keeping the core lean on skills, so I’m 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", + }); + } diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 000000000..6ec73a1a3 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -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 diff --git a/scripts/sync-labels.ts b/scripts/sync-labels.ts new file mode 100644 index 000000000..0220e911a --- /dev/null +++ b/scripts/sync-labels.ts @@ -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; + +type RepoLabel = { + name: string; + color?: string; +}; + +const COLOR_BY_PREFIX = new Map([ + ["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 { + 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])); +}