예제 #1
0
class Bot():
    def __init__(self, config, secrets, directory, interactive, reports):
        self.secrets = secrets
        self.directory = directory
        self.interactive = interactive
        self.reports = reports

        self.bot_features = config["bot_features"]

        self.username = config["username"]
        self.orgs = config.get("orgs", [])
        self.repo_maintainers = config.get("repos", {})
        self.default_maintainers = config.get("reviewers", [])
        self.trusted = config.get("trusted", [])
        self.jenkins_repos = config.get("jenkins_repos", [])
        banned_emails = config.get("banned_emails", {})
        self.banned_emails = [v for v in banned_emails.values()]

        self.jenkins = None
        if "jenkins" in config:
            self.jenkins = Jenkins(config["jenkins"], config["jenkins_job"],
                                   secrets, self.username)

        self.github = GitHub(secrets["GITHUB_TOKEN"], self.username,
                             self.jenkins_repos)

        self.slack = None
        self.dispatcher = None
        if secrets.get("SLACK_READ_TOKEN"):
            self.slack = Slack(read_token=secrets.get("SLACK_READ_TOKEN"),
                               bot_token=secrets.get("SLACK_SEND_TOKEN"),
                               app_token=secrets.get("SLACK_APP_TOKEN"),
                               username=self.username,
                               interactive=interactive)
            self.dispatcher = CommandDispatcher(self.slack)

            if 'create_prs_from_slack' in self.bot_features:
                self.github_interface = GitHubInterface(
                    self.github, self.slack, self.dispatcher)
            if 'update_dependencies' in self.bot_features:
                self.updater = UpdateChecker(self.github, self.slack,
                                             self.dispatcher, 'Lex-2008')
            if 'generate_changelogs' in self.bot_features:
                self.changelogger = ChangelogGenerator(self.github, self.slack,
                                                       self.dispatcher,
                                                       'Lex-2008')
            if 'map_packages' in self.bot_features:
                self.package_mapper = PackageMapper(self.github, self.slack,
                                                    self.dispatcher,
                                                    'Lex-2008')
            if 'tag_builds' in self.bot_features:
                self.tagger = Tagger(self.github, self.slack, self.dispatcher,
                                     'Lex-2008')

    def post(self, path, data, msg=None):
        if self.interactive:
            print("I'd like to POST something")
            msg = "" if msg is None else msg
            msg += "Path: {}".format(path)
            msg += "Data: {}".format(data)
            if not confirmation(msg):
                return False
        self.github.post(path, data)
        return True

    def comment(self, pr, message):
        path = pr.comments_url
        pr_string = "PR: {} ({}#{})".format(pr.title, pr.repo, pr.number)
        data = {"body": str(message)}
        commented = self.post(path, data, pr_string)
        if commented:
            print("Commented on {}".format(pr_string))
            print(message)
            print("")

    def ping_reviewer(self, pr):
        if pr.age < datetime.timedelta(days=1):
            log.info("This PR is less than a day old, I won't ping yet")
        elif self.username in pr.comments.users:
            log.info("I have already commented :)")
        elif len(pr.comments) > 0:
            log.info("There are already comments there, so I won't disturb")
        elif len(pr.requested_reviewers) > 0:
            log.info("Someone already assigned a reviewer, I won't disturb")
        elif len(pr.reviews) > 0:
            log.info(
                "This PR has reviews already, so I'll leave it to you humans")
        elif pr.has_label("WIP") or "WIP" in pr.title.upper():
            log.info("This is a WIP PR, so I won't disturb")
        else:
            thanks = random.choice(["Thanks", "Thank you"])
            pull = random.choice(["PR", "pull request"])
            comment = "{thanks} for submitting a {pr}! Maybe @{user} can review this?".format(
                thanks=thanks, pr=pull, user=pr.reviewer)
            self.comment(pr, comment)

    def leave_review(self, pr):
        tom = self.username
        if tom in pr.maintainers and tom not in pr.denials + pr.approvals:
            for person in pr.approvals:
                if person in pr.maintainers:
                    log.info("Approved by: " + str(pr.approvals))
                    body = "I trust @{}, approved!".format(person)
                    event = "APPROVE"
                    data = {"body": body, "event": event}
                    self.post(pr.reviews_url, data)
                    print("Approved PR: {}".format(pr.title))
                    return

    def check_emails(self, pr):
        log.debug("E-mails: {}".format(pr.emails))
        bad_emails = set()
        for email in pr.emails:
            hash = email_sha256(email)
            log.debug("{}: {}".format(email, hash))
            if email in self.banned_emails or hash in self.banned_emails:
                log.info("Found banned email: " + email)
                bad_emails.add(email)

        obfuscated = [f"{e[0]}***@{e[e.index('@')+1:]}" for e in bad_emails]
        bad_emails = ",".join(obfuscated)

        if bad_emails:
            body = f"Please use a company e-mail instead of {bad_emails}"
            event = "REQUEST_CHANGES"
            data = {"body": body, "event": event}
            self.post(pr.reviews_url, data)
            print("Denied PR: {}".format(pr.title))
            return False
        return True

    def review(self, pr):
        tom = self.username
        log.info("Reviewing: {}".format(pr.title))

        if tom in pr.denials:
            log.info("I've already denied this PR")
            return

        if "check_commit_emails" in self.bot_features:
            success = self.check_emails(pr)
            if not success:
                return

        if "approve_prs" in self.bot_features:
            self.leave_review(pr)

    def comment_badge(self, pr, num, url, badge_text):
        badge_icon = "{url}/buildStatus/icon?job={job}&build={num}".format(
            url=self.jenkins.url, job=self.jenkins.job_name, num=num)
        badge_link = "{url}/job/{job}/{num}/".format(url=self.jenkins.url,
                                                     job=self.jenkins.job_name,
                                                     num=num)
        badge = "[![Build Status]({})]({})".format(badge_icon, badge_link)
        response = random.choice(["Alright", "Sure"])
        if badge_text:
            badge_text = "\n\n" + badge_text  # Under looks better
        buildcache = "http://buildcache.cfengine.com"
        packages = "{}/packages/testing-pr/jenkins-pr-pipeline-{}/".format(
            buildcache, num)
        new_comment = "{}, I triggered a build:\n\n{}{}\n\n**Jenkins:** {}\n\n**Packages:** {}".format(
            response, badge, badge_text, url, packages)
        self.comment(pr, new_comment)

    def trigger_build(self, pr, comment):
        prs = {}
        prs[pr.short_repo_name] = pr.number
        for repo_name in pr.merge_with:
            if repo_name not in prs:
                prs[repo_name] = pr.merge_with[repo_name]
        description = ""
        exotics = False
        if "exotic" in comment:
            exotics = True
            description = "(with exotics)"

        if self.interactive:
            msg = []
            msg.append(str(comment))
            msg.append("Triger build for: {}".format(pr.title))
            msg.append("PRs: {}".format(prs))
            msg.append("EXOTICS: {}".format(exotics))
            msg = "\n".join(msg)
            if not confirmation(msg):
                return

        headers, body = self.jenkins.trigger(prs, pr.base_branch, pr.title,
                                             exotics, comment.author)

        queue_url = headers["Location"]

        num, url = self.jenkins.wait_for_queue(queue_url)

        print("Triggered build ({}): {}".format(num, url))
        self.comment_badge(pr, num, url, description)

    def handle_mention(self, pr, comment):
        deny = "@{} : I'm sorry, I cannot do that. @olehermanse please help.".format(
            comment.author)
        if comment.author not in self.trusted:
            print("Denying mention from {}".format(comment.author))
            self.comment(pr, deny)
            return
        body = comment.body.lower()
        trigger_words = ["jenkins", "pipeline", "build", "trigger"]
        for word in trigger_words:
            if word.lower() in body:
                self.trigger_build(pr, comment)
                return

        no_comprendo = "I'm not sure I understand, @{}.".format(comment.author)
        self.comment(pr, no_comprendo)

    def handle_comments(self, pr):
        for comment in reversed(pr.comments):
            if comment.author == self.username:
                return
            if "@" + self.username in comment:
                self.handle_mention(pr, comment)

    def find_reviewers(self, pr):
        maintainers = self.default_maintainers
        if pr.repo in self.repo_maintainers:
            maintainers = self.repo_maintainers[pr.repo]

        pr.maintainers = maintainers
        pr.reviewers = copy(maintainers)
        if pr.author in pr.reviewers:
            pr.reviewers.remove(pr.author)
        if self.username in pr.reviewers:
            pr.reviewers.remove(self.username)
        if len(pr.reviewers) <= 0:
            pr.reviewers = self.default_maintainers
        pr.reviewer = random.choice(pr.reviewers)

    def handle_pr(self, pr):
        url = pr["url"].replace("https://api.github.com/repos/", "")
        log.info("Looking at: {} ({})".format(pr["title"], pr["html_url"]))

        pr = PR(pr, self.github)
        if "ping_reviewers" in self.bot_features:
            self.find_reviewers(pr)
            self.ping_reviewer(pr)
        if ("check_commit_emails" in self.bot_features
                or "approve_prs" in self.bot_features):
            self.review(pr)
        if "trigger_jenkins_from_gh_comments" in self.bot_features:
            self.handle_comments(pr)

        if "report_open_prs" in self.bot_features:
            self.reports.log_pr(pr)

    def run(self):
        self.repos = []
        if self.orgs:
            for org in self.orgs:
                self.repos += self.github.get("/orgs/{}/repos".format(org))

        self.repos = {repo["full_name"]: repo["url"] for repo in self.repos}
        for repo in self.repo_maintainers:
            self.repos[repo] = "/repos/" + repo

        self.pulls = []
        for repo, url in self.repos.items():
            log.info("Fetching pull requests for {}".format(repo))
            pulls = self.github.get(url + "/pulls")
            if pulls:
                self.pulls.extend(pulls)

        if self.pulls:
            log.info("Found {} open pull requests".format(len(self.pulls)))
        else:
            log.warning("Couldn't find any open pull requests!")

        errs = 0
        for pull in self.pulls:
            try:
                self.handle_pr(pull)
            except AssertionError:
                log.error(
                    "AssertionError encountered while handling '{}'".format(
                        pull["title"]))
                errs += 1
        if (errs == 0):
            log.info("Tom successful")
        else:
            log.error("Tom encountered {} errors".format(errs))

    def talk(self):
        if not self.interactive:
            self.slack.parse_stdin(self.dispatcher)
            return

        print('Type Slack messages (do not prefix them with bot name)')
        print('Type "help" for list of commands')
        print('Type "quit" or "exit" when bored')
        prompt = '<@{}> '.format(self.username)
        self.slack.reply_to_user = '******'
        while True:
            try:
                text = input(prompt)
            except EOFError:
                # Ctrl-D was pressed
                return
            if text.lower().strip() in ['quit', 'exit']:
                return
            self.dispatcher.parse_text(text)
