Skip to content

Commit fe4f818

Browse files
Add PR workflow to detect missing rebase rules and auto-fix via Claude
- Add check-rebase-rules.yml: runs on PR open/sync, detects files modified in code/ without rebase rules, posts a comment listing them - Add add-rebase-rules.yml: triggered by /add-rebase-rules slash command, runs dw-claude-runner to create missing rules automatically - Add .github/scripts/check-unprotected-changes.sh: detection script that compares PR changes against rebase rules coverage - Update CLAUDE.md routing table with add-rebase-rules entry Made-with: Cursor
1 parent fed2326 commit fe4f818

4 files changed

Lines changed: 422 additions & 6 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#!/bin/bash
2+
# This file was generated using AI assistance (Cursor AI) and reviewed by the maintainers.
3+
#
4+
# Detect files under code/ changed since the last rebase that have no rebase
5+
# rule. Such modifications would be silently lost on the next upstream rebase.
6+
#
7+
# How it works:
8+
# 1. Finds the last rebase commit (message starts with "Rebase against the upstream")
9+
# 2. Lists files in code/ changed since that commit
10+
# 3. Filters out Che-only additions (che-* extensions, che/ subdirs) and lockfiles
11+
# 4. For remaining files, checks whether a rebase rule exists in:
12+
# - resolve_conflicts() elif chain in rebase.sh
13+
# - .rebase/replace/ (from→by rules)
14+
# - .rebase/add/ (JSON fragments merged in)
15+
# - .rebase/override/ (JSON overrides)
16+
# 5. Reports any uncovered files
17+
#
18+
# Usage:
19+
# bash check-unprotected-changes.sh # default: changes since last rebase
20+
# bash check-unprotected-changes.sh <base>..<head> # explicit commit range
21+
# bash check-unprotected-changes.sh --verbose # show covered files too
22+
# bash check-unprotected-changes.sh --pr-comment # output as GitHub PR comment (markdown)
23+
# bash check-unprotected-changes.sh --list # output bare file paths only
24+
#
25+
# Exit codes:
26+
# 0 — All modifications are covered by rebase rules (or are Che-only additions)
27+
# 1 — Unprotected modifications found
28+
# 2 — Error (can't find rebase commit, etc.)
29+
set -u
30+
31+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
32+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
33+
cd "$REPO_ROOT"
34+
35+
VERBOSE=false
36+
OUTPUT_MODE="text"
37+
COMMIT_RANGE=""
38+
for arg in "$@"; do
39+
case "$arg" in
40+
--verbose|-v) VERBOSE=true ;;
41+
--pr-comment) OUTPUT_MODE="pr-comment" ;;
42+
--list) OUTPUT_MODE="list" ;;
43+
*..*) COMMIT_RANGE="$arg" ;;
44+
esac
45+
done
46+
47+
# --- Locate the last rebase commit ---
48+
if [ -z "$COMMIT_RANGE" ]; then
49+
REBASE_COMMIT=$(git log --format='%H' --grep="^Rebase against the upstream" -1)
50+
if [ -z "$REBASE_COMMIT" ]; then
51+
if [ "$OUTPUT_MODE" = "list" ]; then
52+
exit 2
53+
fi
54+
echo "ERROR: Could not find a rebase commit in the history."
55+
echo "Expected a commit whose message starts with 'Rebase against the upstream'."
56+
exit 2
57+
fi
58+
REBASE_MSG=$(git log -1 --format='%s' "$REBASE_COMMIT")
59+
COMMIT_RANGE="${REBASE_COMMIT}..HEAD"
60+
fi
61+
62+
if [ "$OUTPUT_MODE" = "text" ]; then
63+
if [ -n "${REBASE_MSG:-}" ]; then
64+
echo "=== Unprotected Changes Check ==="
65+
echo "Base: $REBASE_COMMIT (${REBASE_MSG})"
66+
else
67+
echo "=== Unprotected Changes Check ==="
68+
echo "Range: $COMMIT_RANGE"
69+
fi
70+
echo ""
71+
fi
72+
73+
# --- Build the set of files covered by rebase rules ---
74+
COVERED_LIST=$(mktemp)
75+
trap 'rm -f "$COVERED_LIST"' EXIT
76+
77+
# 1. Parse resolve_conflicts() elif chain for explicit file paths.
78+
while IFS= read -r line; do
79+
if [[ "$line" =~ \[\[.*==.*\"([^\"]+)\".*\]\] ]]; then
80+
echo "${BASH_REMATCH[1]}"
81+
fi
82+
done < <(sed -n '/^resolve_conflicts()/,/^}/p' rebase.sh) >> "$COVERED_LIST"
83+
84+
# 2. .rebase/replace/ rules
85+
find .rebase/replace -name '*.json' -type f 2>/dev/null | while IFS= read -r rule_file; do
86+
code_path="${rule_file#.rebase/replace/}"
87+
code_path="${code_path%.json}"
88+
if [[ "$code_path" != code/* ]]; then
89+
code_path="code/$code_path"
90+
fi
91+
echo "$code_path"
92+
done >> "$COVERED_LIST"
93+
94+
# 3. .rebase/add/ rules
95+
find .rebase/add -type f 2>/dev/null | while IFS= read -r rule_file; do
96+
echo "${rule_file#.rebase/add/}"
97+
done >> "$COVERED_LIST"
98+
99+
# 4. .rebase/override/ rules
100+
find .rebase/override -type f 2>/dev/null | while IFS= read -r rule_file; do
101+
echo "${rule_file#.rebase/override/}"
102+
done >> "$COVERED_LIST"
103+
104+
sort -u -o "$COVERED_LIST" "$COVERED_LIST"
105+
106+
# --- Check changed files ---
107+
UNCOVERED=()
108+
COVERED_COUNT=0
109+
SKIPPED_COUNT=0
110+
111+
while IFS= read -r file_path; do
112+
# Skip package-lock.json (handled by resolve_package_lock during rebase)
113+
case "$file_path" in
114+
*/package-lock.json) ((SKIPPED_COUNT++)); continue ;;
115+
esac
116+
117+
# Skip Che-only additions — these don't exist in upstream and won't conflict
118+
case "$file_path" in
119+
code/extensions/che-*) ((SKIPPED_COUNT++)); continue ;;
120+
*/che/*.ts|*/che/*.js) ((SKIPPED_COUNT++)); continue ;;
121+
esac
122+
123+
if grep -qxF "$file_path" "$COVERED_LIST"; then
124+
((COVERED_COUNT++))
125+
continue
126+
fi
127+
128+
UNCOVERED+=("$file_path")
129+
done < <(git diff --name-only "$COMMIT_RANGE" -- code/)
130+
131+
# --- Output ---
132+
133+
# --list mode: bare file paths, nothing else
134+
if [ "$OUTPUT_MODE" = "list" ]; then
135+
for f in "${UNCOVERED[@]+${UNCOVERED[@]}}"; do
136+
echo "$f"
137+
done
138+
[ ${#UNCOVERED[@]} -eq 0 ] && exit 0 || exit 1
139+
fi
140+
141+
# --pr-comment mode: markdown for a GitHub PR comment
142+
if [ "$OUTPUT_MODE" = "pr-comment" ]; then
143+
if [ ${#UNCOVERED[@]} -eq 0 ]; then
144+
exit 0
145+
fi
146+
cat <<'HEADER'
147+
<!-- rebase-rules-check -->
148+
### Missing Rebase Rules
149+
150+
The following files were modified in this PR but do not have corresponding rebase rules.
151+
Without these rules, the changes **will be lost** during the next upstream rebase.
152+
153+
| # | File |
154+
|---|------|
155+
HEADER
156+
i=1
157+
for f in "${UNCOVERED[@]}"; do
158+
echo "| $i | \`$f\` |"
159+
((i++))
160+
done
161+
echo ""
162+
echo "Comment \`/add-rebase-rules\` on this PR to add the missing rules automatically."
163+
exit 1
164+
fi
165+
166+
# Default text mode
167+
if [ "$VERBOSE" = true ]; then
168+
echo "Covered: $COVERED_COUNT | Skipped: $SKIPPED_COUNT | Uncovered: ${#UNCOVERED[@]}"
169+
echo ""
170+
echo "Rebase rules on file:"
171+
sed 's/^/ /' "$COVERED_LIST"
172+
echo ""
173+
fi
174+
175+
if [ ${#UNCOVERED[@]} -eq 0 ]; then
176+
echo "All modified files in code/ are covered by rebase rules (or are Che-only additions)."
177+
echo " Covered: $COVERED_COUNT Skipped (lockfiles/Che-only): $SKIPPED_COUNT"
178+
exit 0
179+
fi
180+
181+
echo "**Found ${#UNCOVERED[@]} file(s) modified since the last rebase without rebase rules:**"
182+
echo ""
183+
for f in "${UNCOVERED[@]}"; do
184+
echo " - $f"
185+
done
186+
echo ""
187+
echo "These changes will be lost on the next upstream rebase unless a rule is added."
188+
echo ""
189+
echo "To fix, for each file above either:"
190+
echo " 1. Add a .rebase/replace/<path>.json with {\"from\": ..., \"by\": ...} rules"
191+
echo " 2. Add a .rebase/add/<path> or .rebase/override/<path> JSON fragment"
192+
echo " 3. Add an elif branch in resolve_conflicts() in rebase.sh"
193+
exit 1
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#
2+
# Copyright (c) 2026 Red Hat, Inc.
3+
# This program and the accompanying materials are made
4+
# available under the terms of the Eclipse Public License 2.0
5+
# which is available at https://www.eclipse.org/legal/epl-2.0/
6+
#
7+
# SPDX-License-Identifier: EPL-2.0
8+
#
9+
10+
#
11+
# This file was generated using AI assistance (Cursor AI)
12+
# and reviewed by the maintainers.
13+
#
14+
15+
name: Add Rebase Rules via dw-claude-runner
16+
17+
on:
18+
issue_comment:
19+
types: [created]
20+
21+
jobs:
22+
add-rebase-rules:
23+
if: >-
24+
github.event.issue.pull_request
25+
&& contains(github.event.comment.body, '/add-rebase-rules')
26+
runs-on: ubuntu-22.04
27+
permissions:
28+
pull-requests: write
29+
issues: write
30+
31+
steps:
32+
- name: Check author permission
33+
env:
34+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35+
run: |
36+
PERM=$(gh api "repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission" \
37+
--jq '.permission')
38+
if [[ "$PERM" != "admin" && "$PERM" != "write" ]]; then
39+
echo "User ${{ github.event.comment.user.login }} does not have write access (permission: $PERM)"
40+
exit 1
41+
fi
42+
43+
- name: Add reaction to command comment
44+
env:
45+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46+
run: |
47+
gh api "repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions" \
48+
-f content='rocket' --silent
49+
50+
- name: Get PR details
51+
id: pr
52+
env:
53+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
54+
run: |
55+
PR_JSON=$(gh api "repos/${{ github.repository }}/pulls/${{ github.event.issue.number }}")
56+
HEAD_SHA=$(echo "$PR_JSON" | jq -r '.head.sha')
57+
HEAD_REF=$(echo "$PR_JSON" | jq -r '.head.ref')
58+
PR_URL=$(echo "$PR_JSON" | jq -r '.html_url')
59+
echo "head_sha=${HEAD_SHA}" >> "$GITHUB_OUTPUT"
60+
echo "head_ref=${HEAD_REF}" >> "$GITHUB_OUTPUT"
61+
echo "pr_url=${PR_URL}" >> "$GITHUB_OUTPUT"
62+
63+
- name: Find and update bot comment to in-progress
64+
id: bot-comment
65+
env:
66+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
67+
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
68+
run: |
69+
COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments" \
70+
--paginate --jq '.[] | select(.body | contains("<!-- rebase-rules-check -->")) | .id' | head -1)
71+
72+
BODY="<!-- rebase-rules-check -->"$'\n'"### Missing Rebase Rules"$'\n\n'"Adding rebase rules — **in progress**... [View run](${RUN_URL})"
73+
74+
if [ -n "$COMMENT_ID" ]; then
75+
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
76+
-X PATCH -f body="$BODY"
77+
echo "comment_id=${COMMENT_ID}" >> "$GITHUB_OUTPUT"
78+
else
79+
COMMENT_URL=$(gh pr comment "${{ github.event.issue.number }}" \
80+
--repo "${{ github.repository }}" \
81+
--body "$BODY")
82+
NEW_ID=$(echo "$COMMENT_URL" | grep -oE '[0-9]+$')
83+
echo "comment_id=${NEW_ID}" >> "$GITHUB_OUTPUT"
84+
fi
85+
86+
- name: Checkout dw-claude-runner
87+
uses: actions/checkout@v4
88+
with:
89+
repository: RomanNikitenko/dw-claude-runner
90+
path: dw-claude-runner
91+
92+
- name: Install oc CLI
93+
uses: redhat-actions/openshift-tools-installer@v1
94+
with:
95+
oc: latest
96+
97+
- name: Login to OpenShift
98+
run: |
99+
echo "::add-mask::${{ secrets.OC_SERVER }}"
100+
echo "::add-mask::${{ secrets.OC_PROJECT }}"
101+
oc login --token=${{ secrets.OC_TOKEN }} \
102+
--server=${{ secrets.OC_SERVER }} > /dev/null
103+
104+
- name: Select OpenShift project
105+
run: oc project ${{ secrets.OC_PROJECT }} > /dev/null
106+
107+
- name: Run dw-claude-runner
108+
working-directory: dw-claude-runner
109+
env:
110+
PROJECT_URL: '"https://github.com/${{ github.repository }}.git"'
111+
TARGET_REPO: ${{ github.repository }}
112+
PROMPT: >-
113+
Checkout branch '${{ steps.pr.outputs.head_ref }}' first:
114+
git fetch origin && git checkout ${{ steps.pr.outputs.head_ref }}
115+
116+
Then run: add-rebase-rules ${{ steps.pr.outputs.head_sha }}
117+
118+
After committing, push the changes to the PR branch:
119+
git push origin ${{ steps.pr.outputs.head_ref }}
120+
TIMEOUT: "600"
121+
CLAUDE_TIMEOUT: "600"
122+
run: |
123+
set -o pipefail
124+
./run.sh -v 2>&1 | tee /tmp/runner-output.txt
125+
126+
- name: Update comment with result
127+
if: always() && steps.bot-comment.outputs.comment_id
128+
env:
129+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
130+
COMMENT_ID: ${{ steps.bot-comment.outputs.comment_id }}
131+
RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
132+
run: |
133+
if [ "${{ job.status }}" = "success" ]; then
134+
BODY="<!-- rebase-rules-check -->"$'\n'"### Missing Rebase Rules"$'\n\n'"Rebase rules have been **added successfully**. [View run](${RUN_URL})"$'\n\n'"Please review the new commit pushed to this PR."
135+
else
136+
BODY="<!-- rebase-rules-check -->"$'\n'"### Missing Rebase Rules"$'\n\n'"Failed to add rebase rules. [View run](${RUN_URL})"
137+
fi
138+
gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
139+
-X PATCH -f body="$BODY"

0 commit comments

Comments
 (0)