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
|
#!/usr/bin/env bun
interface PR {
number: number
title: string
}
interface Repo {
nameWithOwner: string
}
interface HeadRepo {
nameWithOwner: string
}
interface PRHead {
headRefName: string
headRepository: HeadRepo
}
async function main() {
console.log("Fetching open contributor PRs...")
const prsResult = await $`gh pr list --label contributor --state open --json number,title --limit 100`.nothrow()
if (prsResult.exitCode !== 0) {
throw new Error(`Failed to fetch PRs: ${prsResult.stderr}`)
}
const prs: PR[] = JSON.parse(prsResult.stdout)
console.log(`Found ${prs.length} open contributor PRs`)
const repoResult = await $`gh repo view --json nameWithOwner`.nothrow()
if (repoResult.exitCode !== 0) {
throw new Error(`Failed to fetch repo info: ${repoResult.stderr}`)
}
const repo: Repo = JSON.parse(repoResult.stdout)
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}`)
const headResult = await $`gh pr view ${pr.number} --json headRefName,headRepository`.nothrow()
if (headResult.exitCode !== 0) {
console.log(` Failed to get head info`)
skipped.push({ number: pr.number, reason: `Failed to get head info: ${headResult.stderr}` })
continue
}
const head: PRHead = JSON.parse(headResult.stdout)
// Get the diff from GitHub compare API
console.log(` Getting diff...`)
const compare = `${repo.nameWithOwner}/compare/dev...${head.headRepository.nameWithOwner}:${head.headRefName}`
const diffResult = await $`gh api -H Accept:application/vnd.github.v3.diff repos/${compare}`.nothrow()
if (diffResult.exitCode !== 0) {
console.log(` Failed to get diff`)
skipped.push({ number: pr.number, reason: `Failed to get diff: ${diffResult.stderr}` })
continue
}
if (!diffResult.stdout.trim()) {
console.log(` No changes, skipping`)
skipped.push({ number: pr.number, reason: "No changes" })
continue
}
// Try to apply the diff
console.log(` Applying...`)
const apply = await Bun.spawn(["git", "apply", "--3way"], {
stdin: new TextEncoder().encode(diffResult.stdout),
stdout: "pipe",
stderr: "pipe",
})
const applyExit = await apply.exited
const applyStderr = await Bun.readableStreamToText(apply.stderr)
if (applyExit !== 0) {
console.log(` Failed to apply (conflicts)`)
await $`git checkout -- .`.nothrow()
await $`git clean -fd`.nothrow()
skipped.push({ number: pr.number, reason: "Has conflicts" })
continue
}
// Stage and commit
const add = await $`git add -A`.nothrow()
if (add.exitCode !== 0) {
console.log(` Failed to stage`)
await $`git checkout -- .`.nothrow()
await $`git clean -fd`.nothrow()
skipped.push({ number: pr.number, reason: "Failed to stage" })
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: ${commitStderr}`)
await $`git checkout -- .`.nothrow()
await $`git clean -fd`.nothrow()
skipped.push({ number: pr.number, reason: `Commit failed: ${commitStderr}` })
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}`))
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 --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)
})
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 }
},
}
}
|