github-triage.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. /// <reference path="../env.d.ts" />
  2. import { tool } from "@opencode-ai/plugin"
  3. import DESCRIPTION from "./github-triage.txt"
  4. const TEAM = {
  5. desktop: ["adamdotdevin", "iamdavidhill", "Brendonovich", "nexxeln"],
  6. zen: ["fwang", "MrMushrooooom"],
  7. tui: [
  8. "thdxr",
  9. "kommander",
  10. // "rekram1-node" (on vacation)
  11. ],
  12. core: [
  13. "thdxr",
  14. // "rekram1-node", (on vacation)
  15. "jlongster",
  16. ],
  17. docs: ["R44VC0RP"],
  18. windows: ["Hona"],
  19. } as const
  20. const ASSIGNEES = [...new Set(Object.values(TEAM).flat())]
  21. function pick<T>(items: readonly T[]) {
  22. return items[Math.floor(Math.random() * items.length)]!
  23. }
  24. function getIssueNumber(): number {
  25. const issue = parseInt(process.env.ISSUE_NUMBER ?? "", 10)
  26. if (!issue) throw new Error("ISSUE_NUMBER env var not set")
  27. return issue
  28. }
  29. async function githubFetch(endpoint: string, options: RequestInit = {}) {
  30. const response = await fetch(`https://api.github.com${endpoint}`, {
  31. ...options,
  32. headers: {
  33. Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
  34. Accept: "application/vnd.github+json",
  35. "Content-Type": "application/json",
  36. ...options.headers,
  37. },
  38. })
  39. if (!response.ok) {
  40. throw new Error(`GitHub API error: ${response.status} ${response.statusText}`)
  41. }
  42. return response.json()
  43. }
  44. export default tool({
  45. description: DESCRIPTION,
  46. args: {
  47. assignee: tool.schema.enum(ASSIGNEES as [string, ...string[]]).describe("The username of the assignee"),
  48. labels: tool.schema
  49. .array(tool.schema.enum(["nix", "opentui", "perf", "web", "desktop", "zen", "docs", "windows", "core"]))
  50. .describe("The labels(s) to add to the issue")
  51. .default([]),
  52. },
  53. async execute(args) {
  54. const issue = getIssueNumber()
  55. const owner = "anomalyco"
  56. const repo = "opencode"
  57. const results: string[] = []
  58. let labels = [...new Set(args.labels.map((x) => (x === "desktop" ? "web" : x)))]
  59. const web = labels.includes("web")
  60. const text = `${process.env.ISSUE_TITLE ?? ""}\n${process.env.ISSUE_BODY ?? ""}`.toLowerCase()
  61. const zen = /\bzen\b/.test(text) || text.includes("opencode black")
  62. const nix = /\bnix(os)?\b/.test(text)
  63. if (labels.includes("nix") && !nix) {
  64. labels = labels.filter((x) => x !== "nix")
  65. results.push("Dropped label: nix (issue does not mention nix)")
  66. }
  67. // const assignee = nix ? "rekram1-node" : web ? pick(TEAM.desktop) : args.assignee
  68. const assignee = web ? pick(TEAM.desktop) : args.assignee
  69. if (labels.includes("zen") && !zen) {
  70. throw new Error("Only add the zen label when issue title/body contains 'zen'")
  71. }
  72. if (web && !nix && !(TEAM.desktop as readonly string[]).includes(assignee)) {
  73. throw new Error("Web issues must be assigned to adamdotdevin, iamdavidhill, Brendonovich, or nexxeln")
  74. }
  75. if ((TEAM.zen as readonly string[]).includes(assignee) && !labels.includes("zen")) {
  76. throw new Error("Only zen issues should be assigned to fwang or MrMushrooooom")
  77. }
  78. if (assignee === "Hona" && !labels.includes("windows")) {
  79. throw new Error("Only windows issues should be assigned to Hona")
  80. }
  81. if (assignee === "R44VC0RP" && !labels.includes("docs")) {
  82. throw new Error("Only docs issues should be assigned to R44VC0RP")
  83. }
  84. if (assignee === "kommander" && !labels.includes("opentui")) {
  85. throw new Error("Only opentui issues should be assigned to kommander")
  86. }
  87. await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/assignees`, {
  88. method: "POST",
  89. body: JSON.stringify({ assignees: [assignee] }),
  90. })
  91. results.push(`Assigned @${assignee} to issue #${issue}`)
  92. if (labels.length > 0) {
  93. await githubFetch(`/repos/${owner}/${repo}/issues/${issue}/labels`, {
  94. method: "POST",
  95. body: JSON.stringify({ labels }),
  96. })
  97. results.push(`Added labels: ${labels.join(", ")}`)
  98. }
  99. return results.join("\n")
  100. },
  101. })