From a487f11a30981f44896ac771f7ade87fba9d6092 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Tue, 24 Feb 2026 23:17:31 -0500 Subject: [PATCH] ci: auto-resolve merge conflicts in beta sync using opencode When merging PRs into the beta branch, the sync script now attempts to automatically resolve merge conflicts using opencode before failing. This reduces manual intervention needed for beta releases when multiple PRs have overlapping changes. --- .github/workflows/beta.yml | 4 ++ script/beta.ts | 75 +++++++++++++++++++++++++++++++------- 2 files changed, 66 insertions(+), 13 deletions(-) diff --git a/.github/workflows/beta.yml b/.github/workflows/beta.yml index 20d2bc18d..a7106667b 100644 --- a/.github/workflows/beta.yml +++ b/.github/workflows/beta.yml @@ -27,7 +27,11 @@ jobs: opencode-app-id: ${{ vars.OPENCODE_APP_ID }} opencode-app-secret: ${{ secrets.OPENCODE_APP_SECRET }} + - name: Install OpenCode + run: bun i -g opencode-ai + - name: Sync beta branch env: GH_TOKEN: ${{ steps.setup-git-committer.outputs.token }} + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} run: bun script/beta.ts diff --git a/script/beta.ts b/script/beta.ts index a5fb027e6..fbb121409 100755 --- a/script/beta.ts +++ b/script/beta.ts @@ -30,6 +30,52 @@ Please resolve this issue to include this PR in the next beta release.` } } +async function conflicts() { + const out = await $`git diff --name-only --diff-filter=U`.text().catch(() => "") + return out + .split("\n") + .map((x) => x.trim()) + .filter(Boolean) +} + +async function cleanup() { + try { + await $`git merge --abort` + } catch {} + try { + await $`git checkout -- .` + } catch {} + try { + await $`git clean -fd` + } catch {} +} + +async function fix(pr: PR, files: string[]) { + console.log(` Trying to auto-resolve ${files.length} conflict(s) with opencode...`) + const prompt = [ + `Resolve the current git merge conflicts while merging PR #${pr.number} into the beta branch.`, + `Only touch these files: ${files.join(", ")}.`, + "Keep the merge in progress, do not abort the merge, and do not create a commit.", + "When done, leave the working tree with no unmerged files.", + ].join("\n") + + try { + await $`opencode run ${prompt}` + } catch (err) { + console.log(` opencode failed: ${err}`) + return false + } + + const left = await conflicts() + if (left.length > 0) { + console.log(` Conflicts remain: ${left.join(", ")}`) + return false + } + + console.log(" Conflicts resolved with opencode") + return true +} + async function main() { console.log("Fetching open PRs with beta label...") @@ -69,19 +115,22 @@ async function main() { try { await $`git merge --no-commit --no-ff pr/${pr.number}` } catch { - console.log(" Failed to merge (conflicts)") - try { - await $`git merge --abort` - } catch {} - try { - await $`git checkout -- .` - } catch {} - try { - await $`git clean -fd` - } catch {} - failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) - await commentOnPR(pr.number, "Merge conflicts with dev branch") - continue + const files = await conflicts() + if (files.length > 0) { + console.log(" Failed to merge (conflicts)") + if (!(await fix(pr, files))) { + await cleanup() + failed.push({ number: pr.number, title: pr.title, reason: "Merge conflicts" }) + await commentOnPR(pr.number, "Merge conflicts with dev branch") + continue + } + } else { + console.log(" Failed to merge") + await cleanup() + failed.push({ number: pr.number, title: pr.title, reason: "Merge failed" }) + await commentOnPR(pr.number, "Merge failed") + continue + } } try {