beta.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. #!/usr/bin/env bun
  2. interface PR {
  3. number: number
  4. title: string
  5. }
  6. interface Repo {
  7. nameWithOwner: string
  8. }
  9. interface HeadRepo {
  10. nameWithOwner: string
  11. }
  12. interface PRHead {
  13. headRefName: string
  14. headRepository: HeadRepo
  15. }
  16. async function main() {
  17. console.log("Fetching open contributor PRs...")
  18. const prsResult = await $`gh pr list --label contributor --state open --json number,title --limit 100`.nothrow()
  19. if (prsResult.exitCode !== 0) {
  20. throw new Error(`Failed to fetch PRs: ${prsResult.stderr}`)
  21. }
  22. const prs: PR[] = JSON.parse(prsResult.stdout)
  23. console.log(`Found ${prs.length} open contributor PRs`)
  24. const repoResult = await $`gh repo view --json nameWithOwner`.nothrow()
  25. if (repoResult.exitCode !== 0) {
  26. throw new Error(`Failed to fetch repo info: ${repoResult.stderr}`)
  27. }
  28. const repo: Repo = JSON.parse(repoResult.stdout)
  29. console.log("Fetching latest dev branch...")
  30. const fetchDev = await $`git fetch origin dev`.nothrow()
  31. if (fetchDev.exitCode !== 0) {
  32. throw new Error(`Failed to fetch dev branch: ${fetchDev.stderr}`)
  33. }
  34. console.log("Checking out beta branch...")
  35. const checkoutBeta = await $`git checkout -B beta origin/dev`.nothrow()
  36. if (checkoutBeta.exitCode !== 0) {
  37. throw new Error(`Failed to checkout beta branch: ${checkoutBeta.stderr}`)
  38. }
  39. const applied: number[] = []
  40. const skipped: Array<{ number: number; reason: string }> = []
  41. for (const pr of prs) {
  42. console.log(`\nProcessing PR #${pr.number}: ${pr.title}`)
  43. const headResult = await $`gh pr view ${pr.number} --json headRefName,headRepository`.nothrow()
  44. if (headResult.exitCode !== 0) {
  45. console.log(` Failed to get head info`)
  46. skipped.push({ number: pr.number, reason: `Failed to get head info: ${headResult.stderr}` })
  47. continue
  48. }
  49. const head: PRHead = JSON.parse(headResult.stdout)
  50. // Get the diff from GitHub compare API
  51. console.log(` Getting diff...`)
  52. const compare = `${repo.nameWithOwner}/compare/dev...${head.headRepository.nameWithOwner}:${head.headRefName}`
  53. const diffResult = await $`gh api -H Accept:application/vnd.github.v3.diff repos/${compare}`.nothrow()
  54. if (diffResult.exitCode !== 0) {
  55. console.log(` Failed to get diff`)
  56. skipped.push({ number: pr.number, reason: `Failed to get diff: ${diffResult.stderr}` })
  57. continue
  58. }
  59. if (!diffResult.stdout.trim()) {
  60. console.log(` No changes, skipping`)
  61. skipped.push({ number: pr.number, reason: "No changes" })
  62. continue
  63. }
  64. // Try to apply the diff
  65. console.log(` Applying...`)
  66. const apply = await Bun.spawn(["git", "apply", "--3way"], {
  67. stdin: new TextEncoder().encode(diffResult.stdout),
  68. stdout: "pipe",
  69. stderr: "pipe",
  70. })
  71. const applyExit = await apply.exited
  72. const applyStderr = await Bun.readableStreamToText(apply.stderr)
  73. if (applyExit !== 0) {
  74. console.log(` Failed to apply (conflicts)`)
  75. await $`git checkout -- .`.nothrow()
  76. await $`git clean -fd`.nothrow()
  77. skipped.push({ number: pr.number, reason: "Has conflicts" })
  78. continue
  79. }
  80. // Stage and commit
  81. const add = await $`git add -A`.nothrow()
  82. if (add.exitCode !== 0) {
  83. console.log(` Failed to stage`)
  84. await $`git checkout -- .`.nothrow()
  85. await $`git clean -fd`.nothrow()
  86. skipped.push({ number: pr.number, reason: "Failed to stage" })
  87. continue
  88. }
  89. const commitMsg = `Apply PR #${pr.number}: ${pr.title}`
  90. const commit = await Bun.spawn(["git", "commit", "-m", commitMsg], {
  91. stdout: "pipe",
  92. stderr: "pipe",
  93. })
  94. const commitExit = await commit.exited
  95. const commitStderr = await Bun.readableStreamToText(commit.stderr)
  96. if (commitExit !== 0) {
  97. console.log(` Failed to commit: ${commitStderr}`)
  98. await $`git checkout -- .`.nothrow()
  99. await $`git clean -fd`.nothrow()
  100. skipped.push({ number: pr.number, reason: `Commit failed: ${commitStderr}` })
  101. continue
  102. }
  103. console.log(` Applied successfully`)
  104. applied.push(pr.number)
  105. }
  106. console.log("\n--- Summary ---")
  107. console.log(`Applied: ${applied.length} PRs`)
  108. applied.forEach((num) => console.log(` - PR #${num}`))
  109. console.log(`Skipped: ${skipped.length} PRs`)
  110. skipped.forEach((x) => console.log(` - PR #${x.number}: ${x.reason}`))
  111. console.log("\nForce pushing beta branch...")
  112. const push = await $`git push origin beta --force --no-verify`.nothrow()
  113. if (push.exitCode !== 0) {
  114. throw new Error(`Failed to push beta branch: ${push.stderr}`)
  115. }
  116. console.log("Successfully synced beta branch")
  117. }
  118. main().catch((err) => {
  119. console.error("Error:", err)
  120. process.exit(1)
  121. })
  122. function $(strings: TemplateStringsArray, ...values: unknown[]) {
  123. const cmd = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "")
  124. return {
  125. async nothrow() {
  126. const proc = Bun.spawn(cmd.split(" "), {
  127. stdout: "pipe",
  128. stderr: "pipe",
  129. })
  130. const exitCode = await proc.exited
  131. const stdout = await new Response(proc.stdout).text()
  132. const stderr = await new Response(proc.stderr).text()
  133. return { exitCode, stdout, stderr }
  134. },
  135. }
  136. }