Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 19 additions & 46 deletions .github/workflows/auto-rebase-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,15 @@
# Failure modes handled gracefully:
# - without `workflows` permission (403): posts an idempotent comment asking
# the author to rebase manually (sentinel: <!-- auto-rebase-blocked -->)
# - merge conflict (422): posts an idempotent comment asking the author to
# resolve conflicts (sentinel: <!-- auto-rebase-conflict -->)
# - merge conflict (422): posts a SHA-keyed sentinel comment and fires a
# repository_dispatch event (type: claude-rebase). The sentinel is keyed to
# the base-branch HEAD SHA, so each new merge to the base branch resets the
# gate and allows Claude another attempt on the next auto-rebase run.
# Note: repository_dispatch is one of only two event types that GITHUB_TOKEN
# is permitted to trigger workflow runs for (the other being workflow_dispatch).
#
# No secrets required — uses github.token only. No auto-merge logic.
# Conflict-handling logic lives in scripts/auto-rebase.sh for testability.
name: Auto-rebase non-Dependabot PRs (Reusable)

on:
Expand All @@ -35,14 +40,23 @@ jobs:
auto-rebase:
runs-on: ubuntu-latest
permissions:
contents: write # needed for update-branch (may touch .github/workflows/)
contents: write # needed for update-branch and repository_dispatch
pull-requests: write # needed to post comments on PRs
steps:
- name: Checkout petry-projects/.github (for scripts)
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: petry-projects/.github
fetch-depth: 1
Comment on lines +46 to +50

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pin the helper checkout to the reusable ref

When a downstream repo invokes auto-rebase-reusable.yml@v1, this checkout does not specify ref, so the helper script is loaded from petry-projects/.github’s default branch rather than the same version as the reusable. I checked actions/checkout’s README: for a different repository, omitted ref “uses the default branch.” That bypasses the standards guarantee that @v1 insulates adopters from changes on main, so any future incompatible edit to scripts/auto-rebase.sh can break all pinned callers immediately; checkout the same tag/SHA as the reusable or vendor the script within the versioned workflow.

Useful? React with 👍 / 👎.

- name: Update behind non-Dependabot PRs
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
# Always sourced from petry-projects/.github regardless of caller repo.
# shellcheck source=scripts/auto-rebase.sh
source scripts/auto-rebase.sh
Comment thread
don-petry marked this conversation as resolved.

# Find open non-Dependabot PRs from the same repo (exclude forks)
PRS=$(gh api "repos/$REPO/pulls?state=open&per_page=100" \
--jq '.[] | select(.user.login != "dependabot[bot]") | select(.head.repo != null) | select(.head.repo.full_name == .base.repo.full_name) | "\(.number) \(.head.ref)"')
Expand Down Expand Up @@ -76,50 +90,9 @@ jobs:
echo " Update failed: $UPDATE_OUTPUT"

