beta.ts 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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: ${diffResult.stderr}`)
  56. console.log(` Compare: ${compare}`)
  57. skipped.push({ number: pr.number, reason: `Failed to get diff: ${diffResult.stderr}` })
  58. continue
  59. }
  60. if (!diffResult.stdout.trim()) {
  61. console.log(` No changes, skipping`)
  62. skipped.push({ number: pr.number, reason: "No changes" })
  63. continue
  64. }
  65. // Try to apply the diff
  66. console.log(` Applying...`)
  67. const apply = await Bun.spawn(["git", "apply", "--3way"], {
  68. stdin: new TextEncoder().encode(diffResult.stdout),
  69. stdout: "pipe",
  70. stderr: "pipe",
  71. })
  72. const applyExit = await apply.exited
  73. const applyStderr = await Bun.readableStreamToText(apply.stderr)
  74. if (applyExit !== 0) {
  75. console.log(` Failed to apply (conflicts)`)
  76. await $`git checkout -- .`.nothrow()
  77. await $`git clean -fd`.nothrow()
  78. skipped.push({ number: pr.number, reason: "Has conflicts" })
  79. continue
  80. }
  81. // Stage and commit
  82. const add = await $`git add -A`.nothrow()
  83. if (add.exitCode !== 0) {
  84. console.log(` Failed to stage`)
  85. await $`git checkout -- .`.nothrow()
  86. await $`git clean -fd`.nothrow()
  87. skipped.push({ number: pr.number, reason: "Failed to stage" })
  88. continue
  89. }
  90. const commitMsg = `Apply PR #${pr.number}: ${pr.title}`
  91. const commit = await Bun.spawn(["git", "commit", "-m", commitMsg], {
  92. stdout: "pipe",
  93. stderr: "pipe",
  94. })
  95. const commitExit = await commit.exited
  96. const commitStderr = await Bun.readableStreamToText(commit.stderr)
  97. if (commitExit !== 0) {
  98. console.log(` Failed to commit: ${commitStderr}`)
  99. await $`git checkout -- .`.nothrow()
  100. await $`git clean -fd`.nothrow()
  101. skipped.push({ number: pr.number, reason: `Commit failed: ${commitStderr}` })
  102. continue
  103. }
  104. console.log(` Applied successfully`)
  105. applied.push(pr.number)
  106. }
  107. console.log("\n--- Summary ---")
  108. console.log(`Applied: ${applied.length} PRs`)
  109. applied.forEach((num) => console.log(` - PR #${num}`))
  110. console.log(`Skipped: ${skipped.length} PRs`)
  111. skipped.forEach((x) => console.log(` - PR #${x.number}: ${x.reason}`))
  112. console.log("\nForce pushing beta branch...")
  113. const push = await $`git push origin beta --force --no-verify`.nothrow()
  114. if (push.exitCode !== 0) {
  115. throw new Error(`Failed to push beta branch: ${push.stderr}`)
  116. }
  117. console.log("Successfully synced beta branch")
  118. }
  119. main().catch((err) => {
  120. console.error("Error:", err)
  121. process.exit(1)
  122. })
  123. function $(strings: TemplateStringsArray, ...values: unknown[]) {
  124. const cmd = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "")
  125. return {
  126. async nothrow() {
  127. const proc = Bun.spawn(cmd.split(" "), {
  128. stdout: "pipe",
  129. stderr: "pipe",
  130. })
  131. const exitCode = await proc.exited
  132. const stdout = await new Response(proc.stdout).text()
  133. const stderr = await new Response(proc.stderr).text()
  134. return { exitCode, stdout, stderr }
  135. },
  136. }
  137. }