diff --git a/elliottlib/cli/find_bugs_kernel_cli.py b/elliottlib/cli/find_bugs_kernel_cli.py index a5896fe4..e34931f7 100644 --- a/elliottlib/cli/find_bugs_kernel_cli.py +++ b/elliottlib/cli/find_bugs_kernel_cli.py @@ -9,7 +9,7 @@ from jira import JIRA, Issue from tenacity import retry, stop_after_attempt -from elliottlib import Runtime, brew +from elliottlib import Runtime, brew, early_kernel from elliottlib.assembly import AssemblyTypes from elliottlib.cli.common import cli, click_coroutine from elliottlib.config_model import KernelBugSweepConfig @@ -25,13 +25,13 @@ def _search_issues(jira_client, *args, **kwargs): class FindBugsKernelCli: def __init__(self, runtime: Runtime, trackers: Sequence[str], - clone: bool, reconcile: bool, comment: bool, dry_run: bool): + clone: bool, reconcile: bool, update_tracker: bool, dry_run: bool): self._runtime = runtime self._logger = runtime.logger self.trackers = list(trackers) self.clone = clone self.reconcile = reconcile - self.comment = comment + self.update_tracker = update_tracker self.dry_run = dry_run self._id_bugs: Dict[int, Bug] = {} # cache for kernel bug; key is bug_id, value is Bug object self._tracker_map: Dict[int, Issue] = {} # bug_id -> KMAINT jira mapping @@ -57,17 +57,17 @@ async def run(self): # Getting KMAINT trackers trackers_keys = self.trackers trackers: List[Issue] = [] - if not trackers_keys: - logger.info("Searching for open trackers...") - trackers = self._find_kmaint_trackers(jira_client, config.tracker_jira.project, config.tracker_jira.labels) - trackers_keys = [t.key for t in trackers] - logger.info("Found %s tracker(s): %s", len(trackers_keys), trackers_keys) - else: + if trackers_keys: logger.info("Find kernel bugs linked from KMAINT tracker(s): %s", trackers_keys) for key in trackers_keys: logger.info("Getting tracker JIRA %s...", key) tracker = jira_client.issue(key) trackers.append(tracker) + else: + logger.info("Searching for open trackers...") + trackers = self._find_kmaint_trackers(jira_client, config.tracker_jira.project, config.tracker_jira.labels) + trackers_keys = [t.key for t in trackers] + logger.info("Found %s tracker(s): %s", len(trackers_keys), trackers_keys) # Get kernel bugs linked from KMAINT trackers report: Dict[str, Any] = {"kernel_bugs": []} @@ -86,9 +86,8 @@ async def run(self): "summary": bug.summary, "tracker": tracker, }) - if self.comment: - logger.info("Checking if making a comment on tracker %s is needed...", tracker.key) - self._comment_on_tracker(jira_client, tracker, koji_api, config.target_jira) + if self.update_tracker: + self._update_tracker(jira_client, tracker, koji_api, config.target_jira) if self.clone and self._id_bugs: # Clone kernel bugs into OCP Jira @@ -175,7 +174,7 @@ def _clone_bugs(self, jira_client: JIRA, bugs: Sequence[Bug], conf: KernelBugSwe jira_client.create_issue_link("Blocks", issue.key, kmaint_tracker) result[bug_id] = [issue] else: - logger.warning("[DRY RUN] Would have created Jira for bug %s", bug_id) + logger.info("[DRY RUN] Would have created Jira for bug %s", bug_id) else: # this bug is already cloned into OCP Jira logger.info("Bug %s is already cloned into OCP: %s", bug_id, [issue.key for issue in found_issues]) result[bug_id] = found_issues @@ -190,7 +189,7 @@ def _clone_bugs(self, jira_client: JIRA, bugs: Sequence[Bug], conf: KernelBugSwe if not self.dry_run: issue.update(fields) else: - logger.warning("[DRY RUN] Would have updated Jira %s to match bug %s", issue.key, bug_id) + logger.info("[DRY RUN] Would have updated Jira %s to match bug %s", issue.key, bug_id) return result @@ -204,39 +203,24 @@ def _print_report(report: Dict, out: TextIO): text = f"{bug['tracker']}\t{bug['id']}\t{'N/A' if not cloned_issues else ','.join(cloned_issues)}\t{bug['status']}\t{bug['summary']}" print_func(text, file=out) - def _comment_on_tracker(self, jira_client: JIRA, tracker: Issue, koji_api: koji.ClientSession, - conf: KernelBugSweepConfig.TargetJiraConfig): + def _update_tracker(self, jira_client: JIRA, tracker: Issue, koji_api: koji.ClientSession, + conf: KernelBugSweepConfig.TargetJiraConfig): logger = self._runtime.logger + logger.info("Checking if an update to tracker %s is needed...", tracker.key) # Determine which NVRs have the fix. e.g. ["kernel-5.14.0-284.14.1.el9_2"] - nvrs = re.findall(r"(kernel(?:-rt)?-\S+-\S+)", tracker.fields.summary) - if not nvrs: - raise ValueError("Couldn't determine build NVRs for tracker %s", tracker.key) - nvrs = sorted(nvrs) - # Check if nvrs are already tagged into OCP - logger.info("Getting Brew tags for build(s) %s...", nvrs) - candidate_brew_tag = conf.candidate_brew_tag - prod_brew_tag = conf.prod_brew_tag - build_tags = brew.get_builds_tags(nvrs, koji_api) - shipped = all([any(map(lambda t: t["name"] == prod_brew_tag, tags)) for tags in build_tags]) - modified = all([any(map(lambda t: t["name"] == candidate_brew_tag, tags)) for tags in build_tags]) - tracker_message = None + nvrs, candidate, shipped = early_kernel.get_tracker_builds_and_tags(logger, tracker, koji_api, conf) + if shipped: - tracker_message = f"Build(s) {nvrs} was/were already shipped and tagged into {prod_brew_tag}." - elif modified: - tracker_message = f"Build(s) {nvrs} was/were already tagged into {candidate_brew_tag}." - if not tracker_message: - logger.info("No need to make a comment on %s", tracker.key) - return - comments = jira_client.comments(tracker.key) - if any(map(lambda comment: comment.body == tracker_message, comments)): - logger.info("A comment was already made on %s", tracker.key) - return - logger.info("Making a comment on tracker %s", tracker.key) - if not self.dry_run: - jira_client.add_comment(tracker.key, tracker_message) - logger.info("Left a comment on tracker %s", tracker.key) + early_kernel.process_shipped_tracker(logger, self.dry_run, jira_client, tracker, nvrs, shipped) + elif candidate: + early_kernel.comment_on_tracker( + logger, self.dry_run, jira_client, tracker, + [f"Build(s) {nvrs} was/were already tagged into {candidate}."] + # do not reword, see NOTE in method + ) else: - logger.warning("[DRY RUN] Would have left a comment on tracker %s", tracker.key) + logger.info("No need to update tracker %s", tracker.key) + return @staticmethod def _new_jira_fields_from_bug(bug: Bug, ocp_target_version: str, kmaint_tracker: Optional[str], conf: KernelBugSweepConfig.TargetJiraConfig): @@ -303,10 +287,10 @@ def _new_jira_fields_from_bug(bug: Bug, ocp_target_version: str, kmaint_tracker: is_flag=True, default=False, help="Update summary, description, etc for already cloned Jira bugs. Must be used with --clone") -@click.option("--comment", +@click.option("--update-tracker", is_flag=True, default=False, - help="Make comments on KMAINT trackers") + help="Update KMAINT trackers state, links, and comments") @click.option("--dry-run", is_flag=True, default=False, @@ -315,7 +299,7 @@ def _new_jira_fields_from_bug(bug: Bug, ocp_target_version: str, kmaint_tracker: @click_coroutine async def find_bugs_kernel_cli( runtime: Runtime, trackers: Tuple[str, ...], clone: bool, - reconcile: bool, comment: bool, dry_run: bool): + reconcile: bool, update_tracker: bool, dry_run: bool): """Find kernel bugs in Bugzilla for weekly kernel release through OCP. Example 1: Find kernel bugs and print them out @@ -336,7 +320,7 @@ async def find_bugs_kernel_cli( trackers=trackers, clone=clone, reconcile=reconcile, - comment=comment, + update_tracker=update_tracker, dry_run=dry_run ) await cli.run() diff --git a/elliottlib/cli/find_bugs_kernel_clones_cli.py b/elliottlib/cli/find_bugs_kernel_clones_cli.py index 6aa28a3e..153ecb33 100644 --- a/elliottlib/cli/find_bugs_kernel_clones_cli.py +++ b/elliottlib/cli/find_bugs_kernel_clones_cli.py @@ -7,7 +7,7 @@ from jira import JIRA, Issue from tenacity import retry, stop_after_attempt, wait_fixed -from elliottlib import Runtime, brew +from elliottlib import Runtime, brew, early_kernel from elliottlib.assembly import AssemblyTypes from elliottlib.cli.common import cli, click_coroutine from elliottlib.config_model import KernelBugSweepConfig @@ -16,21 +16,28 @@ from elliottlib.bzutil import JIRABugTracker +# [lmeyer] I like terms to distinguish between the two types of Jira issues we deal with here. +# trackers: the KMAINT issues the kernel team creates for tracking these special releases. +# bugs: OCPBUGS issues that clone the actual kernel bugs driving the need for a special OCP build. +# issues: when we are dealing with Jira issues generically that may be one or the other. +# Historically, bugs were called "issues" here and so they are still called this in command I/Os. + + class FindBugsKernelClonesCli: - def __init__(self, runtime: Runtime, trackers: Sequence[str], issues: Sequence[str], - move: bool, comment: bool, dry_run: bool): + def __init__(self, runtime: Runtime, trackers: Sequence[str], bugs: Sequence[str], + move: bool, update_tracker: bool, dry_run: bool): self._runtime = runtime self._logger = runtime.logger self.trackers = list(trackers) - self.issues = list(issues) + self.bugs = list(bugs) self.move = move - self.comment = comment + self.update_tracker = update_tracker self.dry_run = dry_run - async def run(self): + def run(self): logger = self._logger - if self.comment and not self.move: - raise ElliottFatalError("--comment must be used with --move") + if self.update_tracker and not self.move: + raise ElliottFatalError("--update-tracker must be used with --move") if self._runtime.assembly_type is not AssemblyTypes.STREAM: raise ElliottFatalError("This command only supports stream assembly.") group_config = self._runtime.group_config @@ -45,52 +52,54 @@ async def run(self): # Search for Jiras report = {"jira_issues": []} - if self.issues: - logger.info("Getting specified Jira issues %s...", self.issues) - found_issues = self._get_jira_issues(jira_client, self.issues, config) + if self.bugs: + logger.info("Getting specified Jira bugs %s...", self.bugs) + found_bugs = self._get_jira_bugs(jira_client, self.bugs, config) else: logger.info("Searching for bug clones in Jira project %s...", config.target_jira.project) - found_issues = self._search_for_jira_issues(jira_client, self.trackers, config) - issue_keys = [issue.key for issue in found_issues] - logger.info("Found %s Jira(s) in %s: %s", len(issue_keys), config.target_jira.project, issue_keys) + found_bugs = self._search_for_jira_bugs(jira_client, self.trackers, config) + bug_keys = [bug.key for bug in found_bugs] + logger.info("Found %s Jira(s) in %s: %s", len(bug_keys), config.target_jira.project, bug_keys) - # Update JIRA issues - if self.move and found_issues: + # Update JIRA bugs + if self.move and found_bugs: logger.info("Moving bug clones...") - self._update_jira_issues(jira_client, found_issues, koji_api, config) + self._update_jira_bugs(jira_client, found_bugs, koji_api, config) logger.info("Done.") # Print a report report["jira_issues"] = [{ - "key": issue.key, - "summary": issue.fields.summary, - "status": str(issue.fields.status.name), - } for issue in found_issues] + "key": bug.key, + "summary": bug.fields.summary, + "status": str(bug.fields.status.name), + } for bug in found_bugs] self._print_report(report, sys.stdout) - def _get_jira_issues(self, jira_client: JIRA, issue_keys: List[str], config: KernelBugSweepConfig): - found_issues: List[Issue] = [] + def _get_jira_bugs(self, jira_client: JIRA, bug_keys: List[str], config: KernelBugSweepConfig): + # get a specified list of jira bugs we created previously as clones of the original kernel bugs + found_bugs: List[Issue] = [] labels = {"art:cloned-kernel-bug"} - for key in issue_keys: - issue = jira_client.issue(key) - if not labels.issubset(set(issue.fields.labels)): + for key in bug_keys: + bug = jira_client.issue(key) + if not labels.issubset(set(bug.fields.labels)): raise ValueError(f"Jira {key} doesn't have all required labels {labels}") - if issue.fields.project.key != config.target_jira.project: + if bug.fields.project.key != config.target_jira.project: raise ValueError(f"Jira {key} doesn't belong to project {config.target_jira.project}") - components = {c.name for c in issue.fields.components} + components = {c.name for c in bug.fields.components} if config.target_jira.component not in components: raise ValueError(f"Jira {key} is not set to component {config.target_jira.component}") - target_versions = getattr(issue.fields, JIRABugTracker.FIELD_TARGET_VERSION) + target_versions = getattr(bug.fields, JIRABugTracker.FIELD_TARGET_VERSION) target_releases = {t.name for t in target_versions} if config.target_jira.target_release not in target_releases: raise ValueError(f"Jira {key} has invalid target version: {target_versions}") - found_issues.append(issue) - return found_issues + found_bugs.append(bug) + return found_bugs @staticmethod @retry(reraise=True, stop=stop_after_attempt(10), wait=wait_fixed(30)) - def _search_for_jira_issues(jira_client: JIRA, trackers: Optional[List[str]], - config: KernelBugSweepConfig): + def _search_for_jira_bugs(jira_client: JIRA, trackers: Optional[List[str]], + config: KernelBugSweepConfig): + # search for jira bugs we created previously as clones of the original kernel bugs conditions = [ "labels = art:cloned-kernel-bug", f"project = {config.target_jira.project}", @@ -101,29 +110,26 @@ def _search_for_jira_issues(jira_client: JIRA, trackers: Optional[List[str]], condition = ' OR '.join(map(lambda t: f"labels = art:kmaint:{t}", trackers)) conditions.append(f"({condition})") jql_str = f'{" AND ".join(conditions)} order by created DESC' - found_issues = jira_client.search_issues(jql_str, maxResults=0) - return cast(List[Issue], found_issues) - - def _update_jira_issues(self, jira_client: JIRA, - issues: List[Issue], - koji_api: koji.ClientSession, - config: KernelBugSweepConfig): - logger = self._runtime.logger - candidate_brew_tag = config.target_jira.candidate_brew_tag - prod_brew_tag = config.target_jira.prod_brew_tag + found_bugs = jira_client.search_issues(jql_str, maxResults=0) + return cast(List[Issue], found_bugs) + + def _find_trackers_for_bugs(self, + config: KernelBugSweepConfig, + bugs: List[Issue], + jira_client: JIRA, + ) -> (Dict[str, Issue], Dict[str, List[Issue]]): + # find relevant KMAINT trackers given the Jira bugs we previously made for them trackers: Dict[str, Issue] = {} - tracker_issues: Dict[str, List[Issue]] = {} - issue_bug_ids: Dict[str, int] = {} - for issue in issues: + tracker_bugs: Dict[str, List[Issue]] = {} + for bug in bugs: # extract bug id from labels: ["art:bz#12345"] -> 12345 - bug_id = next(map(lambda m: int(m[1]), filter(bool, map(lambda label: re.fullmatch(r"art:bz#(\d+)", label), issue.fields.labels))), None) + bug_id = next(map(lambda m: int(m[1]), filter(bool, map(lambda label: re.fullmatch(r"art:bz#(\d+)", label), bug.fields.labels))), None) if not bug_id: - raise ValueError(f"Jira clone {issue.key} doesn't have the required `art:bz#N` label") - issue_bug_ids[issue.key] = bug_id + raise ValueError(f"Jira clone {bug.key} doesn't have the required `art:bz#N` label") # extract KMAINT tracker key from labels: ["art:kmaint:KMAINT-1"] -> KMAINT-1 - tracker_key = next(map(lambda m: str(m[1]), filter(bool, map(lambda label: re.fullmatch(r"art:kmaint:(\S+)", label), issue.fields.labels))), None) + tracker_key = next(map(lambda m: str(m[1]), filter(bool, map(lambda label: re.fullmatch(r"art:kmaint:(\S+)", label), bug.fields.labels))), None) if not tracker_key: - raise ValueError(f"Jira clone {issue.key} doesn't have the required `art:kmaint:*` label") + raise ValueError(f"Jira clone {bug.key} doesn't have the required `art:kmaint:*` label") tracker = trackers.get(tracker_key) if not tracker: tracker = trackers[tracker_key] = jira_client.issue(tracker_key) @@ -131,78 +137,60 @@ def _update_jira_issues(self, jira_client: JIRA, raise ValueError(f"KMAINT tracker {tracker_key} is not in project {config.tracker_jira.project}") if not set(config.tracker_jira.labels).issubset(set(tracker.fields.labels)): raise ValueError(f"KMAINT tracker {tracker_key} doesn't have required labels {config.tracker_jira.labels}") - tracker_issues.setdefault(tracker.key, []).append(issue) + tracker_bugs.setdefault(tracker.key, []).append(bug) + return trackers, tracker_bugs + + def _process_shipped_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, prod_brew_tag): + # when NVRs are shipped, ensure the associated bugs are closed with a comment + logger.info("Build(s) %s shipped (tagged into %s). Moving bug Jira(s) %s to CLOSED...", nvrs, prod_brew_tag, bug_keys) + for bug in bugs: + current_status: str = bug.fields.status.name + if current_status.lower() != "closed": + new_status = 'CLOSED' + message = f"Elliott changed bug status from {current_status} to {new_status} because {nvrs} was/were already shipped and tagged into {prod_brew_tag}." + early_kernel.move_jira(logger, self.dry_run, jira_client, bug, new_status, message) + else: + logger.info("No need to move %s because its status is %s", bug.key, current_status) + + def _process_candidate_bugs(self, logger, bug_keys, bugs, jira_client: JIRA, nvrs, candidate_brew_tag): + # when NVRs are tagged, ensure the associated bugs are modified with a comment + logger.info("Build(s) %s tagged into %s. Moving Jira(s) %s to MODIFIED...", nvrs, candidate_brew_tag, bug_keys) + for bug in bugs: + current_status: str = bug.fields.status.name + if current_status.lower() in {"new", "assigned", "post"}: + new_status = 'MODIFIED' + message = f"Elliott changed bug status from {current_status} to {new_status} because {nvrs} was/were already tagged into {candidate_brew_tag}." + early_kernel.move_jira(logger, self.dry_run, jira_client, bug, new_status, message) + else: + logger.info("No need to move %s because its status is %s", bug.key, current_status) + + def _update_jira_bugs(self, jira_client: JIRA, found_bugs: List[Issue], koji_api: koji.ClientSession, config: KernelBugSweepConfig): + logger = self._runtime.logger + trackers, tracker_bugs = self._find_trackers_for_bugs(config, found_bugs, jira_client) for tracker_id, tracker in trackers.items(): - # Determine which NVRs have the fix. e.g. ["kernel-5.14.0-284.14.1.el9_2"] - nvrs = re.findall(r"(kernel(?:-rt)?-\S+-\S+)", tracker.fields.summary) - if not nvrs: - raise ValueError("Couldn't determine build NVRs for bug %s. Bug status will not be moved.", bug_id) - nvrs = sorted(nvrs) - issues = tracker_issues[tracker_id] - issue_keys = [issue.key for issue in issues] - # Check if nvrs are already tagged into OCP - logger.info("Getting Brew tags for build(s) %s...", nvrs) - build_tags = brew.get_builds_tags(nvrs, koji_api) - shipped = all([any(map(lambda t: t["name"] == prod_brew_tag, tags)) for tags in build_tags]) - tracker_message = None + nvrs, candidate, shipped = early_kernel.get_tracker_builds_and_tags(logger, tracker, koji_api, config.target_jira) + bugs = tracker_bugs[tracker_id] + bug_keys = [bug.key for bug in bugs] if shipped: - logger.info("Build(s) %s shipped (tagged into %s). Moving Jira(s) %s to CLOSED...", nvrs, prod_brew_tag, issue_keys) - for issue in issues: - current_status: str = issue.fields.status.name - new_status = 'CLOSED' - if current_status.lower() != "closed": - new_status = 'CLOSED' - message = f"Elliott changed bug status from {current_status} to {new_status} because {nvrs} was/were already shipped and tagged into {prod_brew_tag}." - self._move_jira(jira_client, issue, new_status, message) - else: - logger.info("No need to move %s because its status is %s", issue.key, current_status) - tracker_message = f"Build(s) {nvrs} was/were already shipped and tagged into {prod_brew_tag}." - else: - modified = all([any(map(lambda t: t["name"] == candidate_brew_tag, tags)) for tags in build_tags]) - if modified: - logger.info("Build(s) %s tagged into %s. Moving Jira(s) %s to MODIFIED...", nvrs, candidate_brew_tag, issue_keys) - for issue in issues: - current_status: str = issue.fields.status.name - if current_status.lower() in {"new", "assigned", "post"}: - new_status = 'MODIFIED' - message = f"Elliott changed bug status from {current_status} to {new_status} because {nvrs} was/were already tagged into {candidate_brew_tag}." - self._move_jira(jira_client, issue, new_status, message) - else: - logger.info("No need to move %s because its status is %s", issue.key, current_status) - tracker_message = f"Build(s) {nvrs} was/were already tagged into {candidate_brew_tag}." - if self.comment and tracker_message: - logger.info("Checking if making a comment on tracker %s is needed", tracker.key) - comments = jira_client.comments(tracker.key) - if any(map(lambda comment: comment.body == tracker_message, comments)): - logger.info("A comment was already made on %s", tracker.key) - continue - logger.info("Making a comment on tracker %s", tracker.key) - if not self.dry_run: - jira_client.add_comment(tracker.key, tracker_message) - logger.info("Left a comment on tracker %s", tracker.key) - else: - logger.warning("[DRY RUN] Would have left a comment on tracker %s", tracker.key) - - def _move_jira(self, jira_client: JIRA, issue: Issue, new_status: str, comment: Optional[str]): - logger = self._runtime.logger - current_status: str = issue.fields.status.name - logger.info("Moving %s from %s to %s", issue.key, current_status, new_status) - if not self.dry_run: - jira_client.assign_issue(issue.key, jira_client.current_user()) - jira_client.transition_issue(issue.key, new_status) - if comment: - jira_client.add_comment(issue.key, comment) - logger.info("Moved %s from %s to %s", issue.key, current_status, new_status) - else: - logger.warning("[DRY RUN] Would have moved Jira %s from %s to %s", issue.key, current_status, new_status) + self._process_shipped_bugs(logger, bug_keys, bugs, jira_client, nvrs, shipped) + if self.update_tracker: + early_kernel.process_shipped_tracker(logger, self.dry_run, jira_client, tracker, nvrs, shipped) + elif candidate: + self._process_candidate_bugs(logger, bug_keys, bugs, jira_client, nvrs, candidate) + if self.update_tracker: + early_kernel.comment_on_tracker( + logger, self.dry_run, jira_client, tracker, + [f"Build(s) {nvrs} was/were already tagged into {candidate}."] + # do not reword, see NOTE in method + ) @staticmethod def _print_report(report: Dict, out: TextIO): print_func = green_print if out.isatty() else print # use green_print if out is a TTY - jira_issues = sorted(report.get("jira_issues", []), key=lambda issue: issue["key"]) - for issue in jira_issues: - text = f"{issue['key']}\t{issue['status']}\t{issue['summary']}" + jira_bugs = sorted(report.get("jira_issues", []), key=lambda bug: bug["key"]) + for bug in jira_bugs: + text = f"{bug['key']}\t{bug['status']}\t{bug['summary']}" print_func(text, file=out) @@ -215,19 +203,18 @@ def _print_report(report: Dict, out: TextIO): is_flag=True, default=False, help="Auto move Jira bugs to MODIFIED or CLOSED") -@click.option("--comment", +@click.option("--update-tracker", is_flag=True, default=False, - help="Make comments on KMAINT trackers") + help="Update KMAINT trackers state, links, and comments") @click.option("--dry-run", is_flag=True, default=False, help="Don't change anything") @click.pass_obj -@click_coroutine -async def find_bugs_kernel_clones_cli( +def find_bugs_kernel_clones_cli( runtime: Runtime, trackers: Tuple[str, ...], issues: Tuple[str, ...], - move: bool, comment: bool, dry_run: bool): + move: bool, update_tracker: bool, dry_run: bool): """Find cloned kernel bugs in JIRA for weekly kernel release through OCP. Example 1: List all bugs in JIRA @@ -238,17 +225,17 @@ async def find_bugs_kernel_clones_cli( \b $ elliott -g openshift-4.14 find-bugs:kernel-clones --move - Example 3: Move bugs and leave a comment on the KMAINT tracker + Example 3: Move bugs and update the KMAINT tracker \b - $ elliott -g openshift-4.14 find-bugs:kernel-clones --move --comment + $ elliott -g openshift-4.14 find-bugs:kernel-clones --move --update-tracker """ runtime.initialize(mode="none") cli = FindBugsKernelClonesCli( runtime=runtime, trackers=trackers, - issues=issues, + bugs=issues, move=move, - comment=comment, + update_tracker=update_tracker, dry_run=dry_run ) - await cli.run() + cli.run() diff --git a/elliottlib/early_kernel.py b/elliottlib/early_kernel.py new file mode 100644 index 00000000..cb700c1b --- /dev/null +++ b/elliottlib/early_kernel.py @@ -0,0 +1,133 @@ +from typing import Dict, List, Optional, Sequence, TextIO, Tuple, cast +import re +import koji +from errata_tool.build import Build +from errata_tool import Erratum, ErrataException +from jira import Issue, JIRA +from elliottlib.config_model import KernelBugSweepConfig +from elliottlib import brew + + +def get_tracker_builds_and_tags( + logger, tracker: Issue, + koji_api: koji.ClientSession, + config: KernelBugSweepConfig.TargetJiraConfig, +) -> Tuple[List[str], str, str]: + """ + Determine NVRs (e.g. ["kernel-5.14.0-284.14.1.el9_2"]) from the summary, + and whether candidate/base tags have been applied + """ + nvrs = sorted(re.findall(r"(kernel(?:-rt)?-\S+-\S+)", tracker.fields.summary)) + if not nvrs: + raise ValueError(f"Couldn't determine build NVRs for tracker {tracker.id}. Status will not be changed.") + + logger.info("Getting Brew tags for build(s) %s...", nvrs) + candidate_brew_tag = config.candidate_brew_tag + prod_brew_tag = config.prod_brew_tag + build_tags = [set(t["name"] for t in tags) for tags in brew.get_builds_tags(nvrs, koji_api)] + shipped = all(prod_brew_tag in tags for tags in build_tags) + candidate = all(candidate_brew_tag in tags for tags in build_tags) + + return nvrs, candidate_brew_tag if candidate else None, prod_brew_tag if shipped else None + + +def _advisories_for_builds(nvrs: List[str]) -> List[Erratum]: + advisories = {} + for nvr in nvrs: + try: + build = Build(nvr) + except ErrataException: + continue # probably build not yet added to an advisory + for errata_id in build.all_errata_ids: + if errata_id in advisories: + continue # already loaded + # TODO: optimize with errata.get_raw_erratum + advisory = Erratum(errata_id=errata_id) + if advisory.errata_state == "SHIPPED_LIVE": + advisories[errata_id] = advisory + return list(advisories.values()) + + +def _link_tracker_advisories( + logger, dry_run: bool, jira_client: JIRA, + advisories: List[Erratum], nvrs: List[str], tracker: Issue, +) -> List[str]: + tracker_messages = [] + links = set(link.raw['object']['url'] for link in jira_client.remote_links(tracker)) # check if we already linked advisories + for advisory in advisories: + if advisory.url() in links: + logger.info(f"Tracker {tracker.id} already links {advisory.url()} ({advisory.synopsis})") + continue + tracker_messages.append(f"Build(s) {nvrs} shipped in advisory {advisory.url()} with title:\n{advisory.synopsis}") + if dry_run: + logger.info(f"[DRY RUN] Tracker {tracker.id} would have added link {advisory.url()} ({advisory.errata_name}: {advisory.synopsis})") + else: + jira_client.add_simple_link( + tracker, dict( + url=advisory.url(), + title=f"{advisory.errata_name}: {advisory.synopsis}")) + return tracker_messages + + +def process_shipped_tracker( + logger, dry_run: bool, + jira_client: JIRA, tracker: Issue, + nvrs: List[str], shipped_tag: str, +) -> List[str]: + # when NVRs are shipped, ensure the associated tracker is closed with a comment + # and a link to any advisory that shipped them + logger.info("Build(s) %s shipped (tagged into %s). Looking for advisories...", nvrs, shipped_tag) + advisories = _advisories_for_builds(nvrs) + if not advisories: + raise RuntimeError(f"NVRs {nvrs} tagged into {shipped_tag} but not found in any shipped advisories!") + tracker_messages = _link_tracker_advisories(logger, dry_run, jira_client, advisories, nvrs, tracker) + + logger.info("Moving tracker Jira %s to CLOSED...", tracker) + current_status: str = tracker.fields.status.name + if current_status.lower() != "closed": + if tracker_messages: + comment_on_tracker(logger, dry_run, jira_client, tracker, tracker_messages) + else: + logger.warning("Closing Jira %s without adding any messages; prematurely closed?", tracker) + move_jira(logger, dry_run, jira_client, tracker, "CLOSED") + else: + logger.info("No need to move %s because its status is %s", tracker.key, current_status) + + +def move_jira( + logger, dry_run: bool, + jira_client: JIRA, issue: Issue, + new_status: str, comment: str = None, +): + current_status: str = issue.fields.status.name + if dry_run: + logger.info("[DRY RUN] Would have moved Jira %s from %s to %s", issue.key, current_status, new_status) + else: + logger.info("Moving %s from %s to %s", issue.key, current_status, new_status) + jira_client.assign_issue(issue.key, jira_client.current_user()) + jira_client.transition_issue(issue.key, new_status) + if comment: + jira_client.add_comment(issue.key, comment) + logger.info("Moved %s from %s to %s", issue.key, current_status, new_status) + + +def comment_on_tracker( + logger, dry_run: bool, + jira_client: JIRA, tracker: Issue, + comments: List[str], +): + # wording NOTE: commenting is intended to avoid duplicates, but this logic may re-comment on old + # trackers if the wording changes after previously commented. think long and hard before + # changing the wording of any tracker comments. + logger.info("Checking if making a comment on tracker %s is needed", tracker.key) + previous_comments = jira_client.comments(tracker.key) + for comment in comments: + if any(previous.body == comment for previous in previous_comments): + logger.info("Intended comment was already made on %s", tracker.key) + continue + logger.info("Making a comment on tracker %s", tracker.key) + if dry_run: + logger.info("[DRY RUN] Would have left a comment on tracker %s", tracker.key) + else: + jira_client.add_comment(tracker.key, comment) + logger.info("Left a comment on tracker %s", tracker.key) diff --git a/tests/test_early_kernel.py b/tests/test_early_kernel.py new file mode 100644 index 00000000..c3e63240 --- /dev/null +++ b/tests/test_early_kernel.py @@ -0,0 +1,137 @@ +from io import StringIO +from unittest import TestCase +from unittest.mock import ANY, MagicMock, Mock, patch + +import koji +from jira import JIRA, Issue +from errata_tool import Erratum +from errata_tool.build import Build + +from elliottlib.config_model import KernelBugSweepConfig +from elliottlib import early_kernel + + +class TestEarlyKernel(TestCase): + @patch("elliottlib.brew.get_builds_tags") + def test_get_tracker_builds_and_tags(self, get_builds_tags: Mock): + logger = MagicMock() + conf = KernelBugSweepConfig.TargetJiraConfig( + project="TARGET-PROJECT", + component="Target Component", + version="4.14", target_release="4.14.z", + candidate_brew_tag="fake-candidate", prod_brew_tag="fake-prod") + tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( + summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", + description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", + )) + koji_api = MagicMock(spec=koji.ClientSession) + get_builds_tags.return_value = [ + [{"name": "irrelevant-1"}, {"name": "fake-candidate"}], + [{"name": "irrelevant-2"}, {"name": "fake-candidate"}], + ] + + nvrs, candidate, shipped = early_kernel.get_tracker_builds_and_tags(logger, tracker, koji_api, conf) + self.assertEqual(["kernel-1.0.1-1.fake", "kernel-rt-1.0.1-1.fake"], nvrs) + self.assertEqual("fake-candidate", candidate) + self.assertFalse(shipped) + + @patch("elliottlib.early_kernel._advisories_for_builds") + @patch("elliottlib.early_kernel._link_tracker_advisories") + @patch("elliottlib.early_kernel.comment_on_tracker") + @patch("elliottlib.early_kernel.move_jira") + def test_process_shipped_tracker(self, move_jira: Mock, comment_on_tracker: Mock, + _link_tracker_advisories: Mock, _advisories_for_builds: Mock): + logger = MagicMock() + jira_client = MagicMock(spec=JIRA) + tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( + summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", + description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", + status=Mock(), # need to set "name" but can't in a mock - set later + )) + nvrs = ["kernel-1.0.1-1.fake", "kernel-rt-1.0.1-1.fake"] + advisory = MagicMock(spec=Erratum) + _advisories_for_builds.return_value = [advisory] + _link_tracker_advisories.return_value = ["comment"] + + setattr(tracker.fields.status, "name", "CLOSED") + early_kernel.process_shipped_tracker(logger, False, jira_client, tracker, nvrs, "tag") + comment_on_tracker.assert_not_called() + move_jira.assert_not_called() + + setattr(tracker.fields.status, "name", "New") + early_kernel.process_shipped_tracker(logger, False, jira_client, tracker, nvrs, "tag") + comment_on_tracker.assert_called_once_with(logger, False, jira_client, tracker, ["comment"]) + move_jira.assert_called_once_with(logger, False, jira_client, tracker, "CLOSED") + + @patch("elliottlib.early_kernel.Erratum") + @patch("elliottlib.early_kernel.Build") + def test_advisories_for_builds(self, build_clz: Mock, erratum_clz: Mock): + build_clz.return_value = MagicMock(spec=Build, all_errata_ids=[42]) + advisory = erratum_clz.return_value = MagicMock(spec=Erratum, errata_state="SHIPPED_LIVE") + + self.assertEqual([advisory], early_kernel._advisories_for_builds(nvrs=["nvr-1", "nvr-2"])) + erratum_clz.assert_called_once_with(errata_id=42) + + advisory.errata_state = "QE" + self.assertEqual([], early_kernel._advisories_for_builds(nvrs=["nvr-1", "nvr-2"])) + + def test_link_tracker_advisories(self): + tracker = MagicMock(spec=Issue, id=42) + advisory = MagicMock(spec=Erratum, errata_name="RHBA-42", synopsis="shipped some stuff") + jira_client = MagicMock(spec=JIRA) + jira_client.remote_links.return_value = [ + MagicMock(raw=dict(object=dict(url="http://example.com"))), + ] + + # test adding an existing link does not happen + advisory.url.return_value = "http://example.com" + msgs = early_kernel._link_tracker_advisories( + MagicMock(), False, jira_client, [advisory], ["nvrs"], tracker + ) + jira_client.add_simple_link.assert_not_called() + self.assertEqual([], msgs) + + # test adding a new link does happen + advisory.url.return_value = "http://different.example.com" + msgs = early_kernel._link_tracker_advisories( + MagicMock(), False, jira_client, [advisory], ["nvrs"], tracker + ) + jira_client.add_simple_link.assert_called_once_with(tracker, ANY) + self.assertEqual(1, len(msgs)) + + def test_move_jira(self): + runtime = MagicMock() + jira_client = MagicMock(spec=JIRA) + comment = "Test message" + issue = MagicMock(spec=Issue, **{ + "key": "FOO-1", "fields": MagicMock(), + "fields.labels": ["art:bz#1", "art:kmaint:KMAINT-1"], + "fields.status.name": "New", + }) + jira_client.current_user.return_value = "fake-user" + early_kernel.move_jira(runtime.logger(), False, jira_client, issue, "MODIFIED", comment) + jira_client.assign_issue.assert_called_once_with("FOO-1", "fake-user") + jira_client.transition_issue.assert_called_once_with("FOO-1", "MODIFIED") + + def test_comment_on_tracker(self): + logger = MagicMock() + jira_client = MagicMock(spec=JIRA) + tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( + summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", + description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", + )) + comment1, comment2 = "Comment 1", "Comment 2" + + # Test 1: making a comment + jira_client.comments.return_value = [MagicMock(body=comment1)] + early_kernel.comment_on_tracker(logger, False, jira_client, tracker, [comment2]) + jira_client.add_comment.assert_called_once_with("TRACKER-1", comment2) + + # Test 2: not making a comment because a comment has been made + jira_client.comments.return_value = [ + MagicMock(body=comment1), + MagicMock(body=comment2), + ] + jira_client.add_comment.reset_mock() + early_kernel.comment_on_tracker(logger, False, jira_client, tracker, [comment2, comment1]) + jira_client.add_comment.assert_not_called() diff --git a/tests/test_find_bugs_kernel_cli.py b/tests/test_find_bugs_kernel_cli.py index b242caf9..7b3837cf 100644 --- a/tests/test_find_bugs_kernel_cli.py +++ b/tests/test_find_bugs_kernel_cli.py @@ -12,6 +12,7 @@ from elliottlib.config_model import KernelBugSweepConfig from elliottlib.runtime import Runtime from elliottlib.bzutil import JIRABugTracker +from elliottlib import early_kernel class TestFindBugsKernelCli(IsolatedAsyncioTestCase): @@ -27,7 +28,7 @@ def test_find_kmaint_trackers(self): def test_get_and_filter_bugs(self): runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) bz_client = MagicMock(spec=Bugzilla) bz_client.getbugs.return_value = [ MagicMock(spec=Bug, id=1, weburl="irrelevant", cf_zstream_target_release=None), @@ -45,7 +46,7 @@ def test_get_and_filter_bugs(self): def test_find_bugs(self, _get_and_filter_bugs: Mock): runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) bz_client = MagicMock(spec=Bugzilla) tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( summary="foo-1.0.1-1.el8_6 and bar-1.0.1-1.el8_6 early delivery via OCP", @@ -72,7 +73,7 @@ def test_clone_bugs1(self): # Test cloning a bug that has not already been cloned runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) jira_client = MagicMock(spec=JIRA) bugs = [ MagicMock(spec=Bug, id=1, weburl="https://example.com/1", @@ -112,7 +113,7 @@ def test_clone_bugs2(self): # Test cloning a bug that has already been cloned runtime = MagicMock() cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) jira_client = MagicMock(spec=JIRA) bugs = [ MagicMock(spec=Bug, id=1, weburl="https://example.com/1", @@ -168,42 +169,6 @@ def test_print_report(self): TRACKER-1 2 N/A Verified test bug 2 """.strip()) - @patch("elliottlib.brew.get_builds_tags") - def test_comment_on_tracker(self, get_builds_tags: Mock): - runtime = MagicMock() - cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) - jira_client = MagicMock(spec=JIRA) - conf = KernelBugSweepConfig.TargetJiraConfig( - project="TARGET-PROJECT", - component="Target Component", - version="4.14", target_release="4.14.z", - candidate_brew_tag="fake-candidate", prod_brew_tag="fake-prod") - tracker = MagicMock(spec=Issue, key="TRACKER-1", fields=MagicMock( - summary="kernel-1.0.1-1.fake and kernel-rt-1.0.1-1.fake early delivery via OCP", - description="Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", - )) - koji_api = MagicMock(spec=koji.ClientSession) - get_builds_tags.return_value = [ - [{"name": "irrelevant-1"}, {"name": "fake-candidate"}], - [{"name": "irrelevant-2"}, {"name": "fake-candidate"}], - ] - jira_client.comments.return_value = [] - - # Test 1: making a comment - cli._comment_on_tracker(jira_client, tracker, koji_api, conf) - jira_client.add_comment.assert_called_once_with("TRACKER-1", "Build(s) ['kernel-1.0.1-1.fake', 'kernel-rt-1.0.1-1.fake'] was/were already tagged into fake-candidate.") - - # Test 2: not making a comment because a comment has been made - jira_client.comments.return_value = [ - MagicMock(body="irrelevant 1"), - MagicMock(body="Build(s) ['kernel-1.0.1-1.fake', 'kernel-rt-1.0.1-1.fake'] was/were already tagged into fake-candidate."), - MagicMock(body="irrelevant 2"), - ] - jira_client.add_comment.reset_mock() - cli._comment_on_tracker(jira_client, tracker, koji_api, conf) - jira_client.add_comment.assert_not_called() - def test_new_jira_fields_from_bug(self): bug = MagicMock(spec=Bug, id=12345, cf_zstream_target_release="8.6.0", weburl="https://example.com/12345", @@ -242,11 +207,11 @@ def test_new_jira_fields_from_bug(self): @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._print_report") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._clone_bugs") - @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._comment_on_tracker") + @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._update_tracker") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_bugs") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_kmaint_trackers") async def test_run_without_specified_trackers( - self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _comment_on_tracker: Mock, + self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _update_tracker: Mock, _clone_bugs: Mock, _print_report: Mock): runtime = MagicMock( autospec=Runtime, assembly_type=AssemblyTypes.STREAM, @@ -292,18 +257,18 @@ async def test_run_without_specified_trackers( summary="fake summary 10003", description="fake description 10003"), ] cli = FindBugsKernelCli( - runtime=runtime, trackers=[], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], clone=True, reconcile=True, update_tracker=True, dry_run=False) await cli.run() - _comment_on_tracker.assert_called_once_with(ANY, _find_kmaint_trackers.return_value[0], ANY, ANY) + _update_tracker.assert_called_once_with(ANY, _find_kmaint_trackers.return_value[0], ANY, ANY) _clone_bugs.assert_called_once_with(ANY, _find_bugs.return_value, ANY) @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._print_report") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._clone_bugs") - @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._comment_on_tracker") + @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._update_tracker") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_bugs") @patch("elliottlib.cli.find_bugs_kernel_cli.FindBugsKernelCli._find_kmaint_trackers") async def test_run_with_specified_trackers( - self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _comment_on_tracker: Mock, + self, _find_kmaint_trackers: Mock, _find_bugs: Mock, _update_tracker: Mock, _clone_bugs: Mock, _print_report: Mock): runtime = MagicMock( autospec=Runtime, assembly_type=AssemblyTypes.STREAM, @@ -349,8 +314,8 @@ async def test_run_with_specified_trackers( summary="fake summary 10003", description="fake description 10003"), ] cli = FindBugsKernelCli( - runtime=runtime, trackers=["TRACKER-999"], clone=True, reconcile=True, comment=True, dry_run=False) + runtime=runtime, trackers=["TRACKER-999"], clone=True, reconcile=True, update_tracker=True, dry_run=False) await cli.run() - _comment_on_tracker.assert_called_once() + _update_tracker.assert_called_once() _clone_bugs.assert_called_once_with(ANY, _find_bugs.return_value, ANY) _find_kmaint_trackers.assert_not_called() diff --git a/tests/test_find_bugs_kernel_clones_cli.py b/tests/test_find_bugs_kernel_clones_cli.py index 588726bd..8d30759f 100644 --- a/tests/test_find_bugs_kernel_clones_cli.py +++ b/tests/test_find_bugs_kernel_clones_cli.py @@ -9,6 +9,7 @@ from elliottlib.cli.find_bugs_kernel_clones_cli import FindBugsKernelClonesCli from elliottlib.config_model import KernelBugSweepConfig from elliottlib.bzutil import JIRABugTracker +from elliottlib import early_kernel class TestFindBugsKernelClonesCli(IsolatedAsyncioTestCase): @@ -31,10 +32,10 @@ def setUp(self) -> None: }, }) - def test_get_jira_issues(self): + def test_get_jira_bugs(self): runtime = MagicMock() cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], issues=[], move=True, comment=True, dry_run=False) + runtime=runtime, trackers=[], bugs=[], move=True, update_tracker=True, dry_run=False) jira_client = MagicMock(spec=JIRA) component = MagicMock() component.configure_mock(name="RHCOS") @@ -48,10 +49,10 @@ def test_get_jira_issues(self): "fields.components": [component], f"fields.{JIRABugTracker.FIELD_TARGET_VERSION}": [target_release], }) - actual = cli._get_jira_issues(jira_client, ["FOO-1", "FOO-2", "FOO-3"], self._config) - self.assertEqual([issue.key for issue in actual], ["FOO-1", "FOO-2", "FOO-3"]) + actual = cli._get_jira_bugs(jira_client, ["FOO-1", "FOO-2", "FOO-3"], self._config) + self.assertEqual([bug.key for bug in actual], ["FOO-1", "FOO-2", "FOO-3"]) - def test_search_for_jira_issues(self): + def test_search_for_jira_bugs(self): jira_client = MagicMock(spec=JIRA) trackers = ["TRACKER-1", "TRACKER-2"] jira_client.search_issues.return_value = [ @@ -59,17 +60,19 @@ def test_search_for_jira_issues(self): MagicMock(key="FOO-2"), MagicMock(key="FOO-3"), ] - actual = FindBugsKernelClonesCli._search_for_jira_issues(jira_client, trackers, self._config) + actual = FindBugsKernelClonesCli._search_for_jira_bugs(jira_client, trackers, self._config) expected_jql = 'labels = art:cloned-kernel-bug AND project = OCPBUGS AND component = RHCOS AND "Target Version" = "4.14.0" AND (labels = art:kmaint:TRACKER-1 OR labels = art:kmaint:TRACKER-2) order by created DESC' jira_client.search_issues.assert_called_once_with(expected_jql, maxResults=0) self.assertEqual([issue.key for issue in actual], ["FOO-1", "FOO-2", "FOO-3"]) - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._move_jira") + @patch("elliottlib.early_kernel.process_shipped_tracker") + @patch("elliottlib.early_kernel.move_jira") @patch("elliottlib.brew.get_builds_tags") - def test_update_jira_issues(self, get_builds_tags: Mock, _move_jira: Mock): + def test_update_jira_bugs(self, get_builds_tags: Mock, _move_jira: Mock, + process_shipped_tracker: Mock): runtime = MagicMock() jira_client = MagicMock(spec=JIRA) - jira_client.issue.return_value = MagicMock(spec=Issue, ** { + tracker = jira_client.issue.return_value = MagicMock(spec=Issue, ** { "key": "KMAINT-1", "fields": MagicMock(), "fields.project.key": "KMAINT", @@ -78,8 +81,8 @@ def test_update_jira_issues(self, get_builds_tags: Mock, _move_jira: Mock): "fields.description": "Fixes bugzilla.redhat.com/show_bug.cgi?id=5 and bz6.", }) cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], issues=[], move=True, comment=True, dry_run=False) - issues = [ + runtime=runtime, trackers=[], bugs=[], move=True, update_tracker=True, dry_run=False) + bugs = [ MagicMock(spec=Issue, **{ "key": "FOO-1", "fields": MagicMock(), "fields.labels": ["art:bz#1", "art:kmaint:KMAINT-1"], @@ -102,25 +105,20 @@ def test_update_jira_issues(self, get_builds_tags: Mock, _move_jira: Mock): [{"name": "irrelevant-1"}, {"name": "rhaos-4.14-rhel-9-candidate"}], [{"name": "irrelevant-2"}, {"name": "rhaos-4.14-rhel-9-candidate"}], ] - cli._update_jira_issues(jira_client, issues, koji_api, self._config) - _move_jira.assert_any_call(jira_client, issues[0], "MODIFIED", ANY) - _move_jira.assert_any_call(jira_client, issues[1], "MODIFIED", ANY) + cli._update_jira_bugs(jira_client, bugs, koji_api, self._config) + _move_jira.assert_any_call(ANY, False, jira_client, bugs[0], "MODIFIED", ANY) + _move_jira.assert_any_call(ANY, False, jira_client, bugs[1], "MODIFIED", ANY) - def test_move_jira(self): - runtime = MagicMock() - jira_client = MagicMock(spec=JIRA) - cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], issues=[], move=True, comment=True, dry_run=False) - comment = "Test message" - issue = MagicMock(spec=Issue, **{ - "key": "FOO-1", "fields": MagicMock(), - "fields.labels": ["art:bz#1", "art:kmaint:KMAINT-1"], - "fields.status.name": "New", - }) - jira_client.current_user.return_value = "fake-user" - cli._move_jira(jira_client, issue, "MODIFIED", comment) - jira_client.assign_issue.assert_called_once_with("FOO-1", "fake-user") - jira_client.transition_issue.assert_called_once_with("FOO-1", "MODIFIED") + # now with shipped + _move_jira.reset_mock() + get_builds_tags.return_value = [ + [{"name": "rhaos-4.14-rhel-9"}, {"name": "rhaos-4.14-rhel-9-candidate"}], + [{"name": "rhaos-4.14-rhel-9"}, {"name": "rhaos-4.14-rhel-9-candidate"}], + ] + cli._update_jira_bugs(jira_client, bugs, koji_api, self._config) + _move_jira.assert_any_call(ANY, False, jira_client, bugs[0], "CLOSED", ANY) + process_shipped_tracker.assert_called_once_with(ANY, False, ANY, tracker, ANY, + "rhaos-4.14-rhel-9") def test_print_report(self): report = { @@ -137,11 +135,11 @@ def test_print_report(self): """.strip()) @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._print_report") - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._get_jira_issues") - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._update_jira_issues") - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._search_for_jira_issues") - async def test_run_without_specified_issues(self, _search_for_jira_issues: Mock, _update_jira_issues: Mock, - _get_jira_issues: Mock, _print_report: Mock): + @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._get_jira_bugs") + @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._update_jira_bugs") + @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._search_for_jira_bugs") + async def test_run_without_specified_bugs(self, _search_for_jira_bugs: Mock, _update_jira_bugs: Mock, + _get_jira_bugs: Mock, _print_report: Mock): runtime = MagicMock(assembly_type=AssemblyTypes.STREAM) runtime.gitdata.load_data.return_value = MagicMock( data={ @@ -163,7 +161,7 @@ async def test_run_without_specified_issues(self, _search_for_jira_issues: Mock, }, } ) - found_issues = [ + found_bugs = [ MagicMock(spec=Issue, **{ "key": "FOO-1", "fields": MagicMock(), "fields.summary": "Fake bug 1", @@ -184,11 +182,11 @@ async def test_run_without_specified_issues(self, _search_for_jira_issues: Mock, "fields.status.name": "ON_QA", }), ] - _search_for_jira_issues.return_value = found_issues + _search_for_jira_bugs.return_value = found_bugs cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], issues=[], move=True, comment=True, dry_run=False) - await cli.run() - _update_jira_issues.assert_called_once() + runtime=runtime, trackers=[], bugs=[], move=True, update_tracker=True, dry_run=False) + cli.run() + _update_jira_bugs.assert_called_once_with(ANY, found_bugs, ANY, ANY) expected_report = { 'jira_issues': [ {'key': 'FOO-1', 'summary': 'Fake bug 1', 'status': 'New'}, @@ -199,11 +197,11 @@ async def test_run_without_specified_issues(self, _search_for_jira_issues: Mock, _print_report.assert_called_once_with(expected_report, ANY) @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._print_report") - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._get_jira_issues") - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._update_jira_issues") - @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._search_for_jira_issues") - async def test_run_with_specified_issues(self, _search_for_jira_issues: Mock, _update_jira_issues: Mock, - _get_jira_issues: Mock, _print_report: Mock): + @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._get_jira_bugs") + @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._update_jira_bugs") + @patch("elliottlib.cli.find_bugs_kernel_clones_cli.FindBugsKernelClonesCli._search_for_jira_bugs") + async def test_run_with_specified_bugs(self, _search_for_jira_bugs: Mock, _update_jira_bugs: Mock, + _get_jira_bugs: Mock, _print_report: Mock): runtime = MagicMock(assembly_type=AssemblyTypes.STREAM) runtime.gitdata.load_data.return_value = MagicMock( data={ @@ -225,7 +223,7 @@ async def test_run_with_specified_issues(self, _search_for_jira_issues: Mock, _u }, } ) - found_issues = [ + found_bugs = [ MagicMock(spec=Issue, **{ "key": "FOO-1", "fields": MagicMock(), "fields.summary": "Fake bug 1", @@ -246,11 +244,12 @@ async def test_run_with_specified_issues(self, _search_for_jira_issues: Mock, _u "fields.status.name": "ON_QA", }), ] - _get_jira_issues.return_value = found_issues + _get_jira_bugs.return_value = found_bugs cli = FindBugsKernelClonesCli( - runtime=runtime, trackers=[], issues=["FOO-1", "FOO-2", "FOO-3"], move=True, comment=True, dry_run=False) - await cli.run() - _update_jira_issues.assert_called_once() + runtime=runtime, trackers=[], bugs=["FOO-1", "FOO-2", "FOO-3"], move=True, + update_tracker=True, dry_run=False) + cli.run() + _update_jira_bugs.assert_called_once_with(ANY, found_bugs, ANY, ANY) expected_report = { 'jira_issues': [ {'key': 'FOO-1', 'summary': 'Fake bug 1', 'status': 'New'},