From b7113e7e0cbb5f0bcd272c7e9d2e5bc0e0e98592 Mon Sep 17 00:00:00 2001 From: Konrad Kleine Date: Fri, 5 Apr 2024 22:43:36 +0200 Subject: [PATCH] All working * splitting errors into their own distinct comments * links from the build matrix to the error comments * and more fixes --- .../snapshot_manager/github_util.py | 16 +- .../snapshot_manager/snapshot_manager.py | 246 +++++++++++------- .../snapshot_manager/testing_farm_util.py | 27 +- snapshot_manager/snapshot_manager/util.py | 2 +- .../tests/snapshot_manager_test.py | 4 +- 5 files changed, 177 insertions(+), 118 deletions(-) diff --git a/snapshot_manager/snapshot_manager/github_util.py b/snapshot_manager/snapshot_manager/github_util.py index 0411926f..0ffec08b 100644 --- a/snapshot_manager/snapshot_manager/github_util.py +++ b/snapshot_manager/snapshot_manager/github_util.py @@ -100,9 +100,13 @@ def initial_comment(self) -> str: {self.config.update_marker} -

Last updated: {datetime.datetime.now().isoformat()}

+{self.last_updated_html()} """ + @classmethod + def last_updated_html(cls) -> str: + return f"

Last updated: {datetime.datetime.now().isoformat()}

" + def create_or_get_todays_github_issue( self, maintainer_handle: str, @@ -269,10 +273,10 @@ def remove_labels_safe( issue (github.Issue.Issue): The issue from which to remove the labels label_names_to_be_removed (list[str]): A list of label names that shall be removed if they exist on the issue. """ - current_label_names = [label.name for label in issue.get_labels()] - label_names_to_be_removed = set(current_label_names).intersection( - label_names_to_be_removed - ) - for label in label_names_to_be_removed: + current_set = {label.name for label in issue.get_labels()} + + remove_set = set(label_names_to_be_removed) + intersection = current_set.intersection(remove_set) + for label in intersection: logging.info(f"Removing label '{label}' from issue: {issue.title}") issue.remove_from_labels(label) diff --git a/snapshot_manager/snapshot_manager/snapshot_manager.py b/snapshot_manager/snapshot_manager/snapshot_manager.py index 43168766..1a600068 100755 --- a/snapshot_manager/snapshot_manager/snapshot_manager.py +++ b/snapshot_manager/snapshot_manager/snapshot_manager.py @@ -27,11 +27,11 @@ def check_todays_builds(self): maintainer_handle=self.config.maintainer_handle, creator=self.config.creator_handle, ) - if issue.state == "closed": - logging.info( - f"Issue {issue.html_url} was already closed. Not doing anything." - ) - return + # if issue.state == "closed": + # logging.info( + # f"Issue {issue.html_url} was already closed. Not doing anything." + # ) + # return all_chroots = self.copr.get_copr_chroots() @@ -52,14 +52,13 @@ def check_todays_builds(self): build_states=states, ) - logging.info("Append ordered list of errors to the issue's body comment") + logging.info("Get ordered list of errors to the issue's body comment") errors = build_status.list_only_errors(states=states) - logging.info( - "Extract and sanitize testing-farm information out of the last comment body." - ) - testing_farm_requests = tf.TestingFarmRequest.parse(comment_body) - logging.info(testing_farm_requests) + logging.info("Recover testing-farm requests") + requests = tf.TestingFarmRequest.parse(comment_body) + if requests is None: + requests = dict() # logging.info(f"Update the issue comment body") # # See https://github.com/fedora-llvm-team/llvm-snapshots/issues/205#issuecomment-1902057639 @@ -70,101 +69,63 @@ def check_todays_builds(self): # f"Github only allows {max_length} characters on a comment body and we have reached {len(comment_body)} characters." # ) - logging.info("Gather labels based on the errors we've found") - error_labels = list({f"error/{err.err_cause}" for err in errors}) - project_labels = list({f"project/{err.package_name}" for err in errors}) - os_labels = list({f"os/{err.os}" for err in errors}) - arch_labels = list({f"arch/{err.arch}" for err in errors}) - strategy_labels = [f"strategy/{self.config.build_strategy}"] - other_labels: list[str] = [] - if errors is None and len(errors) > 0: - other_labels.append("broken_snapshot_detected") - - logging.info("Create labels") - self.github._create_labels( - labels=["broken_snapshot_detected"], color="F46696", prefix="" - ) - self.github.create_labels_for_error_causes(error_labels) - self.github.create_labels_for_oses(os_labels) - self.github.create_labels_for_projects(project_labels) - self.github.create_labels_for_archs(arch_labels) - self.github.create_labels_for_strategies(strategy_labels) - self.github.create_labels_for_in_testing(all_chroots) - self.github.create_labels_for_tested_on(all_chroots) - self.github.create_labels_for_failed_on(all_chroots) - - # Remove old labels from issue if they no longer apply. This is greate - # for restarted builds for example to make all builds green and be able - # to promote this snapshot. - - labels_to_be_removed: list[str] = [] - old_error_labels = self.github.get_error_labels_on_issue(issue=issue) - old_project_labels = self.github.get_project_labels_on_issue(issue=issue) - old_arch_labels = self.github.get_arch_labels_on_issue(issue=issue) - - labels_to_be_removed.extend(set(old_error_labels) - set(error_labels)) - labels_to_be_removed.extend(set(old_project_labels) - set(project_labels)) - labels_to_be_removed.extend(set(old_arch_labels) - set(arch_labels)) - - for label in labels_to_be_removed: - logging.info(f"Removing label that no longer applies: {label}") - issue.remove_from_labels(label=label) - - # Labels must be added or removed manually in order to not remove manually added labels :/ - labels_to_add = ( - error_labels - + project_labels - + os_labels - + arch_labels - + strategy_labels - + other_labels - ) - logging.info(f"Adding label: {labels_to_add}") - issue.add_to_labels(labels_to_add) + self.handle_labels(issue=issue, all_chroots=all_chroots, errors=errors) failed_test_cases: list[tf.FailedTestCase] = [] for chroot in all_chroots: - # Define some label names - in_testing = f"{self.config.label_prefix_in_testing}{chroot}" - tested_on = f"{self.config.label_prefix_tested_on}{chroot}" - failed_on = f"{self.config.label_prefix_tested_on}{chroot}" + # Create or update a comment for each chroot that has errors and render + errors_for_this_chroot = [ + error for error in errors if error.chroot == chroot + ] + if errors_for_this_chroot is not None and len(errors_for_this_chroot) > 0: + comment = self.github.create_or_update_comment( + issue=issue, + marker=f"", + comment_body=f""" +

