summaryrefslogtreecommitdiffhomepage
path: root/script/beta.ts
blob: fff1e91e0d9d28172b01d79374bfb40d9fa414d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env bun

import { Script } from "@opencode-ai/script"
import { parseArgs } from "util"

interface PR {
  number: number
  title: string
  author: { login: string }
}

interface RunResult {
  exitCode: number
  stdout: string
  stderr: string
}

interface FailedPR {
  number: number
  title: string
  reason: string
}

async function postToDiscord(failures: FailedPR[], webhookUrl?: string) {
  const url = webhookUrl || process.env.DISCORD_ISSUES_WEBHOOK_URL
  if (!url) {
    console.log("Warning: No Discord webhook URL provided, skipping notification")
    return
  }

  const message = `**Beta Branch Merge Failures**

The following team PRs failed to merge into the beta branch:

${failures.map((f) => `- **#${f.number}**: ${f.title} - ${f.reason}`).join("\n")}

Please resolve these conflicts manually.`

  const content = JSON.stringify({ content: message })

  const response = await fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: content,
  })

  if (!response.ok) {
    console.error("Failed to post to Discord:", await response.text())
  } else {
    console.log("Posted failures to Discord")
  }
}

async function main() {
  const { values } = parseArgs({
    args: Bun.argv.slice(2),
    options: {
      "discord-webhook": { type: "string", short: "d" },
    },
  })

  const discordWebhook = values["discord-webhook"] as string | undefined

  console.log("Fetching open PRs from team members...")

  const allPrs: PR[] = []
  for (const member of Script.team) {
    const result = await $`gh pr list --state open --author ${member} --json number,title,author --limit 100`.nothrow()
    if (result.exitCode !== 0) continue
    const memberPrs: PR[] = JSON.parse(result.stdout)
    allPrs.push(...memberPrs)
  }

  const seen = new Set<number>()
  const prs = allPrs.filter((pr) => {
    if (seen.has(pr.number)) return false
    seen.add(pr.number)
    return true
  })

  console.log(`Found ${prs.length} open PRs from team members`)

  if (prs.length === 0) {
    console.log("No team PRs to merge")
    return
  }

  console.log("Fetching latest dev branch...")
  const fetchDev = await $`git fetch origin dev`.nothrow()
  if (fetchDev.exitCode !== 0) {
    throw new Error(`Failed to fetch dev branch: ${fetchDev.stderr}`)
  }

  console.log("Checking out beta branch...")
  const checkoutBeta = await $`git checkout -B beta origin/dev`.nothrow()
  if (checkoutBeta.exitCode !== 0) {
    throw new Error(`Failed to checkout beta branch: ${checkoutBeta.stderr}`)
  }

  const applied: number[] = []
  const failed: FailedPR[] = []

  for (const pr of prs) {
    console.log(`\nProcessing PR #${pr.number}: ${pr.title}`)

    console.log("  Fetching PR head...")
    const fetch = await run(["git", "fetch", "origin", `pull/${pr.number}/head:pr/${pr.number}`])
    if (fetch.exitCode !== 0) {
      console.log(`  Failed to fetch: ${fetch.stderr}`)
      failed.push({ number: pr.number, title: pr.title, reason: "Fetch failed" })
      continue
    }

    console.log("  Merging...")
    const merge = await run(["git", "merge", "--no-commit", "--no-ff", `pr/${pr.number}`])
    if (merge.exitCode !== 0) {
      console.log("  Failed to merge (conflicts)")
      await $`git merge --abort`.nothrow()
      await $`git checkout -- .`.nothrow()
      await $`git clean -fd`.nothrow()
      failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" })
      continue
    }

    const mergeHead = await $`git rev-parse -q --verify MERGE_HEAD`.nothrow()
    if (mergeHead.exitCode !== 0) {
      console.log("  No changes, skipping")
      continue
    }

    const add = await $`git add -A`.nothrow()
    if (add.exitCode !== 0) {
      console.log("  Failed to stage changes")
      failed.push({ number: pr.number, title: pr.title, reason: "Staging failed" })
      continue
    }

    const commitMsg = `Apply PR #${pr.number}: ${pr.title}`
    const commit = await run(["git", "commit", "-m", commitMsg])
    if (commit.exitCode !== 0) {
      console.log(`  Failed to commit: ${commit.stderr}`)
      failed.push({ number: pr.number, title: pr.title, reason: "Commit failed" })
      continue
    }

    console.log("  Applied successfully")
    applied.push(pr.number)
  }

  console.log("\n--- Summary ---")
  console.log(`Applied: ${applied.length} PRs`)
  applied.forEach((num) => console.log(`  - PR #${num}`))

  if (failed.length > 0) {
    console.log(`Failed: ${failed.length} PRs`)
    failed.forEach((f) => console.log(`  - PR #${f.number}: ${f.reason}`))

    await postToDiscord(failed, discordWebhook)

    throw new Error(`${failed.length} PR(s) failed to merge. Check Discord for details.`)
  }

  console.log("\nForce pushing beta branch...")
  const push = await $`git push origin beta --force --no-verify`.nothrow()
  if (push.exitCode !== 0) {
    throw new Error(`Failed to push beta branch: ${push.stderr}`)
  }

  console.log("Successfully synced beta branch")
}

main().catch((err) => {
  console.error("Error:", err)
  process.exit(1)
})

async function run(args: string[], stdin?: Uint8Array): Promise<RunResult> {
  const proc = Bun.spawn(args, {
    stdin: stdin ?? "inherit",
    stdout: "pipe",
    stderr: "pipe",
  })
  const exitCode = await proc.exited
  const stdout = await new Response(proc.stdout).text()
  const stderr = await new Response(proc.stderr).text()
  return { exitCode, stdout, stderr }
}

function $(strings: TemplateStringsArray, ...values: unknown[]) {
  const cmd = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "")
  return {
    async nothrow() {
      const proc = Bun.spawn(cmd.split(" "), {
        stdout: "pipe",
        stderr: "pipe",
      })
      const exitCode = await proc.exited
      const stdout = await new Response(proc.stdout).text()
      const stderr = await new Response(proc.stderr).text()
      return { exitCode, stdout, stderr }
    },
  }
}