Skip to content

Commit

Permalink
feat 16099: Rebase strategy for backport PRs
Browse files Browse the repository at this point in the history
  • Loading branch information
PrateekAlsi committed Dec 26, 2024
1 parent 7d2ae0b commit 7fccbde
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 0 deletions.
65 changes: 65 additions & 0 deletions jenkins/opensearch/backport-pr.jenkinsfile
Original file line number Diff line number Diff line change
@@ -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
"""
}
65 changes: 65 additions & 0 deletions scripts/pr-management/BackportPRs.py
Original file line number Diff line number Diff line change
@@ -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)


16 changes: 16 additions & 0 deletions scripts/pr-management/README.md
Original file line number Diff line number Diff line change
@@ -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
36 changes: 36 additions & 0 deletions scripts/pr-management/StalledPRs.py
Original file line number Diff line number Diff line change
@@ -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)


0 comments on commit 7fccbde

Please sign in to comment.