summaryrefslogtreecommitdiffhomepage
path: root/script/beta.ts
blob: 6733e9147c5cdfe8b73e8b4b72d6368e6445f11a (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
#!/usr/bin/env bun

interface PR {
  number: number
  headRefName: string
  headRefOid: string
  createdAt: string
  isDraft: boolean
  title: string
}

async function main() {
  console.log("Fetching open contributor PRs...")

  const prsResult =
    await $`gh pr list --label contributor --state open --json number,headRefName,headRefOid,createdAt,isDraft,title --limit 100`.nothrow()
  if (prsResult.exitCode !== 0) {
    throw new Error(`Failed to fetch PRs: ${prsResult.stderr}`)
  }

  const allPRs: PR[] = JSON.parse(prsResult.stdout)
  const prs = allPRs.filter((pr) => !pr.isDraft)

  console.log(`Found ${prs.length} open non-draft contributor PRs`)

  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 skipped: Array<{ number: number; reason: string }> = []

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

    // Try to update PR branch via GitHub API (rebase onto base branch)
    console.log(`  Attempting to rebase PR #${pr.number} via GitHub API...`)
    const updateBranch = await $`gh pr update-branch ${pr.number} --rebase`.nothrow()
    if (updateBranch.exitCode !== 0) {
      console.log(`  Rebase failed for PR #${pr.number} (has conflicts)`)
      console.log(`  Error: ${updateBranch.stderr}`)
      skipped.push({ number: pr.number, reason: "Rebase failed (conflicts)" })
      continue
    }

    console.log(`  Rebase initiated for PR #${pr.number}`)

    // Wait for rebase to complete by polling PR state
    console.log(`  Waiting for rebase to complete...`)
    let rebaseComplete = false
    let attempts = 0
    const maxAttempts = 30

    while (!rebaseComplete && attempts < maxAttempts) {
      await Bun.sleep(2000) // Wait 2 seconds
      attempts++

      const prCheck = await $`gh pr view ${pr.number} --json mergeStateStatus,headRefOid`.nothrow()
      if (prCheck.exitCode === 0) {
        const prData = JSON.parse(prCheck.stdout)
        // mergeStateStatus will be "clean" when rebase is complete and no conflicts
        if (prData.mergeStateStatus === "clean") {
          rebaseComplete = true
          console.log(`  Rebase completed for PR #${pr.number}`)
        }
      }
    }

    if (!rebaseComplete) {
      console.log(`  Timeout waiting for rebase on PR #${pr.number}`)
      skipped.push({ number: pr.number, reason: "Timeout waiting for rebase" })
      continue
    }

    // Fetch the rebased PR
    const fetchPR = await $`git fetch origin pull/${pr.number}/head:pr-${pr.number}`.nothrow()
    if (fetchPR.exitCode !== 0) {
      console.log(`  Failed to fetch PR #${pr.number} after rebase, skipping`)
      skipped.push({ number: pr.number, reason: "Failed to fetch after rebase" })
      continue
    }

    const merge = await $`git merge --squash pr-${pr.number}`.nothrow()
    if (merge.exitCode !== 0) {
      console.log(`  Squash merge failed for PR #${pr.number}`)
      console.log(`  Error: ${merge.stderr}`)
      await $`git reset --hard HEAD`.nothrow()
      skipped.push({ number: pr.number, reason: `Squash merge failed: ${merge.stderr}` })
      continue
    }

    const add = await $`git add -A`.nothrow()
    if (add.exitCode !== 0) {
      console.log(`  Failed to stage changes for PR #${pr.number}`)
      await $`git reset --hard HEAD`.nothrow()
      skipped.push({ number: pr.number, reason: "Failed to stage" })
      continue
    }

    const status = await $`git status --porcelain`.nothrow()
    if (status.exitCode !== 0 || !status.stdout.trim()) {
      console.log(`  No changes to commit for PR #${pr.number}, skipping`)
      await $`git reset --hard HEAD`.nothrow()
      skipped.push({ number: pr.number, reason: "No changes to commit" })
      continue
    }

    const commitMsg = `Apply PR #${pr.number}: ${pr.title}`
    const commit = await Bun.spawn(["git", "commit", "-m", commitMsg], { stdout: "pipe", stderr: "pipe" })
    const commitExit = await commit.exited
    const commitStderr = await Bun.readableStreamToText(commit.stderr)

    if (commitExit !== 0) {
      console.log(`  Failed to commit PR #${pr.number}`)
      console.log(`  Error: ${commitStderr}`)
      await $`git reset --hard HEAD`.nothrow()
      skipped.push({ number: pr.number, reason: `Commit failed: ${commitStderr}` })
      continue
    }

    console.log(`  Successfully applied PR #${pr.number}`)
    applied.push(pr.number)
  }

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

  console.log("\nForce pushing beta branch...")
  const push = await $`git push origin beta --force`.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)
})

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 }
    },
  }
}