feat: enhance patch application process with AI conflict resolution

This commit is contained in:
robertsLando 2025-08-28 09:23:24 +02:00
parent d231c84ae6
commit 3fb4350468
No known key found for this signature in database
2 changed files with 316 additions and 8 deletions

199
.github/scripts/openai_resolver.py vendored Normal file
View File

@ -0,0 +1,199 @@
#!/usr/bin/env python3
import sys
import json
import os
from urllib.request import Request, urlopen
from urllib.parse import urlencode
def call_openai_api(prompt, api_key, model="gpt-3.5-turbo"):
"""Call OpenAI API to resolve patch conflicts"""
url = "https://api.openai.com/v1/chat/completions"
headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}
data = {
"model": model,
"messages": [
{
"role": "system",
"content": "You are an expert C++ developer helping to resolve Git patch conflicts. Return only the corrected file content without explanations or markdown formatting.",
},
{"role": "user", "content": prompt},
],
"temperature": 0.1,
"max_tokens": 4000,
}
try:
req = Request(url, data=json.dumps(data).encode("utf-8"), headers=headers)
with urlopen(req) as response:
result = json.loads(response.read().decode("utf-8"))
if "choices" in result and len(result["choices"]) > 0:
return result["choices"][0]["message"]["content"].strip()
else:
return None
except Exception as e:
print(f"OpenAI API error: {e}", file=sys.stderr)
return None
def resolve_conflict(reject_file, original_file, api_key):
"""Resolve a single conflict using OpenAI"""
# Read reject file content
try:
with open(reject_file, "r", encoding="utf-8", errors="ignore") as f:
reject_content = f.read()
except Exception as e:
print(f"Error reading reject file {reject_file}: {e}", file=sys.stderr)
return False
# Read current file content
current_content = ""
if os.path.exists(original_file):
try:
with open(original_file, "r", encoding="utf-8", errors="ignore") as f:
current_content = f.read()
except Exception as e:
print(f"Error reading original file {original_file}: {e}", file=sys.stderr)
# Create prompt for OpenAI
prompt = f"""I have a Git patch that failed to apply to a Node.js C++ source file.
REJECTED PATCH HUNKS:
```
{reject_content}
```
CURRENT FILE CONTENT:
```
{current_content}
```
Please analyze the rejected patch hunks and the current file content, then provide the complete corrected file content that successfully applies the intended changes from the patch. The changes should:
1. Apply the intended modifications from the rejected hunks
2. Handle any line number shifts or context changes
3. Maintain proper C++ syntax and formatting
4. Preserve existing functionality while adding the patch changes
Return ONLY the complete corrected file content, no explanations."""
print(f"Calling OpenAI API to resolve {original_file}...", file=sys.stderr)
resolved_content = call_openai_api(prompt, api_key)
if resolved_content:
try:
# Write resolved content
with open(original_file, "w", encoding="utf-8") as f:
f.write(resolved_content)
print(f"✅ Successfully resolved {original_file}")
return True
except Exception as e:
print(
f"Error writing resolved content to {original_file}: {e}",
file=sys.stderr,
)
return False
else:
print(f"❌ Failed to get resolution from OpenAI for {original_file}")
return False
def create_manual_resolution_file(reject_file, original_file):
"""Create a manual resolution file for conflicts that couldn't be auto-resolved"""
try:
with open(reject_file, "r", encoding="utf-8", errors="ignore") as f:
reject_content = f.read()
except:
reject_content = "Could not read reject file"
current_content = ""
if os.path.exists(original_file):
try:
with open(original_file, "r", encoding="utf-8", errors="ignore") as f:
current_content = f.read()
except:
current_content = "Could not read current file"
conflict_file = f"{original_file}.conflict"
with open(conflict_file, "w", encoding="utf-8") as f:
f.write(f"# MANUAL RESOLUTION REQUIRED FOR: {original_file}\n")
f.write("# Original reject content:\n")
f.write("#\n")
f.write(f"{reject_content}\n")
f.write("#\n")
f.write("# Current file content:\n")
f.write("#\n")
f.write(f"{current_content}\n")
print(f"Created manual resolution file: {conflict_file}")
if __name__ == "__main__":
if len(sys.argv) < 3:
print(
"Usage: openai_resolver.py <reject_files_dir> <api_key> [create_manual_only]",
file=sys.stderr,
)
sys.exit(1)
reject_dir = sys.argv[1]
api_key = sys.argv[2]
create_manual_only = len(sys.argv) > 3 and sys.argv[3] == "create_manual_only"
# Find all .rej files
reject_files = []
for root, dirs, files in os.walk(reject_dir):
for file in files:
if file.endswith(".rej"):
reject_files.append(os.path.join(root, file))
if not reject_files:
print("No reject files found")
sys.exit(0)
conflicts_resolved = 0
total_conflicts = len(reject_files)
failed_files = []
print(f"Found {total_conflicts} reject files to process")
for reject_file in reject_files:
original_file = reject_file[:-4] # Remove .rej extension
print(f"Processing: {reject_file} -> {original_file}")
if create_manual_only:
create_manual_resolution_file(reject_file, original_file)
failed_files.append(os.path.basename(original_file))
else:
# Try to resolve with OpenAI
if resolve_conflict(reject_file, original_file, api_key):
conflicts_resolved += 1
else:
failed_files.append(os.path.basename(original_file))
create_manual_resolution_file(reject_file, original_file)
# Clean up reject file
try:
os.remove(reject_file)
except:
pass
# Output results for GitHub Actions
print(f"CONFLICTS_RESOLVED={conflicts_resolved}")
print(f"TOTAL_CONFLICTS={total_conflicts}")
if failed_files:
print("FAILED_FILES<<EOF")
for file in failed_files:
print(f"- {file}")
print("EOF")
print(
f"Resolution summary: {conflicts_resolved}/{total_conflicts} conflicts resolved"
)
# Exit with appropriate code
sys.exit(0 if conflicts_resolved == total_conflicts else 1)