예제 #2
0
class Bot():
    def __init__(self, config, secrets, directory, interactive):
        self.secrets = secrets
        self.directory = directory
        self.interactive = interactive

        self.username = config["username"]
        self.orgs = config["orgs"]
        self.repo_maintainers = config["repos"]
        self.default_maintainers = config["reviewers"]
        self.trusted = config["trusted"]

        self.jenkins = Jenkins(config["jenkins"], config["jenkins_job"], secrets)
        self.github = GitHub(secrets["GITHUB_TOKEN"])

        self.slack = None
        try:
            self.slack_read_token = secrets["SLACK_READ_TOKEN"]
            bot_token = secrets["SLACK_SEND_TOKEN"]
            app_token = secrets["SLACK_APP_TOKEN"]
            self.slack = Slack(bot_token, app_token)

            self.dispatcher = CommandDispatcher(self.slack)
            self.github_interface = GitHubInterface(self.github, self.slack, self.dispatcher)
            # self.updater = UpdateChecker(self.github, self.slack, self.dispatcher)
        except KeyError:
            log.info("Skipping slack integration, secrets missing")

    def post(self, path, data, msg=None):
        if self.interactive:
            print("I'd like to POST something")
            msg = "" if msg is None else msg
            msg += "Path: {}".format(path)
            msg += "Data: {}".format(data)
            if not confirmation(msg):
                return False
        self.github.post(path, data)
        return True

    def comment(self, pr, message):
        path = pr.comments_url
        pr_string = "PR: {} ({}#{})".format(pr.title, pr.repo, pr.number)
        data = {"body": str(message)}
        commented = self.post(path, data, pr_string)
        if commented:
            print("Commented on {}".format(pr_string))
            print(message)
            print("")

    def ping_reviewer(self, pr):
        if self.username in pr.comments.users:
            log.info("I have already commented :)")
        elif len(pr.comments) > 0:
            log.info("There are already comments there, so I won't disturb")
        elif len(pr.review_comments) > 0:
            log.info("This PR has review comments already, so I'll leave it to you humans")
        elif pr.has_label("WIP") or "WIP" in pr.title.upper():
            log.info("This is a WIP PR, so I won't disturb")
        else:
            thanks = random.choice(["Thanks", "Thank you"])
            pull = random.choice(["PR", "pull request"])
            comment = "{thanks} for submitting a {pr}! Maybe @{user} can review this?".format(
                thanks=thanks, pr=pull, user=pr.reviewer)
            self.comment(pr, comment)

    def review(self, pr):
        tom = self.username
        if tom not in pr.maintainers:
            return
        if tom in pr.approvals:
            return
        for person in pr.approvals:
            if person in pr.maintainers:
                log.info("Reviewing: {}".format(pr.title))
                log.info("Approved by: " + str(pr.approvals))
                body = "I trust @{}, approved!".format(person)
                event = "APPROVE"
                data = {"body": body, "event": event}
                self.post(pr.reviews_url, data)
                print("Approved PR: {}".format(pr.title))

    def comment_badge(self, pr, num, url):
        badge_icon = "{url}/buildStatus/icon?job={job}&build={num}".format(
            url=self.jenkins.url, job=self.jenkins.job_name, num=num)
        badge_link = "{url}/job/{job}/{num}/".format(
            url=self.jenkins.url, job=self.jenkins.job_name, num=num)
        badge = "[![Build Status]({})]({})".format(badge_icon, badge_link)
        response = random.choice(["Alright", "Sure"])
        new_comment = "{}, I triggered a build:\n\n{}\n\n{}".format(response, badge, url)
        self.comment(pr, new_comment)

    def trigger_build(self, pr, comment):
        prs = {}
        prs[pr.short_repo_name] = pr.number
        #TODO: allow pr numbers in comments

        if self.interactive:
            msg = []
            msg.append(str(comment))
            msg.append("Triger build for: {}".format(pr.title))
            msg.append("PRs: {}".format(prs))
            msg = "\n".join(msg)
            if not confirmation(msg):
                return

        headers, body = self.jenkins.trigger(prs, branch=pr.base_branch, title=pr.title)

        queue_url = headers["Location"]

        num, url = self.jenkins.wait_for_queue(queue_url)

        print("Triggered build ({}): {}".format(num, url))
        self.comment_badge(pr, num, url)

    def handle_mention(self, pr, comment):
        deny = "@{} : I'm sorry, I cannot do that. @olehermanse please help.".format(comment.author)
        if comment.author not in self.trusted:
            print("Denying mention from {}".format(comment.author))
            self.comment(pr, deny)
            return
        body = comment.body.lower()
        trigger_words = ["jenkins", "pipeline", "build", "trigger"]
        for word in trigger_words:
            if word.lower() in body:
                self.trigger_build(pr, comment)
                return

        no_comprendo = "I'm not sure I understand, @{}.".format(comment.author)
        self.comment(pr, no_comprendo)

    def handle_comments(self, pr):
        for comment in reversed(pr.comments):
            if comment.author == self.username:
                return
            if "@" + self.username in comment:
                self.handle_mention(pr, comment)

    def find_reviewers(self, pr):
        maintainers = self.default_maintainers
        if pr.repo in self.repo_maintainers:
            maintainers = self.repo_maintainers[pr.repo]

        pr.maintainers = maintainers
        pr.reviewers = copy(maintainers)
        if pr.author in pr.reviewers:
            pr.reviewers.remove(pr.author)
        if self.username in pr.reviewers:
            pr.reviewers.remove(self.username)
        if len(pr.reviewers) <= 0:
            pr.reviewers = self.default_maintainers
        pr.reviewer = random.choice(pr.reviewers)

    def handle_pr(self, pr):
        url = pr["url"].replace("https://api.github.com/repos/", "")
        log.info("Looking at: {} ({})".format(pr["title"], url))

        pr = PR(pr, self.github)
        self.find_reviewers(pr)
        self.ping_reviewer(pr)
        self.review(pr)
        self.handle_comments(pr)

    def run(self):
        self.repos = []
        if self.orgs:
            for org in self.orgs:
                self.repos += self.github.get("/orgs/{}/repos".format(org))

        self.repos = {repo["full_name"]: repo["url"] for repo in self.repos}
        for repo in self.repo_maintainers:
            self.repos[repo] = "/repos/" + repo

        self.pulls = []
        for repo, url in self.repos.items():
            log.info("Fetching pull requests for {}".format(repo))
            pulls = self.github.get(url + "/pulls")
            if pulls:
                self.pulls.extend(pulls)

        if self.pulls:
            log.info("Found {} open pull requests".format(len(self.pulls)))
        else:
            log.warning("Couldn't find any open pull requests!")

        for pull in self.pulls:
            self.handle_pr(pull)
        log.info("Tom successful")

    def talk(self):
        if not self.slack:
            sys.exit("Cannot start talk mode, Slack credentials missing")

        message = json.load(sys.stdin)

        log.debug(pretty(message))
        if 'token' not in message or message['token'] != self.slack_read_token:
            log.warning('Unauthorized message - ignoring')
            return
        if 'authed_users' in message and len(message['authed_users']) > 0:
            self.slack.my_username = message['authed_users'][0]
        message = message['event']
        if not 'user' in message:
            # not a user-generated message
            # probably a bot-generated message
            log.warning('Not a user message - ignoring')
            return
        self.slack.set_reply_to(message)
        self.dispatcher.parse_text(message['text'])