Last active
March 23, 2020 17:19
-
-
Save hassenius/e1de8dee3d22449c1bf6cd7da7194810 to your computer and use it in GitHub Desktop.
Backporter experiments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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