Skip to content

Instantly share code, notes, and snippets.

@hassenius
Last active March 23, 2020 17:19
Show Gist options
  • Save hassenius/e1de8dee3d22449c1bf6cd7da7194810 to your computer and use it in GitHub Desktop.
Save hassenius/e1de8dee3d22449c1bf6cd7da7194810 to your computer and use it in GitHub Desktop.
Backporter experiments
#!/usr/bin/env python
##
## A good service here would be a robot that instead of picking up labels goes out and
## checks if the PR _could_ be merged to defined supported release branches _if_ the PR
## is labelled as bug. If it is automatically mergable this would indicate that the bug
## is also applicable to the supported releases.
##
from github import Github
import os, re
import subprocess
backportindicator='releases/'
cherrypick_pr_title="Backport of #{0}: {1}" # to become "Cherry-pick of #pr: {master pr subject}"
cherrypicked_indicator="Backport of #{}:"
access_token=os.environ['github_token']
org=os.environ['github_org']
repo=os.environ['github_repo']
upstreamorg=os.environ['github_upstream']
baseurl=os.environ['github_base_url']
dryrun=False
# Check which PRs are candidates for back porting
g = Github(base_url=baseurl, login_or_token=access_token)
# Create a connection object to github upstream repo
ur = g.get_repo("%s/%s" % (upstreamorg, repo))
# Create a connection object to github origin repo
orr = g.get_repo("%s/%s" % (org, repo))
# Simple function that scans the master branch for closed PRs
# These will be defined as candidates for backporting
def get_backport_candidates(gh=None):
# Get a list of closed pull requests against master
pr = gh.get_pulls(base="master", state="closed", sort="created")
# Add merged PRs with relevant labels as backport candidates
backport_candidates = {}
for n in range(len(list(pr))):
if pr[n].merged == True:
for i in range(len(pr[n].labels)):
if pr[n].labels[i].name.startswith(backportindicator):
if pr[n].labels[i].name not in backport_candidates:
backport_candidates[pr[n].labels[i].name] = []
backport_candidates[pr[n].labels[i].name].append(pr[n].number)
return backport_candidates
def create_backport_in_branch(pr=None,basebranch=None,commits=[]):
# Try to check out a branch off basebranch
cp_branch_name=f"backport-pr-{str(pr)}-{basebranch}"
cmd = ["git", "checkout", "-b", cp_branch_name, "--track", "upstream/"+basebranch]
if dryrun:
print('dry-run: ' + ' '.join(cmd))
else:
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
print(
f"Error checking out the branch {cp_branch_name}."
)
print(err.output)
exit(1)
# Try cherry-pick the commits there
for c in commits:
cmd = ["git", "cherry-pick", "-x", c]
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
print(
f"Error cherry-picking {c}."
)
print(err.output)
exit(1)
return cp_branch_name
# Now determine which candidates have not been back ported to each release branch
# We use a predetermined PR title to determine state of backport / cherry-pick to release
# TODO
# Likely this will run into github api rate limit when it scales, so a better approach would be
# to handle it purely via labelling. For example have a separate bot that manages
# a label on PRs when they merge to master, then remove when successfully back ported
backport_candidates = get_backport_candidates(ur)
for c in backport_candidates:
release=c.replace(backportindicator,"")
pr = ur.get_pulls(base=release, state="all", sort="created")
# Get a list of all the auto-generated PRs in the release
prs_list = [o.title for o in pr]
# Format to checkable format containing just the cherrypicked_indicator string and actual pr number
regex = re.compile(cherrypicked_indicator.format('[0-9]'))
release_pr_titles = [m.group(0) for l in prs_list for m in [regex.search(l)] if m]
# Create a list of what cherry-picked master pr titles would look like in release branch
master_pr_titles = [cherrypicked_indicator.format(x) for x in backport_candidates[c]]
# Subtract to see which PRs are not in release branch
candidate_titles = list(set(master_pr_titles) - set(release_pr_titles))
# Extract the master PR numbers that we want to backport
candidate_master_pr = [ o[0] for o in [re.findall(r'\d+', x) for x in candidate_titles ] ]
# Now we have a list of master PRs to cherry pick and PR to release branch, let's see if we can
for p in candidate_master_pr:
orig_pr = ur.get_pull(int(p))
# Get a list of commit hashes
commits = [x.sha for x in orig_pr.get_commits()]
# Create a backport branch with cherry picked commits
b = create_backport_in_branch(p, release, commits)
# Push the branch to our own repo
cmd = ["git", "push", "origin", b]
if dryrun:
print('dry-run: ' + ' '.join(cmd))
else:
try:
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as err:
print(
f"Error pushing branch {b}."
)
print(err.output)
exit(1)
# Open a pull request
body = f"Automatic back-port of #{p}"
title = cherrypick_pr_title.format(orig_pr.number, orig_pr.title)
npr = ur.create_pull(title=title, body=body, head='{}:{}'.format(org, b), base=release)
print(f"Create pull request {npr} against {release}")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment