From 9a7ce2577b5cfea98f1aba6b641111f33f7aa2b6 Mon Sep 17 00:00:00 2001 From: Mashhur Date: Thu, 12 Dec 2024 09:57:23 -0800 Subject: [PATCH 1/2] Add a PR backporter which we need to backport changes to designnated base branches when we merge PRs to target branches. --- .github/workflows/pr_backporter.yml | 49 ++++++ devtools/backport | 228 ++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+) create mode 100644 .github/workflows/pr_backporter.yml create mode 100755 devtools/backport diff --git a/.github/workflows/pr_backporter.yml b/.github/workflows/pr_backporter.yml new file mode 100644 index 00000000..09fea3c5 --- /dev/null +++ b/.github/workflows/pr_backporter.yml @@ -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 "43502315+logstashmachine@users.noreply.github.com" + - 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 diff --git a/devtools/backport b/devtools/backport new file mode 100755 index 00000000..41ebdd14 --- /dev/null +++ b/devtools/backport @@ -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 + + +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()) From ac0e1cb939a98c91e1046bbfbde36ce73fdb3d21 Mon Sep 17 00:00:00 2001 From: Mashhur <99575341+mashhurs@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:11:32 -0800 Subject: [PATCH 2/2] Apply suggestions from code review Not bad Copilot - apply suggestion of version strip and writing token into a file. Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/pr_backporter.yml | 2 +- devtools/backport | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pr_backporter.yml b/.github/workflows/pr_backporter.yml index 09fea3c5..ecadcba2 100644 --- a/.github/workflows/pr_backporter.yml +++ b/.github/workflows/pr_backporter.yml @@ -43,7 +43,7 @@ jobs: with: python-version: 3.8 - run: | - mkdir ~/.elastic && echo ${{ github.token }} >> ~/.elastic/github.token + 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 diff --git a/devtools/backport b/devtools/backport index 41ebdd14..2b12db5f 100755 --- a/devtools/backport +++ b/devtools/backport @@ -203,7 +203,7 @@ def create_pr(parser, args): def get_version(base_dir): with open(os.path.join(base_dir, "VERSION"), "r") as f: for line in f: - return "v" + line + return "v" + line.strip() def get_github_token():