diff --git a/jenkins/opensearch/backport-pr.jenkinsfile b/jenkins/opensearch/backport-pr.jenkinsfile new file mode 100644 index 0000000000..98796db91c --- /dev/null +++ b/jenkins/opensearch/backport-pr.jenkinsfile @@ -0,0 +1,65 @@ +pipeline{ + agent any + environment{ + GITHUB_TOKEN = credentials('github-token') + REPO_URL = 'https://github.com/opensearch-project/opensearch-build.git' + REPO_DIR = 'opensearch-build' + } + stages{ + stage('Determine PR type'){ + steps{ + echo 'Determining if the PR is Stalled or Backport..' + script{ + def prType = determinePRType() + if(prType == 'stalled'){ + echo 'Processing stalled PR..' + processStalledPR() + } + else if(prType == 'backport'){ + echo 'Processing Backport PR...' + processBackportPR() + } + else{ + echo 'PR does not match criteria. Exiting Pipeline.' + } + } + } + } + } + post{ + success{ + echo 'Pipeline completed successfully' + } + failure{ + echo 'Pipeline failed. Check logs for details' + } + } +} + +def determinePRType(token, repoURL, prId){ + def result = sh(script: """ + curl -s -H "Authorization: Bearer ${token}" -H "Accept: application/vnd.github.v3+json" \ + ${repoURL}/pulls/${prId} | jq '.labels | map(.name)' + """, returnStdout: true).trim() + if(result.contains('stalled')){ + return 'stalled' + }else if (result.contains('backport')){ + return 'backport' + }else{ + return null + } +} + +def processStalledPR(){ + echo 'Rebasing stalled PR branch onto target branch...' + sh """ + scripts/pr-management/StalledPRs.py --repo $REPO_REPO_URL --token $GITHUB_TOKEN + """ +} + +def processBackportPR(){ + echo 'Resolving backport PR conflicts...' + sh """ + scripts/pr-management/BackportPRs.py --repo $REPO_URL --token $GITHUB_TOKEN + """ +} \ No newline at end of file diff --git a/scripts/pr-management/BackportPRs.py b/scripts/pr-management/BackportPRs.py new file mode 100644 index 0000000000..761ef7c8f5 --- /dev/null +++ b/scripts/pr-management/BackportPRs.py @@ -0,0 +1,65 @@ +import os +import requests +import subprocess + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +HEADERS = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"} +BASE_URL = "https://api.github.com" + +def fetch_backport_prs(owner, repo): + """Fetch backport PR's with the `backport` label""" + url = f"{BASE_URL}/search/issues" + query = f"repo:{owner}/{repo} label:backport is:pr is:open" + response = requests.get(url, headers=HEADERS, params={"q":query}) + response.raise_for_status() + return response.json()["items"] + +def fetch_pr_details(owner, repo, pr_number): + """Fetch PR details to get source and target branches""" + url = f"{BASE_URL}/repos/{owner}/{repo}/pulls/{pr_number}" + response = requests.get(url, header=HEADERS) + response.raise_for_status() + return response.json()["items"] + +def resolve_changelog_conflict(repo_dir, pr_branch, target_branch): + """Resolve conflicts in CHANGELOG.md""" + subprocess.run(["git","checkout",pr_branch], cwd=repo_dir) + subprocess.run(["git","fetch","origin", target_branch], cwd=repo_dir) + subprocess.run(["git","rebase",f"origin/{target_branch}"], cwd=repo_dir) + + conflicted_files = subprocess.check_output( + ["git","diff","--name-only","--diff-filter=U"], cwd=repo_dir + ).decode().strip().split("\n") + if "CHANGELOG.md" in conflicted_files: + print("Conflict detected in CHANGELOG.md. Resolving....") + changelog_file = f"{repo_dir}/CHANGELOG.md" + with open(changelog_file, "r") as file: + lines = file.readlines() + start_old = lines.index("<<<<<<< HEAD\n") + middle = lines.index("=======\n") + end_new = lines.index(">>>>>>> ", middle) + old_changes = lines[start_old + 1: middle] + new_changes = lines[middle + 1: end_new] + resolved_changes = ( + ["# CHANGELOG\n\n"] + + ["## Existing Changes (from target branch):\n"] + old_changes + + ["\n## New Changes (from backport PR):\n"] + new_changes + ) + with open(changelog_file, "w") as file: + file.writelines(resolved_changes) + subprocess.run(["git", "add", "CHANGELOG.md"], cwd=repo_dir) + subprocess.run(["git","commit","-m","Resolved CHANGELOG.md conflict"], cwd=repo_dir) + subprocess.run(["git","push","--force-with-lease"], cwd=repo_dir) + +def main_backport(owner, repo, repo_dir): + """Main function to handle backport PRs""" + backport_prs = fetch_backport_prs(owner,repo) + for pr in backport_prs: + pr_number = pr["number"] + pr_details = fetch_pr_details(owner, repo, pr_number) + pr_branch = pr_details["head"]["ref"] + target_branch = pr_details["base"]["ref"] + print(f"Handling Backport PR #{pr_number}: {pr_branch} -> {target_branch}") + resolve_changelog_conflict(repo_dir, pr_branch, target_branch) + + diff --git a/scripts/pr-management/README.md b/scripts/pr-management/README.md new file mode 100644 index 0000000000..54c8e7a0c6 --- /dev/null +++ b/scripts/pr-management/README.md @@ -0,0 +1,16 @@ +# PR Management Scripts + +This folder contains scripts for automating tasks related to pull requests. + +## BackportPRs.py + +This script handles backport PRs by: +- Detecting and resolving conflicts in `CHANGELOG.md`. +- Combining old and new changes during conflict resolution. +- Committing the resolved file back to the PR branch. + +## StalledPRs.py + +This script handles Stalled PRs by: +- Fetching all the stalled PRs using the Stalled label +- Rebase the PRs onto the latest target branch and push updates \ No newline at end of file diff --git a/scripts/pr-management/StalledPRs.py b/scripts/pr-management/StalledPRs.py new file mode 100644 index 0000000000..5725dc1b8a --- /dev/null +++ b/scripts/pr-management/StalledPRs.py @@ -0,0 +1,36 @@ +import os +import requests +import subprocess + + +GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") +HEADERS = {"Authorization": f"Bearer {GITHUB_TOKEN}", "Accept": "application/vnd.github.v3+json"} +BASE_URL = "https://api.github.com" + +def fetch_stalled_prs(owner, repo): + """Fetch stalled PRs with the `stalled` label""" + url = f"{BASE_URL}"/search/issues + query = f"repo:{owner}/{repo} label:stalled is:pr is:open" + response = requests.get(url, headers=HEADERS, params={"q": query}) + response.raise_for_status() + return response.json()["items"] + +def rebase_pr(repo_dir, pr_branch, target_branch): + """Rebase a stalled PR onto the target branch.""" + subprocess.run(["git","checkout",pr_branch], cwd=repo_dir) + subprocess.run(["git","fetch","origin", target_branch], cwd=repo_dir) + subprocess.run(["git","rebase",f"origin"/{target_branch}], cwd=repo_dir) + subprocess.run(["git","push","--force-with-lease"], cwd=repo_dir) + +def main_stalled(owner, repo, repo_dir): + """Main function to handle stalled PRs""" + stalled_prs = fetch_stalled_prs(owner,repo) + for pr in stalled_prs: + pr_number = pr["number"] + pr_details = fetch_stalled_prs(owner, repo, pr_number) + pr_branch = pr_details["head"]["ref"] + target_branch = pr_details["base"]["ref"] + print(f"Handling Stalled PR #{pr_number}: {pr_branch} -> {target_branch}") + rebase_pr(repo_dir, pr_branch, target_branch) + +