From a6f425b5163e4424d57a802d7fc5498dd883c9ac Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 10 Apr 2026 16:44:21 -0700 Subject: [PATCH 1/6] Add AI-powered conflict resolution to backport workflow Signed-off-by: Nathan Rajlich --- .github/workflows/backport.yml | 101 ++++++++++++++++++++++++++++++--- AGENTS.md | 2 +- 2 files changed, 95 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 72d48fb4aa..d0341d6c54 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -36,12 +36,86 @@ jobs: git config user.name "$(git log -1 --format='%an' "$MERGE_SHA")" git config user.email "$(git log -1 --format='%ae' "$MERGE_SHA")" git checkout stable - git cherry-pick "$MERGE_SHA" --no-edit - echo "cherry_pick_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - git push origin stable + if git cherry-pick "$MERGE_SHA" --no-edit; then + echo "status=clean" >> "$GITHUB_OUTPUT" + echo "cherry_pick_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + else + echo "status=conflict" >> "$GITHUB_OUTPUT" + fi - - name: Comment on success - if: success() + - name: Push clean backport + if: steps.cherry-pick.outputs.status == 'clean' + run: git push origin stable + + - name: Install opencode + if: steps.cherry-pick.outputs.status == 'conflict' + run: | + curl -fsSL https://opencode.ai/install | bash + echo "$HOME/.opencode/bin" >> "$GITHUB_PATH" + + - name: Configure opencode + if: steps.cherry-pick.outputs.status == 'conflict' + env: + AI_GATEWAY_TOKEN: ${{ secrets.AI_GATEWAY_TOKEN }} + run: | + mkdir -p ~/.local/share/opencode + echo '{"vercel":{"type":"api","key":"'"$AI_GATEWAY_TOKEN"'"}}' > ~/.local/share/opencode/auth.json + + - name: Resolve conflicts with opencode + if: steps.cherry-pick.outputs.status == 'conflict' + id: ai-resolve + env: + OPENCODE_PERMISSION: '{"allow":["*"]}' + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" + COMMIT_MSG=$(git log -1 --format='%B' "$MERGE_SHA") + + opencode run --model vercel/anthropic/claude-opus-4-20250514 \ + "The working tree has git merge conflicts from a failed cherry-pick. + A commit is being cherry-picked from the main branch to the stable branch. + + PR title: $PR_TITLE + Commit message: $COMMIT_MSG + + Resolve all merge conflicts in the working tree. The content between + <<<<<<< HEAD and ======= is the current stable branch. The content between + ======= and >>>>>>> is the incoming change from main. + + After resolving each file, run git add on it to mark it as resolved. + Do NOT run git cherry-pick --continue or git commit." + + # Verify all conflicts are resolved + REMAINING=$(git diff --name-only --diff-filter=U || true) + if [ -z "$REMAINING" ]; then + GIT_EDITOR=true git cherry-pick --continue + echo "resolved=true" >> "$GITHUB_OUTPUT" + echo "cherry_pick_sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + fi + + - name: Create backport PR with AI-resolved conflicts + if: steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved == 'true' + id: backport-pr + env: + GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + BRANCH="backport/pr-${{ github.event.pull_request.number }}-to-stable" + git checkout -b "$BRANCH" + git push origin "$BRANCH" + PR_URL=$(gh pr create \ + --base stable \ + --head "$BRANCH" \ + --title "Backport #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}" \ + --body "$(cat <<'BODY' + Automated backport of #${{ github.event.pull_request.number }} to `stable`. + + Merge conflicts were resolved by AI ([opencode](https://opencode.ai) with Claude Opus). **Please review the conflict resolution before merging.** + BODY + )") + echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" + + - name: Comment on clean backport + if: steps.cherry-pick.outputs.status == 'clean' uses: actions/github-script@v7 with: github-token: ${{ steps.app-token.outputs.token }} @@ -55,8 +129,21 @@ jobs: body: `Backported to \`stable\` (${originalSha} -> ${cherryPickSha}).` }); + - name: Comment on AI-resolved backport + if: steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved == 'true' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: 'Cherry-pick to `stable` had conflicts that were resolved by AI. Please review the backport PR: ${{ steps.backport-pr.outputs.pr_url }}' + }); + - name: Comment on failure - if: failure() + if: steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved != 'true' uses: actions/github-script@v7 with: github-token: ${{ steps.app-token.outputs.token }} @@ -67,7 +154,7 @@ jobs: repo: context.repo.repo, issue_number: context.payload.pull_request.number, body: [ - '**Backport to `stable` failed** — the cherry-pick could not be applied cleanly.', + '**Backport to `stable` failed** — the cherry-pick had conflicts that could not be resolved automatically.', '', 'To resolve manually:', '```bash', diff --git a/AGENTS.md b/AGENTS.md index f325dc5d8c..592721821e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -190,7 +190,7 @@ Both branches trigger the release workflow (`.github/workflows/release.yml`) on To backport a change from `main` to `stable`, add the `backport-stable` label to the PR on `main`. A GitHub Action (`.github/workflows/backport.yml`) will automatically cherry-pick the squashed commit to `stable`. The label can be added before or after merging — the action triggers on both merge and label events. The changeset file is included in the cherry-pick, so the correct semver bump type is preserved on `stable`. -If the cherry-pick fails due to conflicts, the action will comment on the original PR with instructions for manual resolution. +If the cherry-pick fails due to conflicts, the action will attempt to resolve them automatically using [opencode](https://opencode.ai) (AI-powered conflict resolution). If successful, it creates a PR targeting `stable` for human review instead of pushing directly. If the AI cannot resolve the conflicts, the action will comment on the original PR with instructions for manual resolution. ### Pre-release Lifecycle From c3e919f7bf95d066240d2771c02eff360dd1c2d5 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 10 Apr 2026 16:47:46 -0700 Subject: [PATCH 2/6] Use anthropic/claude-opus-4.6 model identifier Signed-off-by: Nathan Rajlich --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index d0341d6c54..6dd7874acd 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -71,7 +71,7 @@ jobs: MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" COMMIT_MSG=$(git log -1 --format='%B' "$MERGE_SHA") - opencode run --model vercel/anthropic/claude-opus-4-20250514 \ + opencode run --model vercel/anthropic/claude-opus-4.6 \ "The working tree has git merge conflicts from a failed cherry-pick. A commit is being cherry-picked from the main branch to the stable branch. From 07b7f7f79ab6615b7b4503df5ebec7c83ffaddf9 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 10 Apr 2026 16:54:07 -0700 Subject: [PATCH 3/6] Address review feedback on AI conflict resolution Signed-off-by: Nathan Rajlich --- .github/workflows/backport.yml | 47 ++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 6dd7874acd..6976b19b70 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -47,11 +47,15 @@ jobs: if: steps.cherry-pick.outputs.status == 'clean' run: git push origin stable + - name: Setup Node.js + if: steps.cherry-pick.outputs.status == 'conflict' + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + - name: Install opencode if: steps.cherry-pick.outputs.status == 'conflict' - run: | - curl -fsSL https://opencode.ai/install | bash - echo "$HOME/.opencode/bin" >> "$GITHUB_PATH" + run: npm install -g opencode-ai - name: Configure opencode if: steps.cherry-pick.outputs.status == 'conflict' @@ -59,20 +63,21 @@ jobs: AI_GATEWAY_TOKEN: ${{ secrets.AI_GATEWAY_TOKEN }} run: | mkdir -p ~/.local/share/opencode - echo '{"vercel":{"type":"api","key":"'"$AI_GATEWAY_TOKEN"'"}}' > ~/.local/share/opencode/auth.json + jq -n --arg key "$AI_GATEWAY_TOKEN" '{vercel:{type:"api",key:$key}}' > ~/.local/share/opencode/auth.json - name: Resolve conflicts with opencode if: steps.cherry-pick.outputs.status == 'conflict' id: ai-resolve + continue-on-error: true env: OPENCODE_PERMISSION: '{"allow":["*"]}' + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + PR_TITLE: ${{ github.event.pull_request.title }} run: | - PR_TITLE="${{ github.event.pull_request.title }}" - MERGE_SHA="${{ github.event.pull_request.merge_commit_sha }}" COMMIT_MSG=$(git log -1 --format='%B' "$MERGE_SHA") - opencode run --model vercel/anthropic/claude-opus-4.6 \ - "The working tree has git merge conflicts from a failed cherry-pick. + cat > /tmp/backport-prompt.txt <>>>>>> is the incoming change from main. After resolving each file, run git add on it to mark it as resolved. - Do NOT run git cherry-pick --continue or git commit." + Do NOT run git cherry-pick --continue or git commit. + PROMPT + + opencode run --model vercel/anthropic/claude-opus-4.6 "$(cat /tmp/backport-prompt.txt)" # Verify all conflicts are resolved REMAINING=$(git diff --name-only --diff-filter=U || true) @@ -100,18 +108,25 @@ jobs: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} run: | BRANCH="backport/pr-${{ github.event.pull_request.number }}-to-stable" - git checkout -b "$BRANCH" - git push origin "$BRANCH" - PR_URL=$(gh pr create \ - --base stable \ - --head "$BRANCH" \ - --title "Backport #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}" \ - --body "$(cat <<'BODY' + git checkout -B "$BRANCH" + git push --force-with-lease origin "$BRANCH" + + EXISTING_PR=$(gh pr list --state open --base stable --head "$BRANCH" --json url --jq '.[0].url') + + if [ -n "$EXISTING_PR" ]; then + PR_URL="$EXISTING_PR" + else + PR_URL=$(gh pr create \ + --base stable \ + --head "$BRANCH" \ + --title "Backport #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}" \ + --body "$(cat <<'BODY' Automated backport of #${{ github.event.pull_request.number }} to `stable`. Merge conflicts were resolved by AI ([opencode](https://opencode.ai) with Claude Opus). **Please review the conflict resolution before merging.** BODY )") + fi echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT" - name: Comment on clean backport From 43d959ff623a2c5a62bd7706ea77bc37b4193082 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 10 Apr 2026 18:14:05 -0700 Subject: [PATCH 4/6] Update .github/workflows/backport.yml Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Signed-off-by: Nathan Rajlich --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 6976b19b70..605a9fa4f2 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -158,7 +158,7 @@ jobs: }); - name: Comment on failure - if: steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved != 'true' + if: always() && steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved != 'true' uses: actions/github-script@v7 with: github-token: ${{ steps.app-token.outputs.token }} From 2593999fceab006c44d42d6af06d3a4d7670f76e Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Fri, 10 Apr 2026 18:15:37 -0700 Subject: [PATCH 5/6] Fix script injection in gh pr create --title Signed-off-by: Nathan Rajlich --- .github/workflows/backport.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 605a9fa4f2..f9942011b5 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -106,8 +106,10 @@ jobs: id: backport-pr env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} run: | - BRANCH="backport/pr-${{ github.event.pull_request.number }}-to-stable" + BRANCH="backport/pr-${PR_NUMBER}-to-stable" git checkout -B "$BRANCH" git push --force-with-lease origin "$BRANCH" @@ -119,7 +121,7 @@ jobs: PR_URL=$(gh pr create \ --base stable \ --head "$BRANCH" \ - --title "Backport #${{ github.event.pull_request.number }}: ${{ github.event.pull_request.title }}" \ + --title "Backport #${PR_NUMBER}: $PR_TITLE" \ --body "$(cat <<'BODY' Automated backport of #${{ github.event.pull_request.number }} to `stable`. From 53d54289ee37cfaa977d650591533e175a4cbe91 Mon Sep 17 00:00:00 2001 From: Nathan Rajlich Date: Mon, 13 Apr 2026 12:24:16 -0700 Subject: [PATCH 6/6] Add catch-all failure comment and pin opencode-ai version Signed-off-by: Nathan Rajlich --- .github/workflows/backport.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index f9942011b5..d13a237da0 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -55,7 +55,7 @@ jobs: - name: Install opencode if: steps.cherry-pick.outputs.status == 'conflict' - run: npm install -g opencode-ai + run: npm install -g opencode-ai@1.4.3 - name: Configure opencode if: steps.cherry-pick.outputs.status == 'conflict' @@ -159,7 +159,7 @@ jobs: body: 'Cherry-pick to `stable` had conflicts that were resolved by AI. Please review the backport PR: ${{ steps.backport-pr.outputs.pr_url }}' }); - - name: Comment on failure + - name: Comment on conflict failure if: always() && steps.cherry-pick.outputs.status == 'conflict' && steps.ai-resolve.outputs.resolved != 'true' uses: actions/github-script@v7 with: @@ -184,3 +184,17 @@ jobs: '```' ].join('\n') }); + + - name: Comment on unexpected failure + if: always() && steps.cherry-pick.outputs.status != 'clean' && steps.cherry-pick.outputs.status != 'conflict' + uses: actions/github-script@v7 + with: + github-token: ${{ steps.app-token.outputs.token }} + script: | + const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.payload.pull_request.number, + body: `**Backport to \`stable\` failed** — unexpected error before the cherry-pick could be attempted. [See workflow run](${runUrl}).` + });