triage-unassigned.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. #!/usr/bin/env bun
  2. import { parseArgs } from "util"
  3. async function run(command: string, args: string[], options: Bun.SpawnOptions.OptionsObject = {}) {
  4. const process = Bun.spawn([command, ...args], options)
  5. const status = await process.exited
  6. if (status !== 0) throw new Error(`${command} ${args.join(" ")} exited with ${status}`)
  7. return process
  8. }
  9. async function text(command: string, args: string[]) {
  10. const process = await run(command, args, { stdout: "pipe", stderr: "inherit" })
  11. return new Response(process.stdout).text()
  12. }
  13. async function main() {
  14. const { values } = parseArgs({
  15. args: Bun.argv.slice(2),
  16. options: {
  17. days: { type: "string", short: "d", default: "30" },
  18. limit: { type: "string", short: "l", default: "200" },
  19. "dry-run": { type: "boolean", default: false },
  20. help: { type: "boolean", short: "h", default: false },
  21. },
  22. })
  23. if (values.help) {
  24. console.log(`
  25. Usage: bun script/triage-unassigned.ts [options]
  26. Triage open GitHub issues created in the last 30 days with no assignee.
  27. Options:
  28. -d, --days <days> Look back this many days (default: 30)
  29. -l, --limit <count> Maximum issues to process (default: 200)
  30. --dry-run Print matching issues without running triage
  31. -h, --help Show this help message
  32. Examples:
  33. bun script/triage-unassigned.ts
  34. bun script/triage-unassigned.ts --limit 3
  35. bun script/triage-unassigned.ts --dry-run
  36. `)
  37. process.exit(0)
  38. }
  39. const days = Number(values.days)
  40. const limit = Number(values.limit)
  41. if (!Number.isInteger(days) || days < 1) throw new Error("--days must be a positive integer")
  42. if (!Number.isInteger(limit) || limit < 1) throw new Error("--limit must be a positive integer")
  43. const created = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString().slice(0, 10)
  44. const query = `no:assignee created:>=${created}`
  45. const issues = JSON.parse(
  46. await text("gh", [
  47. "issue",
  48. "list",
  49. "--state",
  50. "open",
  51. "--search",
  52. query,
  53. "--limit",
  54. String(limit),
  55. "--json",
  56. "number,title,body",
  57. ]),
  58. ) as Array<{ number: number; title: string; body?: string | null }>
  59. console.log(`Found ${issues.length} open unassigned issues created since ${created}`)
  60. if (issues.length === 0) return
  61. if (values["dry-run"]) {
  62. for (const issue of issues) console.log(`#${issue.number} ${issue.title}`)
  63. return
  64. }
  65. const githubToken = process.env.GITHUB_TOKEN || (await text("gh", ["auth", "token"])).trim()
  66. const failures: Array<{ issue: number; error: string }> = []
  67. for (const [index, issue] of issues.entries()) {
  68. console.log(`\n[${index + 1}/${issues.length}] Triaging #${issue.number} ${issue.title}`)
  69. const result = Bun.spawn(
  70. [
  71. "opencode",
  72. "run",
  73. "--agent",
  74. "triage",
  75. `The following issue was just opened, triage it:
  76. Issue: #${issue.number}
  77. Title: ${issue.title}
  78. Body:
  79. ${issue.body ?? ""}`,
  80. ],
  81. {
  82. env: {
  83. ...process.env,
  84. GITHUB_TOKEN: githubToken,
  85. ISSUE_NUMBER: String(issue.number),
  86. ISSUE_TITLE: issue.title,
  87. ISSUE_BODY: issue.body ?? "",
  88. },
  89. stdin: "inherit",
  90. stdout: "inherit",
  91. stderr: "inherit",
  92. },
  93. )
  94. const status = await result.exited
  95. if (status === 0) {
  96. console.log(`[${index + 1}/${issues.length}] Done #${issue.number}`)
  97. continue
  98. }
  99. failures.push({ issue: issue.number, error: `opencode exited with ${status}` })
  100. console.error(`[${index + 1}/${issues.length}] Failed #${issue.number}: opencode exited with ${status}`)
  101. }
  102. console.log(`\nFinished triaging ${issues.length - failures.length}/${issues.length} issues`)
  103. if (failures.length === 0) return
  104. console.error("Failures:")
  105. for (const failure of failures) console.error(`#${failure.issue}: ${failure.error}`)
  106. process.exit(1)
  107. }
  108. void main()