feat: enhance patch application process with AI conflict resolution
This commit is contained in:
parent
d231c84ae6
commit
3fb4350468
199
.github/scripts/openai_resolver.py
vendored
Normal file
199
.github/scripts/openai_resolver.py
vendored
Normal 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)
|
||||
125
.github/workflows/patch-node.yml
vendored
125
.github/workflows/patch-node.yml
vendored
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user