Errors found in {chroot}

+{self.github.last_updated_html()} +{build_status.render_as_markdown(errors_for_this_chroot)} +""", + ) + build_status_matrix = build_status_matrix.replace( + chroot, + f'{chroot}
:x: Copr build(s) failed', + ) - if not tf.is_chroot_supported(chroot): + for chroot in all_chroots: + # Check if we can ignore the chroot because it is not supported by testing-farm + if not tf.TestingFarmRequest.is_chroot_supported(chroot): # see https://docs.testing-farm.io/Testing%20Farm/0.1/test-environment.html#_supported_architectures logging.debug( f"Ignoring chroot {chroot} because testing-farm doesn't support it." ) continue + in_testing = f"{self.config.label_prefix_in_testing}{chroot}" + tested_on = f"{self.config.label_prefix_tested_on}{chroot}" + failed_on = f"{self.config.label_prefix_tested_on}{chroot}" + # Gather build IDs associated with this chroot. + # We'll attach them a new testing-farm request, and for a recovered + # request we'll check if they still match the current ones. current_copr_build_ids = [ state.build_id for state in states if state.chroot == chroot ] - def flip_test_label(issue: github.Issue.Issue, new_label: str | None): - all_states = [in_testing, tested_on, failed_on] - labels_to_be_removed = all_states - if new_label is not None: - labels_to_be_removed = [set(all_states).difference(new_label)] - self.github.remove_labels_safe(issue, labels_to_be_removed) - issue.add_to_labels(new_label) - # Check if we need to invalidate a recovered testing-farm requests. # Background: It can be that we have old testing-farm request IDs in the issue comment. # But if a package was re-build and failed, the old request ID for that chroot is invalid. # To compensate for this scenario that we saw on April 1st 2024 btw., we're gonna # delete any request that has a different set of Copr build IDs associated with it. - if chroot in testing_farm_requests: - recovered_request = testing_farm_requests[chroot] + if chroot in requests: + recovered_request = requests[chroot] if set(recovered_request.copr_build_ids) != set(current_copr_build_ids): logging.info( - "The recovered testing-farm request no longer applies because build IDs have changed" + f"Recovered request ({recovered_request.request_id}) invalid (build IDs changed):\\nRecovered: {recovered_request.copr_build_ids}\\nCurrent: {current_copr_build_ids}" ) - flip_test_label(issue, None) - del testing_farm_requests[chroot] - - tf.TestingFarmRequest(chroot=chroot) + self.flip_test_label(issue=issue, chroot=chroot, new_label=None) + del requests[chroot] logging.info(f"Check if all builds in chroot {chroot} have succeeded") builds_succeeded = self.copr.has_all_good_builds( @@ -181,8 +142,8 @@ def flip_test_label(issue: github.Issue.Issue, new_label: str | None): logging.info(f"All builds in chroot {chroot} have succeeded!") # Check for current status of testing-farm request - if chroot in testing_farm_requests: - request = testing_farm_requests[chroot] + if chroot in requests: + request = requests[chroot] watch_result, artifacts_url = request.watch() html = tf.render_html(request, watch_result, artifacts_url) @@ -191,32 +152,31 @@ def flip_test_label(issue: github.Issue.Issue, new_label: str | None): f"{chroot}
{html}", ) - # Fetch all failed tests for this request - if watch_result.is_error: - failed_test_cases.extend( - request.fetch_failed_test_cases(artifacts_url=artifacts_url) - ) - logging.info( f"Chroot {chroot} testing-farm watch result: {watch_result} (URL: {artifacts_url})" ) if watch_result.is_error: - flip_test_label(issue, failed_on) + # Fetch all failed test cases for this request + failed_test_cases.extend( + request.fetch_failed_test_cases(artifacts_url=artifacts_url) + ) + self.flip_test_label(issue, chroot, failed_on) elif watch_result == tf.TestingFarmWatchResult.TESTS_PASSED: - flip_test_label(issue, tested_on) + self.flip_test_label(issue, chroot, tested_on) else: logging.info(f"Starting tests for chroot {chroot}") - request_id = tf.TestingFarmRequest.make( + request = tf.TestingFarmRequest.make( chroot=chroot, config=self.config, issue=issue, copr_build_ids=current_copr_build_ids, ) - logging.info(f"Request ID: {request_id}") - testing_farm_requests[chroot] = request_id - flip_test_label(issue, in_testing) + logging.info(f"Request ID: {request.request_id}") + requests[chroot] = request + self.flip_test_label(issue, chroot, in_testing) + # Create or update a comment for testing-farm results display if len(failed_test_cases) > 0: self.github.create_or_update_comment( issue=issue, @@ -226,12 +186,11 @@ def flip_test_label(issue: github.Issue.Issue, new_label: str | None): ), ) - logging.info("Reconstructing issue comment body") + logging.info("Constructing issue comment body") comment_body = f""" {self.github.initial_comment} {build_status_matrix} -{build_status.render_as_markdown(errors)} -{tf.TestingFarmRequest.dict_to_html_comment(testing_farm_requests)} +{tf.TestingFarmRequest.dict_to_html_comment(requests)} """ issue.edit(body=comment_body) @@ -245,7 +204,7 @@ def flip_test_label(issue: github.Issue.Issue, new_label: str | None): required_chroot_abels = [ "{self.config.label_prefix_tested_on}{chroot}" for chroot in all_chroots - if tf.is_chroot_supported(chroot) + if tf.TestingFarmRequest.is_chroot_supported(chroot) ] if set(tested_chroot_labels) == set(required_chroot_abels): msg = f"@{self.config.maintainer_handle}, all required packages have been successfully built and tested on all required chroots. We'll close this issue for you now as completed. Congratulations!" @@ -257,3 +216,90 @@ def flip_test_label(issue: github.Issue.Issue, new_label: str | None): logging.info("Cannot close issue yet.") logging.info(f"Updated today's issue: {issue.html_url}") + + def handle_labels( + self, + issue: github.Issue.Issue, + all_chroots: list[str], + errors: build_status.BuildStateList, + ): + logging.info("Gather labels based on the errors we've found") + error_labels = list({f"error/{err.err_cause}" for err in errors}) + project_labels = list({f"project/{err.package_name}" for err in errors}) + os_labels = list({f"os/{err.os}" for err in errors}) + arch_labels = list({f"arch/{err.arch}" for err in errors}) + strategy_labels = [f"strategy/{self.config.build_strategy}"] + other_labels: list[str] = [] + if errors is None and len(errors) > 0: + other_labels.append("broken_snapshot_detected") + + logging.info("Create labels") + self.github._create_labels( + labels=["broken_snapshot_detected"], color="F46696", prefix="" + ) + self.github.create_labels_for_error_causes(error_labels) + self.github.create_labels_for_oses(os_labels) + self.github.create_labels_for_projects(project_labels) + self.github.create_labels_for_archs(arch_labels) + self.github.create_labels_for_strategies(strategy_labels) + self.github.create_labels_for_in_testing(all_chroots) + self.github.create_labels_for_tested_on(all_chroots) + self.github.create_labels_for_failed_on(all_chroots) + + # Remove old labels from issue if they no longer apply. This is great + # for restarted builds for example to make all builds green and be able + # to promote this snapshot. + + labels_to_be_removed: list[str] = [] + old_error_labels = self.github.get_error_labels_on_issue(issue=issue) + old_project_labels = self.github.get_project_labels_on_issue(issue=issue) + old_arch_labels = self.github.get_arch_labels_on_issue(issue=issue) + + labels_to_be_removed.extend(set(old_error_labels) - set(error_labels)) + labels_to_be_removed.extend(set(old_project_labels) - set(project_labels)) + labels_to_be_removed.extend(set(old_arch_labels) - set(arch_labels)) + + for label in labels_to_be_removed: + logging.info(f"Removing label that no longer applies: {label}") + issue.remove_from_labels(label=label) + + # Labels must be added or removed manually in order to not remove manually added labels :/ + labels_to_add = ( + error_labels + + project_labels + + os_labels + + arch_labels + + strategy_labels + + other_labels + ) + logging.info(f"Adding label: {labels_to_add}") + issue.add_to_labels(*labels_to_add) + + def flip_test_label( + self, issue: github.Issue.Issue, chroot: str, new_label: str | None + ): + """Let's you change the label on an issue for a specific chroot. + + If `new_label` is `None`, then all test labels will be removed. + + Args: + issue (github.Issue.Issue): The issue to modify + chroot (str): The chroot for which you want to flip the test label + new_label (str | None): The new label or `None`. + """ + in_testing = f"{self.config.label_prefix_in_testing}{chroot}" + tested_on = f"{self.config.label_prefix_tested_on}{chroot}" + failed_on = f"{self.config.label_prefix_tested_on}{chroot}" + + all_states = [in_testing, tested_on, failed_on] + labels_to_be_removed = all_states + + if new_label is not None: + labels_to_be_removed = list(set(all_states).difference(set([new_label]))) + self.github.remove_labels_safe(issue, labels_to_be_removed) + + # Check if new_label is already set + if new_label is not None and new_label not in [ + label.name for label in issue.labels + ]: + issue.add_to_labels(new_label) diff --git a/snapshot_manager/snapshot_manager/testing_farm_util.py b/snapshot_manager/snapshot_manager/testing_farm_util.py index 82e3f379..388f2155 100644 --- a/snapshot_manager/snapshot_manager/testing_farm_util.py +++ b/snapshot_manager/snapshot_manager/testing_farm_util.py @@ -107,14 +107,15 @@ def parse(cls, string: str) -> dict[str, "TestingFarmRequest"]: >>> keys dict_keys(['fedora-rawhide-x86_64', 'fedora-39-x86_64', 'fedora-40-x86_64']) >>> requests['fedora-rawhide-x86_64'] - TestingFarmRequest(request_id=UUID('271a79e8-fc9a-4e1d-95fe-567cc9d62ad4'), chroot='fedora-rawhide-x86_64', copr_build_ids=['1', '2', '3']) + TestingFarmRequest(request_id=UUID('271a79e8-fc9a-4e1d-95fe-567cc9d62ad4'), chroot='fedora-rawhide-x86_64', copr_build_ids=[1, 2, 3]) >>> requests['fedora-39-x86_64'] - TestingFarmRequest(request_id=UUID('22222222-fc9a-4e1d-95fe-567cc9d62ad4'), chroot='fedora-39-x86_64', copr_build_ids=['5', '6', '7']) + TestingFarmRequest(request_id=UUID('22222222-fc9a-4e1d-95fe-567cc9d62ad4'), chroot='fedora-39-x86_64', copr_build_ids=[5, 6, 7]) >>> requests['fedora-40-x86_64'] - TestingFarmRequest(request_id=UUID('33333333-fc9a-4e1d-95fe-567cc9d62ad4'), chroot='fedora-40-x86_64', copr_build_ids=['12', '13', '14']) + TestingFarmRequest(request_id=UUID('33333333-fc9a-4e1d-95fe-567cc9d62ad4'), chroot='fedora-40-x86_64', copr_build_ids=[12, 13, 14]) """ matches = re.findall(r"", string) if not matches: + logging.info("No testing-farm requests found to recover.") return None res: dict[str, TestingFarmRequest] = {} @@ -124,12 +125,14 @@ def parse(cls, string: str) -> dict[str, "TestingFarmRequest"]: tfr = TestingFarmRequest( chroot=chroot, request_id=sanitize_request_id(str(match[1])), - copr_build_ids=[item.strip() for item in match[2].split(",")], + copr_build_ids=[int(item.strip()) for item in match[2].split(",")], ) res[chroot] = tfr logging.info(f"ADDED testing-farm request: {tfr}") except ValueError as e: logging.info(f"ignoring: {match} : {str(e)}") + + logging.info(f"Recovered testing-farm-requests: {res}") return res @classmethod @@ -166,7 +169,7 @@ def make( ranch = cls.select_ranch(chroot) - if not cls._is_chroot_supported(chroot=chroot, ranch=ranch): + if not cls.is_chroot_supported(chroot=chroot, ranch=ranch): raise ValueError( f"Chroot {chroot} has an unsupported architecture on ranch {ranch}" ) @@ -586,11 +589,11 @@ def render_html( return f'{title}{vpn}' -def sanitize_request_id(request_id: str) -> uuid.UUID: - """Sanitizes a testing-farm request ID by ensuring that it matches a pattern. +def sanitize_request_id(request_id: str | uuid.UUID) -> uuid.UUID: + """Sanitizes a testing-farm request ID by ensuring that it is a UUID. Args: - request_id (str): A testing-farm request ID + request_id (str | uuid.UUID): A testing-farm request ID Raises: ValueError: if the given string is not in the right format @@ -603,11 +606,17 @@ def sanitize_request_id(request_id: str) -> uuid.UUID: >>> sanitize_request_id(request_id="271a79e8-fc9a-4e1d-95fe-567cc9d62ad4") UUID('271a79e8-fc9a-4e1d-95fe-567cc9d62ad4') + >>> import uuid + >>> sanitize_request_id(uuid.UUID('271a79e8-fc9a-4e1d-95fe-567cc9d62ad5')) + UUID('271a79e8-fc9a-4e1d-95fe-567cc9d62ad5') + >>> sanitize_request_id(request_id="; cat /etc/passwd") Traceback (most recent call last): ... ValueError: string is not a valid testing-farm request ID: badly formed hexadecimal UUID string """ + if isinstance(request_id, uuid.UUID): + return request_id res: uuid.UUID = None try: res = uuid.UUID(request_id) @@ -675,7 +684,7 @@ def render_list_as_markdown(cls, test_cases: list["FailedTestCase"]) -> str:

