Skip to content
Permalink
Browse files
Merge pull request #7 from knaufk/FLINK-22034
[Flink-22034] Implement Rule 1 (and cleanup)
  • Loading branch information
knaufk committed Apr 22, 2021
2 parents 1bd0618 + 3fcb3ff commit dfb2fd88a3625d059a03f2b310395b5a58c452e2
Showing 8 changed files with 358 additions and 248 deletions.
@@ -17,3 +17,4 @@
################################################################################

venv
__pycache__
@@ -40,7 +40,12 @@ The configuration of the rules can be found in [config.yaml](config.yaml).

## About the Rules

### Rule 1 (not implemented yet)
### Rule 1 Major+ Need Assignee or Discussion

Tickets major and above need an assignee, or an update within {stale_<blocker|critical|major>.stale_days}, otherwise the priority will be reduced after a warning period of {stale_<blocker|critical|major>.warning_days} days.
An update of a Sub-Task counts as an update to the ticket.
Before this happens the assignee/reporter/watchers are notified that the ticket is about to become stale and will be deprioritized.
The time periods before warning differ based on the priority:

### Rule 2: Unassign Stale Assigned Tickets

@@ -32,3 +32,26 @@ stale_minor:
done_label: "auto-closed"
done_comment: 'This issue has been labeled "{warning_label}" for {warning_days} days. It is closed now. If you are still affected by this or would like to raise the priority of this ticket please re-open, removing the label "{done_label}" and raise the ticket priority accordingly.'

stale_blocker:
stale_days: 1
warning_days: 7
warning_label: "stale-blocker"
warning_comment: 'This Blockers is unassigned and itself and all of its Sub-Tasks have not been updated for {stale_days} days. So, it has been labeled "{warning_label}". If this ticket is a Blocker, please either assign yourself or give an update. Afterwards, please remove the label. In {warning_days} days the issue will be deprioritized.'
done_label: "auto-deprioritized-blocker"
done_comment: 'This issue has been labeled "{warning_label}" for {warning_days} days. It is deprioritized now. If this ticket is actually a Blocker, please raise the ticket priority again and assign yourself or revive the public discussion.'

stale_critical:
stale_days: 7
warning_days: 7
warning_label: "stale-critical"
warning_comment: 'This critical issue is unassigned and itself and all of its Sub-Tasks have not been updated for {stale_days} days. So, it has been labeled "{warning_label}". If this ticket is indeed critical, please either assign yourself or give an update. Afterwards, please remove the label. In {warning_days} days the issue will be deprioritized.'
done_label: "auto-deprioritized-critical"
done_comment: 'This issue has been labeled "{warning_label}" for {warning_days} days. It is deprioritized now. If this ticket is actually critical, please raise the ticket priority again and assign yourself or revive the public discussion.'

stale_major:
stale_days: 30
warning_days: 7
warning_label: "stale-major"
warning_comment: 'This major issue is unassigned and itself and all of its Sub-Tasks have not been updated for {stale_days} days. So, it has been labeled "{warning_label}". If this ticket is indeed "major", please either assign yourself or give an update. Afterwards, please remove the label. In {warning_days} days the issue will be deprioritized.'
done_label: "auto-deprioritized-major"
done_comment: 'This issue has been labeled "{warning_label}" for {warning_days} days. It is deprioritized now. If this ticket is actually "major", please raise the ticket priority again and assign yourself or revive the public discussion.'
@@ -20,252 +20,12 @@
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_major_or_above_rule import StaleMajorOrAboveRule
from stale_minor_rule import StaleMinorRule


def get_args():
@@ -302,7 +62,21 @@ def get_args():
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["stale_assigned"], args.dryrun
)
stale_minor_rule = StaleMinorRule(jira, jira_bot_config["stale_minor"], args.dryrun)
stale_major_rule = StaleMajorOrAboveRule(
jira, jira_bot_config["stale_major"], args.dryrun, "Major", "Minor"
)
stale_critical_rule = StaleMajorOrAboveRule(
jira, jira_bot_config["stale_critical"], args.dryrun, "Critical", "Major"
)
stale_blocker_rule = StaleMajorOrAboveRule(
jira, jira_bot_config["stale_blocker"], args.dryrun, "Blocker", "Critical"
)
stale_assigned_rule.run()
stale_minor_rule.run()
stale_major_rule.run()
stale_critical_rule.run()
stale_blocker_rule.run()

0 comments on commit dfb2fd8

Please sign in to comment.