-
Notifications
You must be signed in to change notification settings - Fork 0
feat(auto-rebase): trigger Claude agentic rebase on merge conflict #280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1ee5f5b
75afe83
de97a13
a8b49e9
55877e1
a1db44f
836f546
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,55 @@ | ||||||||||||
| # Quality gates for the auto-rebase workflow and its supporting scripts. | ||||||||||||
| # | ||||||||||||
| # Triggered on any PR that touches: | ||||||||||||
| # - .github/workflows/auto-rebase*.yml | ||||||||||||
| # - scripts/auto-rebase.sh | ||||||||||||
| # - test/workflows/auto-rebase/** | ||||||||||||
| # | ||||||||||||
| # Gates (all must pass before merge): | ||||||||||||
| # 1. shellcheck — static analysis of scripts/auto-rebase.sh | ||||||||||||
| # 2. bats — unit test suite for conflict-handling logic | ||||||||||||
|
|
||||||||||||
| name: Auto-Rebase Tests | ||||||||||||
|
|
||||||||||||
| on: | ||||||||||||
| pull_request: | ||||||||||||
| paths: | ||||||||||||
| - '.github/workflows/auto-rebase*.yml' | ||||||||||||
| - 'scripts/auto-rebase.sh' | ||||||||||||
| - 'test/workflows/auto-rebase/**' | ||||||||||||
| push: | ||||||||||||
| branches: | ||||||||||||
| - main | ||||||||||||
| paths: | ||||||||||||
| - '.github/workflows/auto-rebase*.yml' | ||||||||||||
| - 'scripts/auto-rebase.sh' | ||||||||||||
| - 'test/workflows/auto-rebase/**' | ||||||||||||
|
|
||||||||||||
| permissions: | ||||||||||||
| contents: read | ||||||||||||
|
|
||||||||||||
| concurrency: | ||||||||||||
| group: auto-rebase-tests-${{ github.ref }}-${{ github.sha }} | ||||||||||||
| cancel-in-progress: true | ||||||||||||
|
Comment on lines
+32
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win Make concurrency key branch/PR-scoped (not SHA-scoped). Using Proposed fix concurrency:
- group: auto-rebase-tests-${{ github.ref }}-${{ github.sha }}
+ group: auto-rebase-tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||
|
|
||||||||||||
| jobs: | ||||||||||||
| test: | ||||||||||||
| name: ShellCheck and bats | ||||||||||||
| runs-on: ubuntu-latest | ||||||||||||
| timeout-minutes: 10 | ||||||||||||
| steps: | ||||||||||||
| - name: Checkout repository | ||||||||||||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||||||||||||
| with: | ||||||||||||
| fetch-depth: 1 | ||||||||||||
|
|
||||||||||||
| - name: Install bats and shellcheck | ||||||||||||
| run: | | ||||||||||||
| sudo apt-get update -qq | ||||||||||||
| sudo apt-get install -y --no-install-recommends bats shellcheck | ||||||||||||
|
|
||||||||||||
| - name: ShellCheck | ||||||||||||
| run: shellcheck --severity=warning scripts/auto-rebase.sh | ||||||||||||
|
|
||||||||||||
| - name: Run bats suite | ||||||||||||
| run: bats --print-output-on-failure test/workflows/auto-rebase/ | ||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -346,3 +346,100 @@ jobs: | |
| - If the comment requires human judgment, leave a reply explaining what you need. | ||
| 4. Check CI status. If CI fails, read the logs, fix the issues, and push again. Repeat until CI passes. | ||
| 5. When CI is green, all actionable review comments are resolved, and the PR is ready, read the CODEOWNERS file and leave a comment tagging the relevant code owners to review and merge. | ||
|
|
||
| # Automation mode: agentic rebase — triggered by a repository_dispatch event | ||
| # (type: claude-rebase) fired by auto-rebase-reusable.yml when a conflict | ||
| # cannot be resolved via the merge strategy. | ||
| # Checks out the branch, rebases onto the base branch, resolves conflicts using | ||
| # best-effort judgment, and pushes. Posts a summary comment on completion. | ||
| # | ||
| # Note: repository_dispatch is one of only two event types that GITHUB_TOKEN | ||
| # is permitted to trigger new workflow runs for. The caller (auto-rebase.yml | ||
| # in each repo) must include `repository_dispatch: types: [claude-rebase]` | ||
| # in its `on:` block for this job to receive the event. | ||
| claude-rebase: | ||
| if: >- | ||
| github.event_name == 'repository_dispatch' && | ||
| github.event.action == 'claude-rebase' | ||
|
Comment on lines
+361
to
+363
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This privileged job now trusts any Useful? React with 👍 / 👎. |
||
| concurrency: | ||
| group: claude-rebase-${{ github.event.client_payload.pr_number }} | ||
| cancel-in-progress: true | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: write | ||
| id-token: write | ||
| pull-requests: write | ||
| issues: write | ||
| actions: read | ||
| checks: read | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| fetch-depth: 0 | ||
| token: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} | ||
| - name: Run Claude Code | ||
| uses: anthropics/claude-code-action@476e359e6203e73dad705c8b322e333fabbd7416 # v1.0.119 | ||
| with: | ||
| claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} | ||
| github_token: ${{ secrets.GH_PAT_WORKFLOWS || github.token }} | ||
| # yamllint disable rule:line-length | ||
| claude_args: | | ||
| --allowedTools "Bash(git*:*),Bash(gh pr checkout:*),Bash(gh pr view:*),Bash(gh pr comment:*),Bash(gh api:*),Edit,Write" | ||
| # yamllint enable rule:line-length | ||
| # yamllint disable rule:line-length | ||
| prompt: | | ||
| PR #${{ github.event.client_payload.pr_number }} has a merge conflict that the auto-rebase workflow could not resolve using the fast-forward merge strategy. You've been assigned to resolve it via a rebase. | ||
|
|
||
| ## Your task | ||
|
|
||
| ### 1. Configure git identity and check out the PR branch | ||
| ``` | ||
| git config user.name "claude[bot]" | ||
| git config user.email "claude[bot]@users.noreply.github.com" | ||
| gh pr checkout ${{ github.event.client_payload.pr_number }} | ||
| ``` | ||
|
|
||
| ### 2. Rebase onto the base branch | ||
| ``` | ||
| git fetch origin | ||
| BASE=$(gh pr view ${{ github.event.client_payload.pr_number }} --json baseRefName -q .baseRefName) | ||
| git rebase origin/$BASE | ||
| ``` | ||
|
|
||
| ### 3. Resolve conflicts intelligently | ||
| During a rebase, conflicts are resolved commit-by-commit. For each stopped commit: | ||
|
|
||
| a. For every conflicted file in this commit, resolve it using judgment: | ||
| - **Workflow YAML / action pins**: if main already pins the same action to a newer SHA, accept main's version (`git checkout --ours <file>` — during a rebase, `--ours` is the target branch i.e. main). If the PR's pin is newer or correct, keep it (`git checkout --theirs <file>`). | ||
| - **`.gitignore`, config files**: merge both sides — preserve all unique entries from both. | ||
| - **Application code**: carefully merge both sides. If too complex to resolve safely, abort (see below). | ||
|
|
||
| b. After **all** conflicted files for this commit are resolved, stage them all at once, then continue: | ||
| ``` | ||
| git add <file1> <file2> ... # stage every resolved file for this commit | ||
| git rebase --continue # move to the next commit (run once per commit, not per file) | ||
| ``` | ||
|
|
||
| c. Repeat steps a–b for each subsequent commit that has conflicts. | ||
|
|
||
| ### 4. Push on success | ||
| ``` | ||
| git push --force-with-lease | ||
| ``` | ||
|
|
||
| ### 5. Post a summary comment on PR #${{ github.event.client_payload.pr_number }} | ||
| Summarize: | ||
| - Which files had conflicts and how each was resolved | ||
| - The rebase outcome (success or failure) | ||
| - If any commits were dropped because they were fully superseded by main (0 unique commits remaining), note that the PR may be a candidate for closure | ||
|
|
||
| ## If rebase cannot be completed safely | ||
| If conflicts involve complex application code that requires human judgment: | ||
| 1. `git rebase --abort` | ||
| 2. Post a comment on PR #${{ github.event.client_payload.pr_number }} explaining: | ||
| - Which specific files conflict | ||
| - Why the conflict needs human resolution | ||
| - The exact conflict markers so the author can find them quickly | ||
| # yamllint enable rule:line-length | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| #!/usr/bin/env bash | ||
| # auto-rebase.sh — conflict-handling helpers for the auto-rebase workflow. | ||
| # Called from .github/workflows/auto-rebase-reusable.yml. | ||
| # | ||
| # All functions read REPO from the environment and accept explicit arguments | ||
| # for the values that vary per PR, making them unit-testable. | ||
| # | ||
| # Required env: GH_TOKEN, REPO | ||
| set -euo pipefail | ||
|
|
||
| # Post the "blocked by workflows permission" comment (idempotent). | ||
| # Returns 0 whether it posts or skips. | ||
| handle_permissions_block() { | ||
| local pr_number="$1" | ||
| local base_branch="$2" | ||
|
|
||
| local sentinel="<!-- auto-rebase-blocked -->" | ||
| local already_posted | ||
| already_posted=$(gh pr view "$pr_number" --repo "$REPO" \ | ||
| --json comments --jq "[.comments[] | select(.body | contains(\"$sentinel\"))] | length") | ||
| if [[ "$already_posted" -gt 0 ]]; then | ||
| echo " Skipping — blocked comment already posted" | ||
| return 0 | ||
| fi | ||
|
|
||
| echo " Posting manual-rebase request (workflows permission missing)" | ||
| local body="$sentinel" | ||
| body+=$'\n'"**Auto-rebase blocked** — the base branch contains \`.github/workflows/\` changes" | ||
| body+=" that require the \`workflows\` permission to merge into this branch," | ||
| body+=" but the auto-rebase workflow's token does not have that permission." | ||
| body+=$'\n\n'"Please rebase this branch manually:" | ||
| body+=$'\n'"\`\`\`"$'\n'"git fetch origin" | ||
| body+=$'\n'"git rebase origin/$base_branch" | ||
| body+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`" | ||
| gh pr comment "$pr_number" --repo "$REPO" --body "$body" | ||
| } | ||
|
|
||
| # Post a SHA-keyed conflict sentinel comment and fire a repository_dispatch | ||
| # event to trigger the claude-rebase job. Idempotent: skips if a comment with | ||
| # this exact sentinel (tied to the current base-branch HEAD SHA) already exists, | ||
| # so a new merge to the base branch resets the gate and allows Claude another attempt. | ||
| handle_conflict() { | ||
| local pr_number="$1" | ||
| local head_ref="$2" | ||
| local base_branch="$3" | ||
|
|
||
| # First 8 chars of the base branch HEAD SHA — changes with every merge. | ||
| local base_sha | ||
| base_sha=$(gh api "repos/$REPO/branches/$base_branch" --jq '.commit.sha' | cut -c1-8) | ||
|
|
||
| # Sentinel is SHA-keyed so a new main commit resets idempotency for that PR. | ||
| local sentinel="<!-- auto-rebase-conflict:$base_sha -->" | ||
| local already_posted | ||
| already_posted=$(gh pr view "$pr_number" --repo "$REPO" \ | ||
| --json comments --jq "[.comments[] | select(.body | contains(\"$sentinel\"))] | length") | ||
| if [[ "$already_posted" -gt 0 ]]; then | ||
| echo " Skipping — conflict for $base_branch@$base_sha already dispatched" | ||
| return 0 | ||
| fi | ||
|
|
||
| echo " Dispatching claude-rebase for $base_branch@$base_sha and posting conflict comment" | ||
|
|
||
| # Dispatch FIRST: if this fails the sentinel comment is never posted, | ||
| # so the next auto-rebase run for this base SHA can retry. | ||
| # repository_dispatch is one of two event types GITHUB_TOKEN IS allowed to | ||
| # trigger new workflow runs for (the other being workflow_dispatch). | ||
| gh api "repos/$REPO/dispatches" \ | ||
| -X POST \ | ||
|
Comment on lines
+67
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the Useful? React with 👍 / 👎. |
||
| -f event_type=claude-rebase \ | ||
| -F "client_payload[pr_number]=$pr_number" \ | ||
| -F "client_payload[head_ref]=$head_ref" \ | ||
| -F "client_payload[base_branch]=$base_branch" | ||
|
Comment on lines
+67
to
+72
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For downstream repos that pick up the updated Useful? React with 👍 / 👎. |
||
|
|
||
| # Post sentinel only after successful dispatch so the idempotency guard | ||
| # never permanently blocks retries when a prior dispatch failed. | ||
| local body="$sentinel" | ||
| body+=$'\n'"**Auto-rebase failed — merge conflict** — this branch conflicts" | ||
| body+=" with \`$base_branch\` and cannot be updated via the merge strategy." | ||
| body+=$'\n\n'"Claude has been dispatched to attempt an agentic rebase with conflict resolution." | ||
| body+=" If Claude's rebase also fails, resolve manually:" | ||
| body+=$'\n'"\`\`\`"$'\n'"git fetch origin" | ||
| body+=$'\n'"git rebase origin/$base_branch" | ||
| body+=$'\n'"# resolve conflicts per file, then for each commit:" | ||
| body+=$'\n'"git add <resolved-files>"$'\n'"git rebase --continue"$'\n'"git push --force-with-lease"$'\n'"\`\`\`" | ||
| gh pr comment "$pr_number" --repo "$REPO" --body "$body" | ||
|
don-petry marked this conversation as resolved.
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a downstream repo invokes
auto-rebase-reusable.yml@v1, this checkout does not specifyref, so the helper script is loaded frompetry-projects/.github’s default branch rather than the same version as the reusable. I checkedactions/checkout’s README: for a different repository, omittedref“uses the default branch.” That bypasses the standards guarantee that@v1insulates adopters from changes on main, so any future incompatible edit toscripts/auto-rebase.shcan break all pinned callers immediately; checkout the same tag/SHA as the reusable or vendor the script within the versioned workflow.Useful? React with 👍 / 👎.