View File

@ -51,16 +51,74 @@ jobs:
git clone -b v${{ inputs.nodeVersion }} --single-branch https://github.com/nodejs/node.git
cd node
# apply the patch, if there are conflicts exit with error
# Try to apply the patch cleanly first
echo "Applying patch $PATCH_FILE"
git apply --check ../pkg-fetch/$PATCH_FILE
if [ $? -ne 0 ]; then
echo "Patch $PATCH_FILE does not apply cleanly"
exit 1
PATCH_CHECK_RESULT=$?
if [ $PATCH_CHECK_RESULT -eq 0 ]; then
echo "✅ Patch $PATCH_FILE applies cleanly"
git apply ../pkg-fetch/$PATCH_FILE
echo "PATCH_STATUS=clean" >> $GITHUB_ENV
else
echo "⚠️ Patch $PATCH_FILE does not apply cleanly, attempting conflict resolution"
# Apply patch with --reject to create .rej files
git apply --reject ../pkg-fetch/$PATCH_FILE || true
# Check if we have any .rej files
REJECT_COUNT=$(find . -name "*.rej" -type f | wc -l)
if [ $REJECT_COUNT -eq 0 ]; then
echo " No reject files found, patch applied partially"
echo "PATCH_STATUS=partial" >> $GITHUB_ENV
else
echo "🤖 Found $REJECT_COUNT reject files, using AI to resolve conflicts"
# Use our Python script to resolve conflicts
if [ -n "${{ secrets.OPENAI_KEY }}" ]; then
echo "Using OpenAI API for conflict resolution"
python3 ../pkg-fetch/.github/scripts/openai_resolver.py . "${{ secrets.OPENAI_KEY }}" > resolution_output.txt 2>&1
RESOLUTION_RESULT=$?
else
echo "No OpenAI API key found, creating manual resolution files only"
python3 ../pkg-fetch/.github/scripts/openai_resolver.py . "dummy" "create_manual_only" > resolution_output.txt 2>&1
RESOLUTION_RESULT=1
fi
# Extract results from Python script output
CONFLICTS_RESOLVED=$(grep "CONFLICTS_RESOLVED=" resolution_output.txt | cut -d'=' -f2)
TOTAL_CONFLICTS=$(grep "TOTAL_CONFLICTS=" resolution_output.txt | cut -d'=' -f2)
echo "PATCH_STATUS=conflicts" >> $GITHUB_ENV
echo "CONFLICTS_RESOLVED=${CONFLICTS_RESOLVED:-0}" >> $GITHUB_ENV
echo "TOTAL_CONFLICTS=${TOTAL_CONFLICTS:-0}" >> $GITHUB_ENV
# Extract failed files list
if grep -q "FAILED_FILES<<EOF" resolution_output.txt; then
echo "HAS_UNRESOLVED=true" >> $GITHUB_ENV
echo "FAILED_FILES<<EOF" >> $GITHUB_ENV
sed -n '/FAILED_FILES<<EOF/,/^EOF$/p' resolution_output.txt | grep "^- " >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
else
echo "HAS_UNRESOLVED=false" >> $GITHUB_ENV
fi
# Show resolution summary
cat resolution_output.txt
# Check if we have unresolved conflicts - if so, exit without creating PR
if [ "${HAS_UNRESOLVED:-false}" = "true" ]; then
echo "❌ There are unresolved conflicts. Exiting without creating PR."
echo "Please resolve the conflicts manually and re-run the workflow."
exit 1
fi
fi
fi
echo "Patch $PATCH_FILE applies cleanly, creating new patch file"
git apply ../pkg-fetch/$PATCH_FILE
# Only proceed with patch creation if we got here (no unresolved conflicts)
echo "✅ All conflicts resolved or patch applied successfully. Creating new patch file."
# delete old patch file and create new one
rm -rf ../pkg-fetch/$PATCH_FILE
@ -72,17 +130,68 @@ jobs:
cd ../pkg-fetch/patches
sed -i "s/\"v$PATCH_VERSION\": \[\"node.v$PATCH_VERSION.cpp.patch\"\]/\"v${{ inputs.nodeVersion }}\": \[\"node.v${{ inputs.nodeVersion }}.cpp.patch\"\]/" patches.json
# Set default values for environment variables if not already set
echo "PATCH_STATUS=${PATCH_STATUS:-clean}" >> $GITHUB_ENV
echo "HAS_UNRESOLVED=${HAS_UNRESOLVED:-false}" >> $GITHUB_ENV
echo "CREATE_PR=true" >> $GITHUB_ENV
- name: Summary
run: |
echo "=================================================="
echo "📋 WORKFLOW EXECUTION SUMMARY"
echo "=================================================="
echo "Node.js version: ${{ inputs.nodeVersion }}"
echo "Patch status: ${PATCH_STATUS:-unknown}"
case "${PATCH_STATUS:-unknown}" in
"clean")
echo "✅ Patch applied cleanly without conflicts"
;;
"partial")
echo "⚠️ Patch applied partially"
;;
"conflicts")
echo "🤖 AI-assisted conflict resolution used"
echo "Total conflicts: ${TOTAL_CONFLICTS:-0}"
echo "Resolved conflicts: ${CONFLICTS_RESOLVED:-0}"
if [ "${HAS_UNRESOLVED:-false}" = "true" ]; then
echo "❌ Some conflicts remain unresolved"
else
echo "✅ All conflicts successfully resolved"
fi
;;
esac
if [ "${CREATE_PR:-false}" = "true" ]; then
echo "🚀 Pull request will be created"
else
echo "⏹️ Pull request creation skipped due to unresolved issues"
fi
echo "=================================================="
- name: Create Pull Request
if: env.CREATE_PR == 'true'
uses: peter-evans/create-pull-request@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "feat: add v${{ inputs.nodeVersion }} patch"
title: "feat: add v${{ inputs.nodeVersion }} patch"
body: "This PR bumps the Node.js patch version to v${{ inputs.nodeVersion }}"
body: |
## Node.js Patch Update to v${{ inputs.nodeVersion }}
This PR updates the Node.js patch to version ${{ inputs.nodeVersion }}.
The workflow automatically attempts to resolve patch conflicts using AI when the OpenAI API key is available.
### Steps to verify:
1. Review the patch changes
2. Test the patched Node.js build
3. Validate functionality
4. Merge if everything looks good
branch: "nodejs-v${{ inputs.nodeVersion }}"
base: "main"
delete-branch: true
labels: "enhancement, nodejs"
labels: "enhancement,nodejs"
draft: false