From f84b72c248fe01f136aecef5e06786ce632daf1a Mon Sep 17 00:00:00 2001 From: Ralph Bean Date: Thu, 17 Oct 2024 21:50:32 -0400 Subject: [PATCH] Introduce new feature ranking scheme for konflux --- config/konflux.yaml | 4 + src/rules/team/__init__.py | 2 + src/rules/team/fixversion_rank.py | 167 ++++++++++++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 src/rules/team/fixversion_rank.py diff --git a/config/konflux.yaml b/config/konflux.yaml index 1a3f407..3076e3c 100644 --- a/config/konflux.yaml +++ b/config/konflux.yaml @@ -23,6 +23,10 @@ program_automation: - check_target_end_date team_automation: issues: + Feature: + # collector: get_issues + group_rules: + - check_fixversion_rank Epic: collector: get_child_issues rules: diff --git a/src/rules/team/__init__.py b/src/rules/team/__init__.py index 010e60e..422a80f 100644 --- a/src/rules/team/__init__.py +++ b/src/rules/team/__init__.py @@ -1,4 +1,5 @@ from .due_date import check_due_date +from .fixversion_rank import check_fixversion_rank from .outcome_version import set_fix_version from .parent import check_parent_link from .priority import check_priority @@ -16,4 +17,5 @@ check_rank, check_target_dates, check_timesensitive_rank, + check_fixversion_rank, ] diff --git a/src/rules/team/fixversion_rank.py b/src/rules/team/fixversion_rank.py new file mode 100644 index 0000000..469653a --- /dev/null +++ b/src/rules/team/fixversion_rank.py @@ -0,0 +1,167 @@ +""" +The reranking here prioritizes items with a fixVersion, but otherwise tries to +maintain the existing ordering within a project. +""" + +import datetime +import difflib + +import jira + + +def check_fixversion_rank( + issues: list[jira.resources.Issue], + context: dict, + dry_run: bool, +) -> None: + """Rerank all issues""" + jira_client = context["jira_client"] + + # Get blocks and current ranking + blocks = Blocks(issues) + old_ranking = issues + + # Sort blocks and generate new ranking + blocks.sort() + new_ranking = blocks.get_issues() + + # Apply new ranking + _set_rank(jira_client, old_ranking, new_ranking, dry_run) + + +def _set_rank( + jira_client: jira.client.JIRA, + old_ranking: list[jira.resources.Issue], + new_ranking: list[jira.resources.Issue], + dry_run: bool, +) -> None: + print(f"\n### Reranking issues ({__name__})") + previous_issue = None + total = len(new_ranking) + rerank = False + + print( + "".join( + list( + difflib.unified_diff( + [f"{issue.key} {issue.fields.summary}\n" for issue in old_ranking], + [f"{issue.key} {issue.fields.summary}\n" for issue in new_ranking], + "old_ranking", + "new_ranking", + ) + ) + ) + ) + + for index, issue in enumerate(new_ranking): + if issue != old_ranking[index]: + # Once we start reranking, we don't stop. + # This should avoid any edge case, but it's slow. + rerank = True + if rerank and previous_issue is not None: + if dry_run: + print(f" > {issue.key}") + else: + jira_client.rank(issue=issue.key, prev_issue=previous_issue.key) + previous_issue = issue + print(f"\r{100 * (index + 1) // total}%", end="", flush=True) + + +class Block: + """A block groups issues""" + + def __repr__(self): + return f", containing {len(self.issues)} issues>" + + def __init__(self): + self.issues = [] + + def yield_issues(self): + yield from self.issues + + @property + def rank(self): + return 1 + + def claims(self, issue) -> bool: + return not FixVersionBlock._claims(issue) + + +class FixVersionBlock(Block): + """A special-case block that gets ranked to the top""" + + def yield_issues(self): + yield from sorted(self.issues, key=self._earliest_fixversion_date) + + @property + def rank(self): + return float("-inf") + + def claims(self, issue) -> bool: + return self._claims(issue) + + @staticmethod + def _earliest_fixversion_date(issue): + fixversions = issue.fields.fixVersions + if not fixversions: + return None + dates = [ + fixversion.releaseDate + for fixversion in fixversions + if hasattr(fixversion, "releaseDate") + ] + if not dates: + return None + return sorted(dates)[0] + + @staticmethod + def _claims(issue) -> bool: + critical_deadline = ( + datetime.datetime.today() + datetime.timedelta(days=30 * 3) + ).strftime("%Y-%m-%d") + date = FixVersionBlock._earliest_fixversion_date(issue) + return date and date < critical_deadline + + +class Blocks(list): + def __init__(self, issues: list[jira.resources.Issue]) -> None: + self.blocks = [FixVersionBlock()] + for issue in issues: + self.add_issue(issue) + + def add_issue(self, issue: jira.resources.Issue) -> None: + """Add an issue to the right block""" + block = None + addBlock = True + for block in self.blocks: + if block.claims(issue): + addBlock = False + break + if addBlock: + block = Block() + self.blocks.append(block) + block.issues.append(issue) + + def get_issues(self) -> list[jira.resources.Issue]: + """Return a flat list of issues, in the order of appearance in the blocks""" + issues = [] + for block in self.blocks: + for issue in block.yield_issues(): + issues.append(issue) + return issues + + def sort(self): + self._prioritize_fixversion_block() + + def _prioritize_fixversion_block(self): + """Issues tied to fixVersions rise to the top of the list.""" + fixversion = [] + other = [] + + for block in self.blocks: + if type(block) is FixVersionBlock: + fixversion.append(block) + else: + other.append(block) + + self.blocks = fixversion + other