diff --git a/.github/scripts/openai_resolver.py b/.github/scripts/openai_resolver.py new file mode 100644 index 0000000..963e119 --- /dev/null +++ b/.github/scripts/openai_resolver.py @@ -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 [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<> $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<> $GITHUB_ENV + echo "FAILED_FILES<> $GITHUB_ENV + sed -n '/FAILED_FILES<> $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 \ No newline at end of file