Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat 16099: Rebase strategy for backport PRs #5216

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)


Loading