| ################################################################################ |
| # 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.is_dry_run = is_dry_run |
| self.stale_days = config["stale_days"].get() |
| self.warning_days = config["warning_days"].get() |
| self.warning_label = config["warning_label"].get() |
| self.done_label = config["done_label"].get() |
| self.done_comment = config["done_comment"].get() |
| self.warning_comment = config["warning_comment"].get() |
| |
| 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.") |
| |
| def set_priority(self, key, priority): |
| if not self.is_dry_run: |
| fields = {"priority": {"name": priority}} |
| self.jira_client.update_issue_field(key, fields) |
| else: |
| logging.info(f"DRY_RUN (({key})): Setting to {priority}") |
| |
| @abc.abstractmethod |
| def run(self): |
| return |
| |
| @abc.abstractmethod |
| def handle_stale_ticket(self, key): |
| return |
| |
| def mark_stale_tickets_stale(self, jql_query): |
| |
| logging.info(f"Looking for stale tickets.") |
| issues = self.get_issues(jql_query) |
| |
| 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." |
| ) |
| |
| def handle_tickets_marked_stale(self, jql_query): |
| logging.info(f"Looking for ticket previously marked as {self.warning_label}.") |
| issues = self.get_issues(jql_query) |
| |
| for issue in issues: |
| key = issue["key"] |
| logging.info( |
| f"Found https://issues.apache.org/jira/browse/{key}. It is now processed as stale." |
| ) |
| |
| 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.handle_stale_ticket(key) |