if echo "$UPDATE_OUTPUT" | grep -qF "without \`workflows\` permission"; then
# GitHub blocks update-branch when merging the base would add
# .github/workflows/ changes and the token lacks the 'workflows'
# permission. Ask the author to rebase manually.
#
# Idempotent: skip if sentinel comment already exists.
SENTINEL="<!-- auto-rebase-blocked -->"
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"
else
echo " Posting manual-rebase request (workflows permission missing)"
BLOCKED_BODY="<!-- auto-rebase-blocked -->"
BLOCKED_BODY+=$'\n'"**Auto-rebase blocked** — the base branch contains \`.github/workflows/\` changes"
BLOCKED_BODY+=" that require the \`workflows\` permission to merge into this branch,"
BLOCKED_BODY+=" but the auto-rebase workflow's token does not have that permission."
BLOCKED_BODY+=$'\n\n'"Please rebase this branch manually:"
BLOCKED_BODY+=$'\n'"\`\`\`"$'\n'"git fetch origin"
BLOCKED_BODY+=$'\n'"git rebase origin/$BASE_BRANCH"
BLOCKED_BODY+=$'\n'"git push --force-with-lease"$'\n'"\`\`\`"
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$BLOCKED_BODY"
fi
handle_permissions_block "$PR_NUMBER" "$BASE_BRANCH"
elif echo "$UPDATE_OUTPUT" | grep -qi "merge conflict"; then
# Merge conflict — ask the author to resolve it.
# gh api surfaces the GitHub API error JSON: {"message":"merge conflict",...}
#
# Idempotent: skip if sentinel comment already exists.
SENTINEL="<!-- auto-rebase-conflict -->"
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 comment already posted"
else
echo " Posting conflict resolution request"
CONFLICT_BODY="<!-- auto-rebase-conflict -->"
CONFLICT_BODY+=$'\n'"**Auto-rebase failed — merge conflict** — this branch has conflicts"
CONFLICT_BODY+=" with \`$BASE_BRANCH\` that must be resolved manually."
CONFLICT_BODY+=$'\n\n'"Please resolve the conflicts and push:"
CONFLICT_BODY+=$'\n'"\`\`\`"$'\n'"git fetch origin"
CONFLICT_BODY+=$'\n'"git merge origin/$BASE_BRANCH"
CONFLICT_BODY+=$'\n'"# resolve conflicts, then:"
CONFLICT_BODY+=$'\n'"git add ."$'\n'"git commit"$'\n'"git push"$'\n'"\`\`\`"
gh pr comment "$PR_NUMBER" --repo "$REPO" --body "$CONFLICT_BODY"
fi
handle_conflict "$PR_NUMBER" "$HEAD_REF" "$BASE_BRANCH"
else
echo " Warning: failed to update PR #$PR_NUMBER — $UPDATE_OUTPUT"
fi
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/auto-rebase-tests.yml
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 ${{ github.sha }} in the group makes each run unique, so cancel-in-progress: true won’t cancel older runs after new commits. Use a stable key per ref/PR instead.

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
group: auto-rebase-tests-${{ github.ref }}-${{ github.sha }}
cancel-in-progress: true
concurrency:
group: auto-rebase-tests-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/auto-rebase-tests.yml around lines 32 - 33, The
concurrency group currently uses the commit SHA (group: auto-rebase-tests-${{
github.ref }}-${{ github.sha }}), which makes each run unique and prevents
cancel-in-progress from working; update the group key to be stable per branch/PR
by removing `${{ github.sha }}` and using a ref/PR-scoped value instead (for
example use `${{ github.ref }}` or for pull requests `${{
github.event.pull_request.number }}`/`${{ github.head_ref }}`) so that
cancel-in-progress: true will actually cancel previous runs for the same
branch/PR.


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/
97 changes: 97 additions & 0 deletions .github/workflows/claude-code-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate dispatch payload before running privileged rebase

This privileged job now trusts any repository_dispatch payload with action claude-rebase, but it never re-checks that the referenced PR is the same-repo, non-Dependabot PR that auto-rebase is supposed to handle. If another automation or a maintainer token dispatches this event with a fork PR number, the job still gets write permissions and inherited secrets and then instructs Claude to gh pr checkout and rebase that PR; add the same head-repo/bot gate used by the auto-rebase producer before invoking Claude.

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
86 changes: 86 additions & 0 deletions scripts/auto-rebase.sh
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue after a failed Claude dispatch

If the repository_dispatch request fails for one conflicted PR due to a transient API/token problem, this unguarded gh api runs under the sourced script's set -e, so the entire auto-rebase step exits instead of moving on to the remaining PRs. That regresses the workflow's per-PR graceful handling: a single dispatch failure can leave later behind branches unprocessed until another push or manual run.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify the Claude listener before marking dispatch handled

For downstream repos that pick up the updated auto-rebase-reusable.yml@v1 but have not yet copied the updated local claude.yml stub with on.repository_dispatch, this API call can succeed while no Claude workflow is actually listening on the repo’s default branch; GitHub documents repository_dispatch runs only when the workflow file exists on the default branch. Because the sentinel comment is posted immediately after this and future runs skip the same base SHA, those conflicted PRs are left with “Claude has been dispatched” but no rebase attempt. Add a compatibility check/fallback or avoid writing the sentinel until a run is confirmed.

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"
Comment thread
don-petry marked this conversation as resolved.
}
Loading