ci(backport): add mergiraf + Claude fallback and workflow_dispatch trigger#4375
ci(backport): add mergiraf + Claude fallback and workflow_dispatch trigger#4375sveitser wants to merge 13 commits into
Conversation
sveitser
commented
May 22, 2026
- install latest mergiraf and nightly rustfmt; korthout uses draft_commit_conflicts so conflicts produce draft PRs with markers committed
- single Claude step finalizes each created PR: mergiraf solve, manual fixups, just fmt, commit, push, gh pr ready
- new workflow_dispatch with pr_number and target_branch inputs for manually-triggered backports when the label was forgotten
- new scripts/backport-dispatch cherry-picks for the dispatch path, handling true merge commits via -m 1
…igger - install latest mergiraf and nightly rustfmt; korthout uses draft_commit_conflicts so conflicts produce draft PRs with markers committed - single Claude step finalizes each created PR: mergiraf solve, manual fixups, just fmt, commit, push, gh pr ready - new workflow_dispatch with pr_number and target_branch inputs for manually-triggered backports when the label was forgotten - new scripts/backport-dispatch cherry-picks for the dispatch path, handling true merge commits via -m 1
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. |
|
Claude finished @sveitser's task in 1m 43s —— View job Code Review: ci(backport): add mergiraf + Claude fallback and workflow_dispatch trigger
SummaryWell-structured PR that adds two useful capabilities to the backport workflow: automated conflict resolution via mergiraf + Claude, and manual Actionable items (posted as inline comments):
Non-blocking observations:
Overall this looks good — nice layered approach with mergiraf as first pass and Claude as fallback. |
| pull-requests: write # so it can create pull requests | ||
| contents: write | ||
| pull-requests: write | ||
| id-token: write |
There was a problem hiding this comment.
Is id-token: write needed here? The original workflow only had contents: write and pull-requests: write. Neither the korthout action, the dispatch script, nor the Claude action appear to require OIDC tokens. If this was added for a future need, it'd be worth commenting why — otherwise removing it would tighten the permission scope.
There was a problem hiding this comment.
| - name: Finalize backport PRs | ||
| if: | | ||
| steps.backport.outputs.created_pull_numbers != '' || | ||
| steps.dispatch_backport.outputs.created_pull_numbers != '' | ||
| uses: anthropics/claude-code-action@v1 | ||
| with: | ||
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| prompt: | | ||
| Finalize these draft backport PRs: | ||
| ${{ steps.backport.outputs.created_pull_numbers }} ${{ steps.dispatch_backport.outputs.created_pull_numbers }} | ||
|
|
||
| For each PR: check it out, resolve any conflict markers (try `mergiraf solve <file>` first), run `just fmt`, commit and push, then `gh pr ready <pr>`. | ||
|
|
||
| Keep mergiraf-resolved commits separate from your manual resolutions for a clear audit trail. | ||
| claude_args: "--model claude-opus-4-6" |
There was a problem hiding this comment.
Two suggestions for the Claude finalization step:
-
Add a
timeout-minutes— if the agent hits edge cases (many conflicted files, a hang), this step could run for a very long time and burn CI minutes. Something liketimeout-minutes: 30seems reasonable. -
Consider what happens when both outputs are empty but the
ifcondition is truthy — ifkorthout/backport-actionsucceeds but creates no PRs (e.g. label exists but target branch doesn't),created_pull_numberswould be'', so theifcorrectly skips. Looks fine. -
Minor: the prompt concatenates both outputs with a space (
${{ steps.backport.outputs.created_pull_numbers }} ${{ steps.dispatch_backport.outputs.created_pull_numbers }}). Since only one path runs per invocation, one will always be empty, yielding a leading/trailing space. Claude will handle this fine, but you could use a conditional expression or trim if you want it cleaner.
| parent_count=$(git rev-list --parents -n 1 "${sha}" | wc -w) | ||
| if (( parent_count > 2 )); then | ||
| cherry_args=(-x -m 1) | ||
| else | ||
| cherry_args=(-x) | ||
| fi |
There was a problem hiding this comment.
Nit: git rev-list --parents -n 1 outputs <sha> <parent1> [<parent2>...], so wc -w returns 1 + num_parents. The condition parent_count > 2 means "has more than 1 parent" (i.e., is a merge commit). This is correct, but the indirection through word-counting is non-obvious — a short comment would help a future reader.
Also, for a regular (non-merge) commit with 1 parent, wc -w = 2, so -m 1 is correctly skipped. Good.
| if [[ "${cherry_status}" -ne 0 ]]; then | ||
| mapfile -t conflicted < <(git diff --name-only --diff-filter=U) | ||
|
|
||
| if [[ ${#conflicted[@]} -eq 0 ]]; then | ||
| echo "cherry-pick failed (status ${cherry_status}) with no conflicted paths" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| git add -- "${conflicted[@]}" |
There was a problem hiding this comment.
When cherry-pick fails due to conflicts, non-conflicted changes are already staged but conflicted files have markers in the working tree. The git add -- "${conflicted[@]}" stages those marker files, then git commit captures everything. This is correct and mirrors what korthout's draft_commit_conflicts does.
One edge case to consider: if the cherry-pick fails for a reason other than conflicts (e.g., the commit doesn't exist, or an empty commit), git diff --name-only --diff-filter=U returns nothing, and the script correctly exits with an error on line 57. Good error handling.
| pr_url=$(gh pr create \ | ||
| --base "${target_branch}" \ | ||
| --head "${branch}" \ | ||
| --draft \ | ||
| --title "[Backport ${target_branch}] ${title}" \ | ||
| --body "Backport of #${pr_number} to \`${target_branch}\`.") |
There was a problem hiding this comment.
Consider adding --assignee "${login}" to attribute the backport PR to the original author. The login is already captured on line 29 but only used in the log message. The korthout action typically does this attribution automatically for its path.
Repo squash-merges all PRs, so the merge commit always has a single parent; -m 1 detection is dead code.
…t message - single gh pr view call instead of two - drop unused login field and defensive sha null check - inline the two-line commit body instead of heredoc
gh pr view --json doesn't expose a 'merged' field; use 'state' and check for 'MERGED' instead.
- mergiraf is invoked automatically during cherry-pick (both korthout and scripts/backport-dispatch) via a global gitattributes entry - post-step just scans each created PR branch for surviving conflict markers and only sends those to Claude - clean and mergiraf-resolved backports are marked ready without invoking Claude
- cherry-pick produces a new commit on each run (fresh committer timestamp), so a re-run of the same backport produces a different history on the same branch name; switch to --force-with-lease - if a PR for the head branch already exists (re-run case), reuse its number instead of trying to create a duplicate
Replaces the prior force-push approach with an explicit early-exit check using git ls-remote. If a previous backport attempt left a branch behind, the user must delete it (and close any open PR) before retrying. Avoids silently overwriting work.
- set git user.name/user.email globally in the job so every git operation, including any commit Claude makes, defaults to the github-actions bot identity - drop the now-redundant local git config in scripts/backport-dispatch - tell Claude in the prompt to never use --amend, --author=, --reuse-message=, git rebase, or cherry-pick --continue, since those preserve the original PR author's identity - cherry-pick -x intentionally still preserves the original author of the cherry-picked commit; that is fine
Nextest failures (1) in this run
See the step summary for flaky tests and slowest tests. |
| Mergiraf left conflict markers in these backport PRs: | ||
| ${{ steps.resolve.outputs.needs_claude }} | ||
|
|
||
| For each PR: check it out, resolve the markers, run `just fmt`, commit, push, then `gh pr ready <pr>`. |
There was a problem hiding this comment.
maybe it could also run cargo check --tests --examples? (we could even add rust-cache here)
There was a problem hiding this comment.
is there any way to avoid it using gh? I'm sort of worried about it doing weird stuff
I guess at a minimum it would be nice if there was some (guaranteed) indicator that claude had to fix some of the merge conflicts, so we need to pay special attention reviewing backport PR
There was a problem hiding this comment.
the cache is quite tricky on release branches, i think it would just take a long time to run tests and they are flaky, we run CI anyway
i will try to lock down stuff with claude
it would already be a separate commit if claude was invoked on top of the others
- Drop `gh` CLI from Claude's prompt; workflow now handles ready/label - Add `claude-resolved` label to PRs Claude touched as a reviewer signal - Run `cargo check --tests --examples` with rust-cache after Claude resolves conflicts; failure leaves PR as draft
- Claude commits to local branches only; no push, no `gh` - Verify step checks out each local branch, runs `cargo check`, then pushes. cargo check failure leaves origin unchanged so reviewer sees the original failed-cherry-pick state instead of a broken resolution