Skip to content

Commit 80a92b1

Browse files
committed
feat: add beta branch sync workflow for contributor PRs
1 parent 5716d1b commit 80a92b1

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

.github/workflows/beta-sync.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: beta-sync
2+
3+
on:
4+
push:
5+
branches: [dev]
6+
pull_request_target:
7+
types: [opened, synchronize, labeled, unlabeled]
8+
branches: [dev]
9+
10+
jobs:
11+
sync-beta:
12+
if: |
13+
github.event_name == 'push' ||
14+
(github.event_name == 'pull_request_target' &&
15+
contains(github.event.pull_request.labels.*.name, 'contributor'))
16+
runs-on: blacksmith-4vcpu-ubuntu-2404
17+
permissions:
18+
contents: write
19+
pull-requests: read
20+
steps:
21+
- name: Checkout repository
22+
uses: actions/checkout@v4
23+
with:
24+
fetch-depth: 0
25+
token: ${{ secrets.GITHUB_TOKEN }}
26+
27+
- name: Setup Bun
28+
uses: ./.github/actions/setup-bun
29+
30+
- name: Configure Git
31+
run: |
32+
git config user.name "github-actions[bot]"
33+
git config user.email "github-actions[bot]@users.noreply.github.com"
34+
35+
- name: Sync beta branch
36+
env:
37+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38+
run: bun script/beta-sync.ts

script/beta-sync.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#!/usr/bin/env bun
2+
3+
interface PR {
4+
number: number
5+
headRefName: string
6+
headRefOid: string
7+
createdAt: string
8+
isDraft: boolean
9+
title: string
10+
}
11+
12+
async function main() {
13+
const token = process.env.GITHUB_TOKEN
14+
if (!token) throw new Error("GITHUB_TOKEN not set")
15+
16+
const repo = process.env.GITHUB_REPOSITORY
17+
if (!repo) throw new Error("GITHUB_REPOSITORY not set")
18+
19+
const [owner, repoName] = repo.split("/")
20+
21+
console.log("Fetching open contributor PRs...")
22+
23+
const prsQuery = `
24+
query($owner: String!, $repo: String!, $labels: [String!]) {
25+
repository(owner: $owner, name: $repo) {
26+
pullRequests(
27+
states: OPEN,
28+
labels: $labels,
29+
first: 100,
30+
orderBy: {field: CREATED_AT, direction: ASC}
31+
) {
32+
nodes {
33+
number
34+
headRefName
35+
headRefOid
36+
createdAt
37+
isDraft
38+
title
39+
}
40+
}
41+
}
42+
}
43+
`
44+
45+
const queryResult = await fetch("https://api.github.com/graphql", {
46+
method: "POST",
47+
headers: {
48+
Authorization: `Bearer ${token}`,
49+
"Content-Type": "application/json",
50+
},
51+
body: JSON.stringify({
52+
query: prsQuery,
53+
variables: { owner, repo: repoName, labels: ["contributor"] },
54+
}),
55+
}).then((x) => x.json())
56+
57+
if (queryResult.errors) {
58+
console.error("GraphQL errors:", queryResult.errors)
59+
throw new Error("Failed to fetch PRs")
60+
}
61+
62+
const allPRs: PR[] = queryResult.data.repository.pullRequests.nodes
63+
64+
const prs = allPRs.filter((pr) => !pr.isDraft)
65+
66+
console.log(`Found ${prs.length} open non-draft contributor PRs`)
67+
68+
console.log("Fetching latest dev branch...")
69+
const fetchDev = await $`git fetch origin dev`.nothrow()
70+
if (fetchDev.exitCode !== 0) {
71+
throw new Error(`Failed to fetch dev branch: ${fetchDev.stderr}`)
72+
}
73+
74+
console.log("Checking out beta branch...")
75+
const checkoutBeta = await $`git checkout -B beta origin/dev`.nothrow()
76+
if (checkoutBeta.exitCode !== 0) {
77+
throw new Error(`Failed to checkout beta branch: ${checkoutBeta.stderr}`)
78+
}
79+
80+
const applied: number[] = []
81+
const skipped: Array<{ number: number; reason: string }> = []
82+
83+
for (const pr of prs) {
84+
console.log(`\nProcessing PR #${pr.number}: ${pr.title}`)
85+
86+
const fetchPR = await $`git fetch origin pull/${pr.number}/head:pr-${pr.number}`.nothrow()
87+
if (fetchPR.exitCode !== 0) {
88+
console.log(` Failed to fetch PR #${pr.number}, skipping`)
89+
skipped.push({ number: pr.number, reason: "Failed to fetch" })
90+
continue
91+
}
92+
93+
const cherryPick = await $`git cherry-pick ${pr.headRefOid} --no-commit`.nothrow()
94+
if (cherryPick.exitCode !== 0) {
95+
console.log(` PR #${pr.number} introduces conflicts, skipping`)
96+
await $`git cherry-pick --abort`.nothrow()
97+
await $`git checkout -- .`.nothrow()
98+
await $`git clean -fd`.nothrow()
99+
skipped.push({ number: pr.number, reason: "Merge conflict" })
100+
continue
101+
}
102+
103+
const commit = await $`git commit -m "Apply PR #${pr.number}: ${pr.title}"`.nothrow()
104+
if (commit.exitCode !== 0) {
105+
console.log(` Failed to commit PR #${pr.number}, skipping`)
106+
await $`git reset --hard HEAD`.nothrow()
107+
skipped.push({ number: pr.number, reason: "Failed to commit" })
108+
continue
109+
}
110+
111+
console.log(` Successfully applied PR #${pr.number}`)
112+
applied.push(pr.number)
113+
}
114+
115+
console.log("\n--- Summary ---")
116+
console.log(`Applied: ${applied.length} PRs`)
117+
applied.forEach((num) => console.log(` - PR #${num}`))
118+
console.log(`Skipped: ${skipped.length} PRs`)
119+
skipped.forEach((x) => console.log(` - PR #${x.number}: ${x.reason}`))
120+
121+
console.log("\nForce pushing beta branch...")
122+
const push = await $`git push origin beta --force`.nothrow()
123+
if (push.exitCode !== 0) {
124+
throw new Error(`Failed to push beta branch: ${push.stderr}`)
125+
}
126+
127+
console.log("Successfully synced beta branch")
128+
}
129+
130+
main().catch((err) => {
131+
console.error("Error:", err)
132+
process.exit(1)
133+
})
134+
135+
function $(strings: TemplateStringsArray, ...values: unknown[]) {
136+
const cmd = strings.reduce((acc, str, i) => acc + str + (values[i] ?? ""), "")
137+
return {
138+
async nothrow() {
139+
const proc = Bun.spawn(cmd.split(" "), {
140+
stdout: "pipe",
141+
stderr: "pipe",
142+
})
143+
const exitCode = await proc.exited
144+
const stdout = await new Response(proc.stdout).text()
145+
const stderr = await new Response(proc.stderr).text()
146+
return { exitCode, stdout, stderr }
147+
},
148+
}
149+
}

0 commit comments

Comments
 (0)