This is an automated email from the ASF dual-hosted git repository. knaufk pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/flink-jira-bot.git
commit 425f75f6f56e35e3fe91a7f2804c02e26fd895f3 Author: Konstantin Knauf <[email protected]> AuthorDate: Wed Apr 21 11:16:15 2021 +0200 [hotfix] give rule classes more descriptive names and refactor to separate files --- .gitignore | 1 + flink_jira_bot.py | 253 ++----------------------------------------------- flink_jira_rule.py | 103 ++++++++++++++++++++ stale_assigned_rule.py | 101 ++++++++++++++++++++ stale_minor_rule.py | 101 ++++++++++++++++++++ 5 files changed, 312 insertions(+), 247 deletions(-) diff --git a/.gitignore b/.gitignore index 4b8ee1e..2d85626 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ ################################################################################ venv +__pycache__ diff --git a/flink_jira_bot.py b/flink_jira_bot.py index 8b46132..211f855 100644 --- a/flink_jira_bot.py +++ b/flink_jira_bot.py @@ -20,252 +20,11 @@ from atlassian import Jira import logging import confuse import os -import abc from argparse import ArgumentParser from pathlib import Path - -class FlinkJiraRule: - __metaclass__ = abc.ABCMeta - - def __init__(self, jira_client, config, is_dry_run): - self.jira_client = jira_client - self.config = config - self.is_dry_run = is_dry_run - - def get_issues(self, jql_query): - """Queries the JIRA PI for all issues that match the given JQL Query - - This method is necessary as requests tend to time out if the number of results reaches a certain number. - So, this method requests the results in multiple queries and returns a final list of all issues. - :param jql_query: the search query - :return: a list of issues matching the query - """ - limit = 200 - current = 0 - total = 1 - issues = [] - while current < total: - response = self.jira_client.jql(jql_query, limit=limit, start=current) - total = response["total"] - issues = issues + response["issues"] - current = len(issues) - logging.info(f'"{jql_query}" returned {len(issues)} issues') - return issues - - def has_recently_updated_subtask(self, parent, updated_within_days): - find_subtasks_updated_within = ( - f"parent = {parent} AND updated > startOfDay(-{updated_within_days}d)" - ) - issues = self.get_issues(find_subtasks_updated_within) - return len(issues) > 0 - - def add_label(self, issue, label): - labels = issue["fields"]["labels"] + [label] - fields = {"labels": labels} - key = issue["key"] - - if not self.is_dry_run: - self.jira_client.update_issue_field(key, fields) - else: - logging.info(f'DRY RUN ({key}): Adding label "{label}".') - - def replace_label(self, issue, old_label, new_label): - labels = issue["fields"]["labels"] + [new_label] - labels.remove(old_label) - fields = {"labels": labels} - key = issue["key"] - - if not self.is_dry_run: - self.jira_client.update_issue_field(key, fields) - else: - logging.info( - f'DRY RUN ({key}): Replace label "{old_label}" for "{new_label}".' - ) - - def add_comment(self, key, comment): - if not self.is_dry_run: - self.jira_client.issue_add_comment(key, comment) - else: - logging.info(f'DRY_RUN ({key}): Adding comment "{comment}".') - - def close_issue(self, key): - if not self.is_dry_run: - self.jira_client.set_issue_status( - key, "Closed", fields={"resolution": {"name": "Auto Closed"}} - ) - else: - logging.info(f"DRY_RUN (({key})): Closing.") - - def unassign(self, key): - if not self.is_dry_run: - self.jira_client.assign_issue(key, None) - else: - logging.info(f"DRY_RUN (({key})): Unassigning.") - - @abc.abstractmethod - def run(self): - return - - -class Rule3(FlinkJiraRule): - """ - An unresolved Minor ticket without an update for {stale_minor.stale_days} is closed after a warning period of - {stale_minor.warning_days} with a comment that encourages users to watch, comment and simply reopen with a higher - priority if the problem insists. - """ - - def __init__(self, jira_client, config, is_dry_run): - super().__init__(jira_client, config, is_dry_run) - self.stale_days = config["stale_minor"]["stale_days"].get() - self.warning_days = config["stale_minor"]["warning_days"].get() - self.warning_label = config["stale_minor"]["warning_label"].get() - self.done_label = config["stale_minor"]["done_label"].get() - self.done_comment = config["stale_minor"]["done_comment"].get() - self.warning_comment = config["stale_minor"]["warning_comment"].get() - - def run(self): - self.close_tickets_marked_stale() - self.mark_stale_tickets_stale() - - def close_tickets_marked_stale(self): - - minor_tickets_marked_stale = ( - f"project=FLINK AND Priority = Minor AND resolution = Unresolved AND labels in " - f'("{self.warning_label}") AND updated < startOfDay(-{self.warning_days}d)' - ) - logging.info( - f"Looking for minor tickets, which were previously marked as {self.warning_label}." - ) - issues = self.get_issues(minor_tickets_marked_stale) - - for issue in issues: - key = issue["key"] - logging.info( - f"Found https://issues.apache.org/jira/browse/{key}. It is now closed due to inactivity." - ) - - formatted_comment = self.done_comment.format( - warning_days=self.warning_days, - warning_label=self.warning_label, - done_label=self.done_label, - ) - - self.add_comment(key, formatted_comment) - self.replace_label(issue, self.warning_label, self.done_label) - self.close_issue(key) - - def mark_stale_tickets_stale(self): - - stale_minor_tickets = ( - f"project = FLINK AND Priority = Minor AND resolution = Unresolved AND updated < " - f"startOfDay(-{self.stale_days}d)" - ) - logging.info(f"Looking for minor tickets, which are stale.") - issues = self.get_issues(stale_minor_tickets) - - for issue in issues: - key = issue["key"] - issue = self.jira_client.get_issue(key) - - if not self.has_recently_updated_subtask(key, self.stale_days): - logging.info( - f"Found https://issues.apache.org/jira/browse/{key}. It is marked stale now." - ) - formatted_comment = self.warning_comment.format( - stale_days=self.stale_days, - warning_days=self.warning_days, - warning_label=self.warning_label, - ) - - self.add_label(issue, self.warning_label) - self.add_comment(key, formatted_comment) - - else: - logging.info( - f"Found https://issues.apache.org/jira/browse/{key}, but is has recently updated Subtasks. " - f"Ignoring for now." - ) - - -class Rule2(FlinkJiraRule): - """ - Assigned tickets without an update for {stale_assigned.stale_days} are unassigned after a warning period of - {stale_assigned.warning_days}. Before this happens the assignee is notified that this is about to happen and - asked for an update on the status of her contribution. - """ - - def __init__(self, jira_client, config, is_dry_run): - super().__init__(jira_client, config, is_dry_run) - self.stale_days = config["stale_assigned"]["stale_days"].get() - self.warning_days = config["stale_assigned"]["warning_days"].get() - self.warning_label = config["stale_assigned"]["warning_label"].get() - self.done_label = config["stale_assigned"]["done_label"].get() - self.done_comment = config["stale_assigned"]["done_comment"].get() - self.warning_comment = config["stale_assigned"]["warning_comment"].get() - - def run(self): - self.unassign_tickets_marked_stale() - self.mark_stale_tickets_stale() - - def unassign_tickets_marked_stale(self): - - assigned_tickets_marked_stale = ( - f"project=FLINK AND resolution = Unresolved AND labels in " - f'("{self.warning_label}") AND updated < startOfDay(-{self.warning_days}d)' - ) - logging.info( - f"Looking for assigned tickets, which were previously marked as {self.warning_label}." - ) - issues = self.get_issues(assigned_tickets_marked_stale) - - for issue in issues: - key = issue["key"] - logging.info( - f"Found https://issues.apache.org/jira/browse/{key}. It is now unassigned due to inactivity." - ) - - formatted_comment = self.done_comment.format( - warning_days=self.warning_days, - warning_label=self.warning_label, - done_label=self.done_label, - ) - - self.add_comment(key, formatted_comment) - self.replace_label(issue, self.warning_label, self.done_label) - self.unassign(key) - - def mark_stale_tickets_stale(self): - - stale_assigned_tickets = ( - f"project = FLINK AND resolution = Unresolved AND assignee is not EMPTY AND updated < " - f"startOfDay(-{self.stale_days}d)" - ) - logging.info(f"Looking for assigned tickets, which are stale.") - issues = self.get_issues(stale_assigned_tickets) - - for issue in issues: - key = issue["key"] - issue = self.jira_client.get_issue(key) - - if not self.has_recently_updated_subtask(key, self.stale_days): - logging.info( - f"Found https://issues.apache.org/jira/browse/{key}. It is marked stale now." - ) - formatted_comment = self.warning_comment.format( - stale_days=self.stale_days, - warning_days=self.warning_days, - warning_label=self.warning_label, - ) - - self.add_label(issue, self.warning_label) - self.add_comment(key, formatted_comment) - - else: - logging.info( - f"Found https://issues.apache.org/jira/browse/{key}, but is has recently updated Subtasks. " - f"Ignoring for now." - ) +from stale_assigned_rule import StaleAssignedRule +from stale_minor_rule import StaleMinorRule def get_args(): @@ -302,7 +61,7 @@ if __name__ == "__main__": password=os.environ["JIRA_PASSWORD"], ) - rule_2 = Rule2(jira, jira_bot_config, args.dryrun) - rule_3 = Rule3(jira, jira_bot_config, args.dryrun) - rule_2.run() - rule_3.run() + stale_assigned_rule = StaleAssignedRule(jira, jira_bot_config, args.dryrun) + stale_minor_rule = StaleMinorRule(jira, jira_bot_config, args.dryrun) + stale_assigned_rule.run() + stale_minor_rule.run() diff --git a/flink_jira_rule.py b/flink_jira_rule.py new file mode 100644 index 0000000..9960aca --- /dev/null +++ b/flink_jira_rule.py @@ -0,0 +1,103 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +import abc +import logging + + +class FlinkJiraRule: + __metaclass__ = abc.ABCMeta + + def __init__(self, jira_client, config, is_dry_run): + self.jira_client = jira_client + self.config = config + self.is_dry_run = is_dry_run + + def get_issues(self, jql_query): + """Queries the JIRA PI for all issues that match the given JQL Query + + This method is necessary as requests tend to time out if the number of results reaches a certain number. + So, this method requests the results in multiple queries and returns a final list of all issues. + :param jql_query: the search query + :return: a list of issues matching the query + """ + limit = 200 + current = 0 + total = 1 + issues = [] + while current < total: + response = self.jira_client.jql(jql_query, limit=limit, start=current) + total = response["total"] + issues = issues + response["issues"] + current = len(issues) + logging.info(f'"{jql_query}" returned {len(issues)} issues') + return issues + + def has_recently_updated_subtask(self, parent, updated_within_days): + find_subtasks_updated_within = ( + f"parent = {parent} AND updated > startOfDay(-{updated_within_days}d)" + ) + issues = self.get_issues(find_subtasks_updated_within) + return len(issues) > 0 + + def add_label(self, issue, label): + labels = issue["fields"]["labels"] + [label] + fields = {"labels": labels} + key = issue["key"] + + if not self.is_dry_run: + self.jira_client.update_issue_field(key, fields) + else: + logging.info(f'DRY RUN ({key}): Adding label "{label}".') + + def replace_label(self, issue, old_label, new_label): + labels = issue["fields"]["labels"] + [new_label] + labels.remove(old_label) + fields = {"labels": labels} + key = issue["key"] + + if not self.is_dry_run: + self.jira_client.update_issue_field(key, fields) + else: + logging.info( + f'DRY RUN ({key}): Replace label "{old_label}" for "{new_label}".' + ) + + def add_comment(self, key, comment): + if not self.is_dry_run: + self.jira_client.issue_add_comment(key, comment) + else: + logging.info(f'DRY_RUN ({key}): Adding comment "{comment}".') + + def close_issue(self, key): + if not self.is_dry_run: + self.jira_client.set_issue_status( + key, "Closed", fields={"resolution": {"name": "Auto Closed"}} + ) + else: + logging.info(f"DRY_RUN (({key})): Closing.") + + def unassign(self, key): + if not self.is_dry_run: + self.jira_client.assign_issue(key, None) + else: + logging.info(f"DRY_RUN (({key})): Unassigning.") + + @abc.abstractmethod + def run(self): + return diff --git a/stale_assigned_rule.py b/stale_assigned_rule.py new file mode 100644 index 0000000..e781a9d --- /dev/null +++ b/stale_assigned_rule.py @@ -0,0 +1,101 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +import logging + +from flink_jira_rule import FlinkJiraRule + + +class StaleAssignedRule(FlinkJiraRule): + """ + Assigned tickets without an update for {stale_assigned.stale_days} are unassigned after a warning period of + {stale_assigned.warning_days}. Before this happens the assignee is notified that this is about to happen and + asked for an update on the status of her contribution. + """ + + def __init__(self, jira_client, config, is_dry_run): + super().__init__(jira_client, config, is_dry_run) + self.stale_days = config["stale_assigned"]["stale_days"].get() + self.warning_days = config["stale_assigned"]["warning_days"].get() + self.warning_label = config["stale_assigned"]["warning_label"].get() + self.done_label = config["stale_assigned"]["done_label"].get() + self.done_comment = config["stale_assigned"]["done_comment"].get() + self.warning_comment = config["stale_assigned"]["warning_comment"].get() + + def run(self): + self.unassign_tickets_marked_stale() + self.mark_stale_tickets_stale() + + def unassign_tickets_marked_stale(self): + + assigned_tickets_marked_stale = ( + f"project=FLINK AND resolution = Unresolved AND labels in " + f'("{self.warning_label}") AND updated < startOfDay(-{self.warning_days}d)' + ) + logging.info( + f"Looking for assigned tickets, which were previously marked as {self.warning_label}." + ) + issues = self.get_issues(assigned_tickets_marked_stale) + + for issue in issues: + key = issue["key"] + logging.info( + f"Found https://issues.apache.org/jira/browse/{key}. It is now unassigned due to inactivity." + ) + + formatted_comment = self.done_comment.format( + warning_days=self.warning_days, + warning_label=self.warning_label, + done_label=self.done_label, + ) + + self.add_comment(key, formatted_comment) + self.replace_label(issue, self.warning_label, self.done_label) + self.unassign(key) + + def mark_stale_tickets_stale(self): + + stale_assigned_tickets = ( + f"project = FLINK AND resolution = Unresolved AND assignee is not EMPTY AND updated < " + f"startOfDay(-{self.stale_days}d)" + ) + logging.info(f"Looking for assigned tickets, which are stale.") + issues = self.get_issues(stale_assigned_tickets) + + for issue in issues: + key = issue["key"] + issue = self.jira_client.get_issue(key) + + if not self.has_recently_updated_subtask(key, self.stale_days): + logging.info( + f"Found https://issues.apache.org/jira/browse/{key}. It is marked stale now." + ) + formatted_comment = self.warning_comment.format( + stale_days=self.stale_days, + warning_days=self.warning_days, + warning_label=self.warning_label, + ) + + self.add_label(issue, self.warning_label) + self.add_comment(key, formatted_comment) + + else: + logging.info( + f"Found https://issues.apache.org/jira/browse/{key}, but is has recently updated Subtasks. " + f"Ignoring for now." + ) diff --git a/stale_minor_rule.py b/stale_minor_rule.py new file mode 100644 index 0000000..78f7b8d --- /dev/null +++ b/stale_minor_rule.py @@ -0,0 +1,101 @@ +################################################################################ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +################################################################################ + +import logging + +from flink_jira_rule import FlinkJiraRule + + +class StaleMinorRule(FlinkJiraRule): + """ + An unresolved Minor ticket without an update for {stale_minor.stale_days} is closed after a warning period of + {stale_minor.warning_days} with a comment that encourages users to watch, comment and simply reopen with a higher + priority if the problem insists. + """ + + def __init__(self, jira_client, config, is_dry_run): + super().__init__(jira_client, config, is_dry_run) + self.stale_days = config["stale_minor"]["stale_days"].get() + self.warning_days = config["stale_minor"]["warning_days"].get() + self.warning_label = config["stale_minor"]["warning_label"].get() + self.done_label = config["stale_minor"]["done_label"].get() + self.done_comment = config["stale_minor"]["done_comment"].get() + self.warning_comment = config["stale_minor"]["warning_comment"].get() + + def run(self): + self.close_tickets_marked_stale() + self.mark_stale_tickets_stale() + + def close_tickets_marked_stale(self): + + minor_tickets_marked_stale = ( + f"project=FLINK AND Priority = Minor AND resolution = Unresolved AND labels in " + f'("{self.warning_label}") AND updated < startOfDay(-{self.warning_days}d)' + ) + logging.info( + f"Looking for minor tickets, which were previously marked as {self.warning_label}." + ) + issues = self.get_issues(minor_tickets_marked_stale) + + for issue in issues: + key = issue["key"] + logging.info( + f"Found https://issues.apache.org/jira/browse/{key}. It is now closed due to inactivity." + ) + + formatted_comment = self.done_comment.format( + warning_days=self.warning_days, + warning_label=self.warning_label, + done_label=self.done_label, + ) + + self.add_comment(key, formatted_comment) + self.replace_label(issue, self.warning_label, self.done_label) + self.close_issue(key) + + def mark_stale_tickets_stale(self): + + stale_minor_tickets = ( + f"project = FLINK AND Priority = Minor AND resolution = Unresolved AND updated < " + f"startOfDay(-{self.stale_days}d)" + ) + logging.info(f"Looking for minor tickets, which are stale.") + issues = self.get_issues(stale_minor_tickets) + + for issue in issues: + key = issue["key"] + issue = self.jira_client.get_issue(key) + + if not self.has_recently_updated_subtask(key, self.stale_days): + logging.info( + f"Found https://issues.apache.org/jira/browse/{key}. It is marked stale now." + ) + formatted_comment = self.warning_comment.format( + stale_days=self.stale_days, + warning_days=self.warning_days, + warning_label=self.warning_label, + ) + + self.add_label(issue, self.warning_label) + self.add_comment(key, formatted_comment) + + else: + logging.info( + f"Found https://issues.apache.org/jira/browse/{key}, but is has recently updated Subtasks. " + f"Ignoring for now." + )
