diff --git a/.github/workflows/upmerge-auto-comment.yml b/.github/workflows/upmerge-auto-comment.yml new file mode 100644 index 000000000..9e4f1a72f --- /dev/null +++ b/.github/workflows/upmerge-auto-comment.yml @@ -0,0 +1,192 @@ +name: Post Auto Upmerge Results + +on: + workflow_run: + workflows: ["Auto Upmerge"] + types: [completed] + +jobs: + post-upmerge-results: + runs-on: ubuntu-latest + if: always() + permissions: + contents: read + pull-requests: write + actions: read + steps: + - name: "Download Auto Upmerge Results" + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + pattern: "auto-upmerge-results-pr-*" + merge-multiple: true + path: artifacts/ + + - name: "Setup Python" + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: "Post Results Comment" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} + WORKFLOW_CONCLUSION: ${{ github.event.workflow_run.conclusion }} + shell: python + run: | + import os + import json + import sys + import subprocess + from pathlib import Path + + def find_results_file(): + artifacts_path = Path('artifacts') + + if not artifacts_path.exists(): + print('Artifacts directory not found') + return None + + files = list(artifacts_path.iterdir()) + print(f'Available files: {[f.name for f in files]}') + + for file in files: + if file.name.endswith('auto-upmerge-results.json'): + return file + + return None + + def load_results(results_file): + try: + with open(results_file, 'r') as f: + return json.load(f) + except Exception as e: + print(f'Error loading results file: {e}') + return None + + def build_comment_body(results, workflow_conclusion): + pr_info = results.get('source_pr', {}) + pr_number = pr_info.get('number', 'unknown') + source_branch = results.get('source_branch', 'unknown') + + successful = results.get('successful_upmerges', []) + failed = results.get('failed_upmerges', []) + attempted = results.get('attempted_branches', []) + + comment_body = "## ๐Ÿ”„ Auto Upmerge Results\n\n" + + if workflow_conclusion == 'success' and successful and not failed: + comment_body += "**Status**: โœ… All upmerges completed successfully\n" + elif successful and failed: + comment_body += "**Status**: โš ๏ธ Partial success - some upmerges failed\n" + elif failed and not successful: + comment_body += "**Status**: โŒ All upmerges failed\n" + else: + comment_body += "**Status**: โŒ Auto upmerge workflow failed\n" + + comment_body += f"**Source PR**: #{pr_number} - {pr_info.get('title', 'Unknown')}\n" + comment_body += f"**Source Branch**: `{source_branch}`\n" + comment_body += f"**Timestamp**: {results.get('timestamp', 'unknown')}\n\n" + + if successful: + comment_body += f"### โœ… Successful Upmerges ({len(successful)})\n\n" + for branch in successful: + branch_info = next((b for b in attempted if b['branch'] == branch), {}) + version = branch_info.get('version', 'unknown') + comment_body += f"- `{branch}` (version: {version}) - PR created for manual review\n" + comment_body += "\n" + + if failed: + comment_body += f"### โŒ Failed Upmerges ({len(failed)})\n\n" + for branch in failed: + branch_info = next((b for b in attempted if b['branch'] == branch), {}) + version = branch_info.get('version', 'unknown') + comment_body += f"- `{branch}` (version: {version}) - Manual intervention required\n" + comment_body += "\n**How To Resolve**: Check the auto upmerge workflow logs for specific error details. You may need to resolve conflicts manually.\n\n" + + if not successful and not failed: + comment_body += "No upmerge attempts were made. This could be due to:\n" + comment_body += "- No branches were marked as successful in the upmerge test\n" + comment_body += "- The auto upmerge workflow failed before attempting any upmerges\n\n" + + comment_body += "*This comment was automatically generated by the auto upmerge workflow.*" + + return comment_body + + def post_github_comment(pr_number, comment_body): + if not pr_number or pr_number == 'unknown': + print('No valid PR number found - cannot post comment') + return False + + comments_url = f'https://api.github.com/repos/{os.environ["GITHUB_REPOSITORY"]}/issues/{pr_number}/comments' + comment_data = json.dumps({"body": comment_body}) + + result = subprocess.run([ + 'curl', '-X', 'POST', comments_url, + '-H', 'Content-Type: application/json', + '-H', f'Authorization: token {os.environ["GITHUB_TOKEN"]}', + '--data', comment_data + ], capture_output=True, text=True) + + if result.returncode == 0: + print('Successfully posted upmerge results comment') + return True + else: + print(f'Failed to post comment: {result.stderr}') + return False + + def post_fallback_comment(workflow_conclusion): + fallback_comment = ( + "## ๐Ÿ”„ Auto Upmerge Results\n\n" + "**Status**: โŒ Auto upmerge workflow failed\n" + "**How To Resolve**: Check the auto upmerge workflow logs for error details. " + "The workflow may have failed before generating detailed results.\n\n" + f"**Workflow Status**: {workflow_conclusion}\n\n" + "*This comment was automatically generated by the auto upmerge workflow.*" + ) + + # Try to get PR number from the original upmerge test artifacts or workflow + # This is a fallback, so we might not have all the context + print('Posting fallback comment - detailed results not available') + return fallback_comment + + try: + workflow_conclusion = os.environ.get('WORKFLOW_CONCLUSION', 'unknown') + + results_file = find_results_file() + + if not results_file: + print('No auto upmerge results found - posting fallback comment') + fallback_comment = post_fallback_comment(workflow_conclusion) + # We don't know the PR number in this case, so we can't post a comment + print('Cannot determine PR number for fallback comment') + sys.exit(0) + + print(f'Found results file: {results_file.name}') + + results = load_results(results_file) + if not results: + print('Could not load results - cannot post comment') + sys.exit(1) + + print(f'Loaded results: {json.dumps(results, indent=2)}') + + pr_number = results.get('source_pr', {}).get('number') + if not pr_number: + print('No PR number in results - cannot post comment') + sys.exit(1) + + comment_body = build_comment_body(results, workflow_conclusion) + success = post_github_comment(pr_number, comment_body) + + if not success: + print('Failed to post comment') + sys.exit(1) + + print('Successfully processed auto upmerge results') + + except Exception as e: + print(f'Error processing auto upmerge results: {e}') + sys.exit(1) \ No newline at end of file diff --git a/.github/workflows/upmerge-auto.yml b/.github/workflows/upmerge-auto.yml new file mode 100644 index 000000000..d96f2b8f4 --- /dev/null +++ b/.github/workflows/upmerge-auto.yml @@ -0,0 +1,446 @@ +name: Auto Upmerge + +on: + workflow_run: + workflows: ["Upmerge Test"] + types: [completed] + +jobs: + auto-upmerge: + runs-on: ubuntu-latest + # Only run if the upmerge test completed successfully + if: ${{ github.event.workflow_run.conclusion == 'success' }} + permissions: + contents: write + pull-requests: write + actions: read + steps: + - name: "Debug Vars" + env: + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} + WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} + run: | + echo "Workflow Run ID: $WORKFLOW_RUN_ID" + echo "Workflow Run Head SHA: $WORKFLOW_RUN_HEAD_SHA" + echo "Workflow Run Head Branch: $WORKFLOW_RUN_HEAD_BRANCH" + echo "Workflow Run PR Number: $WORKFLOW_RUN_PR_NUMBER" + + - name: "Download Upmerge Test Results" + uses: actions/download-artifact@v4 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + run-id: ${{ github.event.workflow_run.id }} + pattern: "upmerge-test-results-pr-*" + merge-multiple: true + path: artifacts/ + + - name: "Setup Python" + uses: actions/setup-python@v5 + with: + python-version: "3.8" + + - name: "Install dependencies" + run: | + set -xe + python -VV + python -m site + python -m pip install --upgrade pip setuptools wheel + + - name: "Checkout" + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + fetch-tags: true + + - name: "Configure Git" + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: "Perform Auto Upmerge" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} + WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} + DRY_RUN: ${{ vars.AUTO_UPMERGE_DRY_RUN || 'false' }} + shell: python + run: | + import os + import re + import subprocess + import time + import sys + import json + from pathlib import Path + + def extractVersion(versionStr): + parts = versionStr.split('.') + if len(parts) != 3: + print('Invalid version: ' + versionStr) + sys.exit(1) + if parts[2].lower() == 'x': + parts[2] = '0' + + major, minor, point = map(int, parts) + return [major, minor, point] + + def getTagVersionForCmd(cmd): + versionPattern = re.compile(r".*([0-9]+\.[0-9]+\.[0-9]+).*") + + # Get latest release version + gitTagProcess = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True) + (output, err) = gitTagProcess.communicate() + gitTagProcessStatus = gitTagProcess.wait() + + if gitTagProcessStatus != 0: + print('Unable to retrieve latest git tag.') + sys.exit(1) + + latestGitTag = str(output) + + versionMatch = versionPattern.match(latestGitTag) + if versionMatch: + return extractVersion(versionMatch.group(1)) + else: + print('Unable to extract version from git tag.') + sys.exit(2) + + def buildBranchString(version): + major, minor, point = map(int, version) + return f"candidate-{major}.{minor}.x" + + def createReleaseTagPattern(projectConfig, major = None, minor = None, point = None): + releaseTagPrefix = projectConfig.get('tagPrefix') + releaseTagPostfix = projectConfig.get('tagPostfix') + + if releaseTagPrefix is None or releaseTagPostfix is None: + print('Error: PROJECT_CONFIG is missing required fields: tagPrefix and/or tagPostfix') + sys.exit(1) + + releaseTagPattern = releaseTagPrefix + if major is not None: + releaseTagPattern += str(major) + '\\.' + else: + releaseTagPattern += '[0-9]+\\.' + + if minor is not None: + releaseTagPattern += str(minor) + '\\.' + else: + releaseTagPattern += '[0-9]+\\.' + + if point is not None: + releaseTagPattern += str(point) + '(-[0-9]+)?' + else: + releaseTagPattern += '[0-9]+(-[0-9]+)?' + + releaseTagPattern += releaseTagPostfix + '$' + + return releaseTagPattern + + def getLatestSemVer(projectConfig, major = None, minor = None, point = None): + cmd = "git tag --list --sort=-v:refname | grep -E '" + createReleaseTagPattern(projectConfig, major, minor, point) + "' | head -n 1" + return getTagVersionForCmd(cmd) + + def generateUpMergeBranchList(projectConfig, branchName): + latestVersion = getLatestSemVer(projectConfig) + + versions = [] + if branchName == "master": + return versions + else: + # Extract candidate branch major / minor version + candidateBranchPattern = re.compile(r"candidate-([0-9]+\.[0-9]+\.([0-9]+|x)).*") + branchVersionMatch = candidateBranchPattern.match(branchName) + branchVersion = extractVersion(branchVersionMatch.group(1)) + + # Get latest release in branch + latestBranchVer = getLatestSemVer(projectConfig, branchVersion[0], branchVersion[1]) + + curMajor = branchVersion[0] + latestMajor = latestVersion[0] + while curMajor <= latestMajor: + latestVersionInMajor = getLatestSemVer(projectConfig, curMajor) + + curMinor = 0 + if curMajor == branchVersion[0]: + curMinor = branchVersion[1] + + latestMinor = latestVersionInMajor[1] + + while curMinor <= latestMinor: + latestPointInMinor = getLatestSemVer(projectConfig, curMajor, curMinor) + versions.append(buildBranchString([latestPointInMinor[0], latestPointInMinor[1], latestPointInMinor[2] + 2])) + curMinor += 2 + curMajor += 1 + + return versions + + def getTargetInBranchVersion(targetBranch): + # Use git show to get the version from the top-level pom.xml + cmd = ["git", "show", f"origin/{targetBranch}:pom.xml"] + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + pom_content = result.stdout + + version_pattern = re.compile(r'(\s*[0-9]+\.[0-9]+\.[0-9]+(-[0-9]+)?(-SNAPSHOT)?\s*)') + match = version_pattern.search(pom_content) + if match: + return match.group(1).strip() + else: + print(f"Error: Unable to find version in pom.xml for branch {targetBranch}") + sys.exit(1) + + def loadTestResults(): + artifacts_path = Path('artifacts') + + if not artifacts_path.exists(): + print('Artifacts directory not found - cannot determine successful merge targets') + return None + + files = list(artifacts_path.iterdir()) + print(f'Available files: {[f.name for f in files]}') + + results_file = None + for file in files: + if file.name.endswith('upmerge-test-results.json'): + results_file = file + break + + if not results_file: + print('No upmerge test results found') + return None + + try: + with open(results_file, 'r') as f: + data = json.load(f) + return data + except Exception as e: + print(f'Error loading results file: {e}') + return None + + def performUpmerge(sourceBranch, targetBranch, targetVersion, pr_info, dry_run=False): + print(f"\n=== {'DRY RUN: ' if dry_run else ''}Performing upmerge from {sourceBranch} to {targetBranch} ===") + + if dry_run: + print("DRY RUN MODE: No actual changes will be made") + + try: + # Checkout target branch + print(f"Checking out target branch: {targetBranch}") + checkout_result = subprocess.run( + ["git", "checkout", f"origin/{targetBranch}"], + capture_output=True, text=True + ) + if checkout_result.returncode != 0: + print(f"Failed to checkout {targetBranch}: {checkout_result.stderr}") + return False + + # Create a new local branch for the upmerge + upmerge_branch = f"upmerge-{sourceBranch}-to-{targetBranch}-{int(time.time())}" + print(f"Creating upmerge branch: {upmerge_branch}") + create_branch_result = subprocess.run( + ["git", "checkout", "-b", upmerge_branch], + capture_output=True, text=True + ) + if create_branch_result.returncode != 0: + print(f"Failed to create branch {upmerge_branch}: {create_branch_result.stderr}") + return False + + # Merge from source branch head + source_sha = os.environ.get('WORKFLOW_RUN_HEAD_SHA') + print(f"Merging changes from {source_sha}") + merge_result = subprocess.run( + ["git", "merge", source_sha, "--no-edit", "-m", f"Auto upmerge from {sourceBranch} to {targetBranch} (PR #{pr_info.get('number')})"], + capture_output=True, text=True + ) + if merge_result.returncode != 0: + print(f"Merge failed: {merge_result.stderr}") + return False + + # Update version in pom.xml files + print(f"Updating version to {targetVersion}") + mvn_result = subprocess.run( + ["mvn", "versions:set", f"-DnewVersion={targetVersion}", "-DgenerateBackupPoms=false"], + capture_output=True, text=True + ) + if mvn_result.returncode != 0: + print(f"Maven version update failed: {mvn_result.stderr}") + return False + + # Add and commit version changes + add_result = subprocess.run(["git", "add", "-A"], capture_output=True, text=True) + if add_result.returncode != 0: + print(f"Git add failed: {add_result.stderr}") + return False + + # Check if there are changes to commit + status_result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True) + if status_result.stdout.strip(): + commit_result = subprocess.run( + ["git", "commit", "-m", f"Update version to {targetVersion} for {targetBranch}"], + capture_output=True, text=True + ) + if commit_result.returncode != 0: + print(f"Git commit failed: {commit_result.stderr}") + return False + + if dry_run: + print("DRY RUN: Would push branch and create PR, but skipping actual execution") + return True + + # Push the upmerge branch + print(f"Pushing upmerge branch: {upmerge_branch}") + push_result = subprocess.run( + ["git", "push", "origin", upmerge_branch], + capture_output=True, text=True + ) + if push_result.returncode != 0: + print(f"Push failed: {push_result.stderr}") + return False + + # Create a pull request for the upmerge + print(f"Creating PR for upmerge to {targetBranch}") + pr_title = f"Auto upmerge from {sourceBranch} to {targetBranch} (Original PR #{pr_info.get('number')})" + pr_body = f"This is an automatic upmerge from {sourceBranch} to {targetBranch}.\n\n" + pr_body += f"Original PR: #{pr_info.get('number')} - {pr_info.get('title')}\n" + pr_body += f"Original Author: {pr_info.get('author')}\n\n" + pr_body += f"Changes:\n- Merged changes from {sourceBranch}\n" + pr_body += f"- Updated version to {targetVersion}\n\n" + pr_body += "This PR was automatically created by the auto-upmerge workflow." + + # Use GitHub API to create the PR + import json as json_mod + pr_data = { + "title": pr_title, + "body": pr_body, + "head": upmerge_branch, + "base": targetBranch + } + + pr_create_result = subprocess.run([ + "curl", "-X", "POST", + f"https://api.github.com/repos/{os.environ['GITHUB_REPOSITORY']}/pulls", + "-H", "Accept: application/vnd.github.v3+json", + "-H", f"Authorization: token {os.environ['GITHUB_TOKEN']}", + "-H", "Content-Type: application/json", + "--data", json_mod.dumps(pr_data) + ], capture_output=True, text=True) + + if pr_create_result.returncode != 0: + print(f"PR creation failed: {pr_create_result.stderr}") + return False + + try: + pr_response = json_mod.loads(pr_create_result.stdout) + if 'html_url' in pr_response: + print(f"Successfully created upmerge PR: {pr_response['html_url']}") + return True + else: + print(f"PR creation response: {pr_create_result.stdout}") + return False + except json_mod.JSONDecodeError: + print(f"Invalid JSON response: {pr_create_result.stdout}") + return False + + except Exception as e: + print(f"Error during upmerge to {targetBranch}: {e}") + return False + + # Main execution + workflow_run_pr_number = os.environ.get('WORKFLOW_RUN_PR_NUMBER') + dry_run = os.environ.get('DRY_RUN', 'false').lower() == 'true' + + if dry_run: + print("๐Ÿงช DRY RUN MODE ENABLED - No actual changes will be made") + + if not workflow_run_pr_number: + print("No PR number found in workflow run") + sys.exit(1) + + # Load test results to determine which branches were successful + test_results = loadTestResults() + if not test_results: + print("Could not load test results - aborting auto upmerge") + sys.exit(1) + + successful_merges = test_results.get('successful_merges', []) + base_branch = test_results.get('base_branch', 'unknown') + + if not successful_merges: + print("No successful merges found in test results - nothing to upmerge") + sys.exit(0) + + pr_info = { + 'number': workflow_run_pr_number, + 'title': test_results.get('pr_title', 'Unknown'), + 'author': test_results.get('pr_author', 'Unknown') + } + + print(f"Starting auto upmerge for PR #{pr_info['number']} from {base_branch}") + print(f"Target branches: {successful_merges}") + + # Hard coding project config (same as in upmerge-test) + projectConfig = { + "projectName": "HPCC4J", + "projectPrefixes": ["HPCC4J"], + "tagPrefix": "hpcc4j_", + "tagPostfix": "-release" + } + + # Track results + upmerge_results = { + "source_pr": pr_info, + "source_branch": base_branch, + "timestamp": time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime()), + "attempted_branches": [], + "successful_upmerges": [], + "failed_upmerges": [] + } + + # Process each target branch + for targetBranch in successful_merges: + try: + targetVersion = getTargetInBranchVersion(targetBranch) + + upmerge_results["attempted_branches"].append({ + "branch": targetBranch, + "version": targetVersion + }) + + success = performUpmerge(base_branch, targetBranch, targetVersion, pr_info, dry_run) + + if success: + upmerge_results["successful_upmerges"].append(targetBranch) + print(f"โœ… Successfully upmerged to {targetBranch}") + else: + upmerge_results["failed_upmerges"].append(targetBranch) + print(f"โŒ Failed to upmerge to {targetBranch}") + + except Exception as e: + print(f"Error processing {targetBranch}: {e}") + upmerge_results["failed_upmerges"].append(targetBranch) + + # Save results + with open('auto-upmerge-results.json', 'w') as f: + json.dump(upmerge_results, f, indent=2) + + print(f"\nAuto upmerge completed:") + print(f"Successful: {len(upmerge_results['successful_upmerges'])}") + print(f"Failed: {len(upmerge_results['failed_upmerges'])}") + + if upmerge_results["failed_upmerges"]: + print("Some upmerges failed. Check the logs for details.") + sys.exit(1) + + - name: "Upload Auto Upmerge Results" + uses: actions/upload-artifact@v4 + if: always() + with: + name: auto-upmerge-results-pr-${{ github.event.workflow_run.pull_requests[0].number }} + path: | + auto-upmerge-results.json + retention-days: 30 \ No newline at end of file diff --git a/docs/AUTO_UPMERGE.md b/docs/AUTO_UPMERGE.md new file mode 100644 index 000000000..9170972cd --- /dev/null +++ b/docs/AUTO_UPMERGE.md @@ -0,0 +1,143 @@ +# Auto Upmerge Workflows + +This repository includes automated upmerge functionality that builds on the existing upmerge testing infrastructure. + +## Overview + +The auto upmerge system consists of three GitHub Actions workflows: + +1. **upmerge-test.yml** - Tests if a PR can be safely upmerged (existing) +2. **upmerge-auto.yml** - Automatically performs upmerges when tests pass (new) +3. **upmerge-auto-comment.yml** - Posts results of the auto upmerge process (new) + +## How It Works + +### Workflow Sequence + +1. A PR is created against a candidate branch (e.g., `candidate-9.8.x`) +2. The **upmerge-test** workflow runs automatically and tests if the PR can be upmerged to all subsequent branches (9.10.x, 9.12.x, 9.14.x, 10.x, master) +3. If the upmerge test **succeeds**, the **auto-upmerge** workflow triggers automatically +4. The auto-upmerge workflow: + - Downloads the test results to determine which branches are safe to upmerge to + - For each successful branch, creates a new upmerge branch + - Merges the original changes and updates the version appropriately + - Creates a new PR for manual review +5. The **upmerge-auto-comment** workflow posts a summary comment on the original PR + +### Branch Resolution Logic + +The system follows the same branch resolution logic as the existing upmerge test: + +- Starting from a candidate branch like `candidate-9.8.x` +- Upmerges to: `candidate-9.10.x`, `candidate-9.12.x`, `candidate-9.14.x`, `candidate-10.x`, `master` +- Each target branch gets its appropriate version from its `pom.xml` + +### Example Workflow + +1. PR #123 created against `candidate-9.8.x` +2. Upmerge test runs and finds that it can safely merge to `candidate-9.10.x` and `master`, but conflicts with `candidate-9.12.x` +3. Auto upmerge workflow creates two new PRs: + - "Auto upmerge from candidate-9.8.x to candidate-9.10.x (Original PR #123)" + - "Auto upmerge from candidate-9.8.x to master (Original PR #123)" +4. A comment is posted on PR #123 summarizing the results + +## Configuration + +### Repository Variables + +- `AUTO_UPMERGE_DRY_RUN` - Set to "true" to enable dry run mode (default: false) + +### Required Permissions + +The workflows require the following permissions: +- `contents: write` - To create branches and commit changes +- `pull-requests: write` - To create PRs and post comments +- `actions: read` - To download artifacts from the upmerge test + +## Safety Features + +### Dry Run Mode + +Set the repository variable `AUTO_UPMERGE_DRY_RUN` to "true" to enable dry run mode. In this mode: +- All git operations are performed (checkout, merge, version updates) +- No branches are pushed to remote +- No PRs are created +- Results show what would have been done + +### Error Handling + +- Each upmerge operation is independent - if one fails, others continue +- Failed upmerges are reported in the summary comment +- Detailed error logs are available in the workflow run +- Original test results are preserved for troubleshooting + +### Manual Review Required + +- Auto upmerge creates PRs but does NOT auto-merge them +- Each upmerged PR requires manual review and approval +- This provides a safety net for any unexpected issues + +## Files Created + +### upmerge-auto.yml + +The main auto upmerge workflow that: +- Triggers when upmerge-test completes successfully +- Downloads test results to determine target branches +- Performs git operations (merge, version update, branch creation) +- Creates PRs using the GitHub API +- Saves detailed results as artifacts + +### upmerge-auto-comment.yml + +A supporting workflow that: +- Triggers when auto-upmerge completes (success or failure) +- Downloads auto upmerge results +- Posts a formatted comment on the original PR +- Provides troubleshooting information for failures + +## Troubleshooting + +### Common Issues + +1. **Permission Errors** + - Ensure the workflows have the required permissions + - Check that the GITHUB_TOKEN has sufficient scope + +2. **Maven Version Update Failures** + - Ensure Maven is available in the runner + - Check that pom.xml files are valid + +3. **Git Operation Failures** + - May indicate merge conflicts that weren't caught in testing + - Check git logs in the workflow output + +4. **PR Creation Failures** + - May indicate API rate limits or permission issues + - Check the GitHub API response in workflow logs + +### Getting Help + +1. Check the workflow run logs for detailed error messages +2. Review the artifacts uploaded by each workflow +3. Enable dry run mode to test changes safely +4. Review the original upmerge test results to understand which branches should have succeeded + +## Testing + +To test the auto upmerge functionality: + +1. Create a test PR against a candidate branch +2. Ensure it passes the upmerge test +3. Monitor the auto upmerge workflow execution +4. Review created PRs and comments +5. Use dry run mode for safe testing + +## Maintenance + +The workflows inherit the project configuration from the upmerge test, including: +- Tag patterns (`hpcc4j_*-release`) +- Branch naming conventions (`candidate-X.Y.x`) +- Version resolution logic + +Updates to the upmerge test logic may require corresponding updates to the auto upmerge workflows. \ No newline at end of file