-
Notifications
You must be signed in to change notification settings - Fork 13
Add a PR backporter #203
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
Merged
Merged
Add a PR backporter #203
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
name: Backport PR to another branch | ||
on: | ||
issue_comment: | ||
types: [created] | ||
|
||
permissions: | ||
pull-requests: write | ||
contents: write | ||
|
||
jobs: | ||
pr_commented: | ||
name: PR comment | ||
if: github.event.issue.pull_request | ||
runs-on: ubuntu-latest | ||
steps: | ||
- uses: actions-ecosystem/action-regex-match@v2 | ||
id: regex-match | ||
with: | ||
text: ${{ github.event.comment.body }} | ||
regex: '^@logstashmachine backport (main|[x0-9\.]+)$' | ||
- if: ${{ steps.regex-match.outputs.group1 == '' }} | ||
run: exit 1 | ||
- name: Fetch logstash-core team member list | ||
uses: tspascoal/get-user-teams-membership@v1 | ||
id: checkUserMember | ||
with: | ||
username: ${{ github.actor }} | ||
organization: elastic | ||
team: logstash | ||
GITHUB_TOKEN: ${{ secrets.READ_ORG_SECRET_JSVD }} | ||
- name: Is user not a core team member? | ||
if: ${{ steps.checkUserMember.outputs.isTeamMember == 'false' }} | ||
run: exit 1 | ||
- name: checkout repo content | ||
uses: actions/checkout@v2 | ||
with: | ||
fetch-depth: 0 | ||
ref: 'main' | ||
- run: git config --global user.email "[email protected]" | ||
- run: git config --global user.name "logstashmachine" | ||
- name: setup python | ||
uses: actions/setup-python@v2 | ||
with: | ||
python-version: 3.8 | ||
- run: | | ||
mkdir ~/.elastic && echo ${{ github.token }} >> ~/.elastic/github.token | ||
- run: pip install requests | ||
- name: run backport | ||
run: python devtools/backport ${{ steps.regex-match.outputs.group1 }} ${{ github.event.issue.number }} --remote=origin --yes |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
#!/usr/bin/env python3 | ||
"""Cherry pick and backport a PR""" | ||
from __future__ import print_function | ||
|
||
from builtins import input | ||
import sys | ||
import os | ||
import argparse | ||
from os.path import expanduser | ||
import re | ||
from subprocess import check_call, call, check_output | ||
import requests | ||
|
||
usage = """ | ||
Example usage: | ||
./devtools/backport 7.16 2565 6490604aa0cf7fa61932a90700e6ca988fc8a527 | ||
|
||
In case of backporting errors, fix them, then run | ||
git cherry-pick --continue | ||
./devtools/backport 7.16 2565 6490604aa0cf7fa61932a90700e6ca988fc8a527 --continue | ||
|
||
This script does the following: | ||
* cleanups both from_branch and to_branch (warning: drops local changes) | ||
* creates a temporary branch named something like "branch_2565" | ||
* calls the git cherry-pick command in this branch | ||
* after fixing the merge errors (if needed), pushes the branch to your | ||
remote | ||
* it will attempt to create a PR for you using the GitHub API, but requires | ||
the GitHub token, with the public_repo scope, available in `~/.elastic/github.token`. | ||
Keep in mind this token has to also be authorized to the Elastic organization as | ||
well as to work with SSO. | ||
(see https://help.github.com/en/articles/authorizing-a-personal-access-token-for-use-with-saml-single-sign-on) | ||
|
||
Note that you need to take the commit hashes from `git log` on the | ||
from_branch, copying the IDs from Github doesn't work in case we squashed the | ||
PR. | ||
""" | ||
|
||
|
||
def main(): | ||
"""Main""" | ||
parser = argparse.ArgumentParser( | ||
description="Creates a PR for cherry-picking commits", | ||
formatter_class=argparse.RawDescriptionHelpFormatter, | ||
epilog=usage) | ||
parser.add_argument("to_branch", | ||
help="To branch (e.g 7.x)") | ||
parser.add_argument("pr_number", | ||
help="The PR number being merged (e.g. 2345)") | ||
parser.add_argument("commit_hashes", metavar="hash", nargs="*", | ||
help="The commit hashes to cherry pick." + | ||
" You can specify multiple.") | ||
parser.add_argument("--yes", action="store_true", | ||
help="Assume yes. Warning: discards local changes.") | ||
parser.add_argument("--continue", action="store_true", | ||
help="Continue after fixing merging errors.") | ||
parser.add_argument("--from_branch", default="main", | ||
help="From branch") | ||
parser.add_argument("--diff", action="store_true", | ||
help="Display the diff before pushing the PR") | ||
parser.add_argument("--remote", default="", | ||
help="Which remote to push the backport branch to") | ||
args = parser.parse_args() | ||
|
||
print(args) | ||
|
||
create_pr(parser, args) | ||
|
||
|
||
def create_pr(parser, args): | ||
info("Checking if GitHub API token is available in `~/.elastic/github.token`") | ||
token = get_github_token() | ||
|
||
tmp_branch = "backport_{}_{}".format(args.pr_number, args.to_branch) | ||
|
||
if not vars(args)["continue"]: | ||
if not args.yes and input("This will destroy all local changes. " + | ||
"Continue? [y/n]: ") != "y": | ||
return 1 | ||
info("Destroying local changes...") | ||
check_call("git reset --hard", shell=True) | ||
check_call("git clean -df", shell=True) | ||
check_call("git fetch", shell=True) | ||
|
||
info("Checkout of {} to backport from....".format(args.from_branch)) | ||
check_call("git checkout {}".format(args.from_branch), shell=True) | ||
check_call("git pull", shell=True) | ||
|
||
info("Checkout of {} to backport to...".format(args.to_branch)) | ||
check_call("git checkout {}".format(args.to_branch), shell=True) | ||
check_call("git pull", shell=True) | ||
|
||
info("Creating backport branch {}...".format(tmp_branch)) | ||
call("git branch -D {} > /dev/null".format(tmp_branch), shell=True) | ||
check_call("git checkout -b {}".format(tmp_branch), shell=True) | ||
|
||
if len(args.commit_hashes) == 0: | ||
if token: | ||
session = github_session(token) | ||
base = "https://api.github.com/repos/elastic/logstash-filter-elastic_integration" | ||
original_pr = session.get(base + "/pulls/" + args.pr_number).json() | ||
merge_commit = original_pr['merge_commit_sha'] | ||
if not merge_commit: | ||
info("Could not auto resolve merge commit - PR isn't merged yet") | ||
return 1 | ||
info("Merge commit detected from PR: {}".format(merge_commit)) | ||
commit_hashes = merge_commit | ||
else: | ||
info("GitHub API token not available. " + | ||
"Please manually specify commit hash(es) argument(s)\n") | ||
parser.print_help() | ||
return 1 | ||
else: | ||
commit_hashes = "{}".format(" ").join(args.commit_hashes) | ||
|
||
info("Cherry-picking {}".format(commit_hashes)) | ||
if call("git cherry-pick -x {}".format(commit_hashes), shell=True) != 0: | ||
info("Looks like you have cherry-pick errors.") | ||
info("Fix them, then run: ") | ||
info(" git cherry-pick --continue") | ||
info(" {} --continue".format(" ".join(sys.argv))) | ||
return 1 | ||
|
||
if len(check_output("git status -s", shell=True).strip()) > 0: | ||
info("Looks like you have uncommitted changes." + | ||
" Please execute first: git cherry-pick --continue") | ||
return 1 | ||
|
||
if len(check_output("git log HEAD...{}".format(args.to_branch), | ||
shell=True).strip()) == 0: | ||
info("No commit to push") | ||
return 1 | ||
|
||
if args.diff: | ||
call("git diff {}".format(args.to_branch), shell=True) | ||
if input("Continue? [y/n]: ") != "y": | ||
info("Aborting cherry-pick.") | ||
return 1 | ||
|
||
info("Ready to push branch.") | ||
|
||
remote = args.remote | ||
if not remote: | ||
remote = input("To which remote should I push? (your fork): ") | ||
|
||
info("Pushing branch {} to remote {}".format(tmp_branch, remote)) | ||
call("git push {} :{} > /dev/null".format(remote, tmp_branch), shell=True) | ||
check_call("git push --set-upstream {} {}".format(remote, tmp_branch), shell=True) | ||
|
||
if not token: | ||
info("GitHub API token not available.\n" + | ||
"Manually create a PR by following this URL: \n\t" + | ||
"https://github.com/elastic/logstash-filter-elastic_integration/compare/{}...{}:{}?expand=1" | ||
.format(args.to_branch, remote, tmp_branch)) | ||
else: | ||
info("Automatically creating a PR for you...") | ||
|
||
session = github_session(token) | ||
base = "https://api.github.com/repos/elastic/logstash-filter-elastic_integration" | ||
original_pr = session.get(base + "/pulls/" + args.pr_number).json() | ||
|
||
# get the github username from the remote where we pushed | ||
remote_url = check_output("git remote get-url {}".format(remote), shell=True) | ||
remote_user = re.search("github.com[:/](.+)/logstash", str(remote_url)).group(1) | ||
|
||
# create PR | ||
request = session.post(base + "/pulls", json=dict( | ||
title="Backport PR #{} to {}: {}".format(args.pr_number, args.to_branch, original_pr["title"]), | ||
head=remote_user + ":" + tmp_branch, | ||
base=args.to_branch, | ||
body="**Backport PR #{} to {} branch, original message:**\n\n---\n\n{}" | ||
.format(args.pr_number, args.to_branch, original_pr["body"]) | ||
)) | ||
if request.status_code > 299: | ||
info("Creating PR failed: {}".format(request.json())) | ||
sys.exit(1) | ||
new_pr = request.json() | ||
|
||
# add labels | ||
labels = ["backport"] | ||
# get the version (vX.Y.Z) we are backporting to | ||
version = get_version(os.getcwd()) | ||
if version: | ||
labels.append(version) | ||
|
||
session.post( | ||
base + "/issues/{}/labels".format(new_pr["number"]), json=labels) | ||
|
||
""" | ||
if not args.keep_backport_label: | ||
# remove needs backport label from the original PR | ||
session.delete(base + "/issues/{}/labels/needs_backport".format(args.pr_number)) | ||
""" | ||
# Set a version label on the original PR | ||
if version: | ||
session.post( | ||
base + "/issues/{}/labels".format(args.pr_number), json=[version]) | ||
|
||
info("Done. PR created: {}".format(new_pr["html_url"])) | ||
info("Please go and check it and add the review tags") | ||
|
||
|
||
def get_version(base_dir): | ||
with open(os.path.join(base_dir, "VERSION"), "r") as f: | ||
for line in f: | ||
return "v" + line | ||
mashhurs marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
||
|
||
def get_github_token(): | ||
try: | ||
token = open(expanduser("~/.elastic/github.token"), "r").read().strip() | ||
except: | ||
token = False | ||
return token | ||
|
||
|
||
def github_session(token): | ||
session = requests.Session() | ||
session.headers.update({"Authorization": "token " + token}) | ||
return session | ||
|
||
|
||
def info(msg): | ||
print("\nINFO: {}".format(msg)) | ||
|
||
|
||
if __name__ == "__main__": | ||
sys.exit(main()) |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.