Failed testing-farm test cases

-{"".join([ test_case.render_as_markdown for test_case in test_cases ])} +{"".join([ test_case.render_as_markdown() for test_case in test_cases ])} """ diff --git a/snapshot_manager/snapshot_manager/util.py b/snapshot_manager/snapshot_manager/util.py index 9526806c..09046f10 100644 --- a/snapshot_manager/snapshot_manager/util.py +++ b/snapshot_manager/snapshot_manager/util.py @@ -115,7 +115,7 @@ def run_cmd(cmd: str, timeout_secs: int = 5) -> tuple[int, str, str]: stderr = proc.stderr.decode() exit_code = proc.returncode if exit_code != 0: - logging.info( + logging.debug( f"exit code: {proc.returncode} for cmd: {cmd}\n\nstdout={stdout}\n\nstderr={stderr}" ) diff --git a/snapshot_manager/tests/snapshot_manager_test.py b/snapshot_manager/tests/snapshot_manager_test.py index 786beccc..ba479905 100644 --- a/snapshot_manager/tests/snapshot_manager_test.py +++ b/snapshot_manager/tests/snapshot_manager_test.py @@ -11,8 +11,8 @@ class TestSnapshotManager(base_test.TestBase): def test_check_todays_builds(self): # cfg = self.config # cfg.copr_ownername = "@fedora-llvm-team" - # cfg.copr_project_tpl = "llvm-snapshots-incubator-20240329" - # cfg.datetime = datetime.date(year=2024, month=3, day=29) + # cfg.copr_project_tpl = "llvm-snapshots-incubator-20240405" + # cfg.datetime = datetime.date(year=2024, month=4, day=5) # cfg.strategy = "standalone" # cfg.maintainer_handle = "kwk" # cfg.creator_handle = "kwk"