From 0cf8f9de9016ca13d2ea3f315c771418a5a43b1f Mon Sep 17 00:00:00 2001 From: Erin Drummond Date: Thu, 10 Jul 2025 01:36:35 +0000 Subject: [PATCH] Chore(cicd_bot): Show warnings/errors in the check output --- sqlmesh/core/console.py | 89 ++++++-- sqlmesh/integrations/github/cicd/command.py | 5 +- .../integrations/github/cicd/controller.py | 215 ++++++++++-------- tests/core/test_console.py | 131 +++++++++++ .../github/cicd/test_github_controller.py | 53 +++++ .../github/cicd/test_integration.py | 2 +- 6 files changed, 373 insertions(+), 122 deletions(-) create mode 100644 tests/core/test_console.py diff --git a/sqlmesh/core/console.py b/sqlmesh/core/console.py index 2f51a20a74..86de61c829 100644 --- a/sqlmesh/core/console.py +++ b/sqlmesh/core/console.py @@ -3049,34 +3049,47 @@ class CaptureTerminalConsole(TerminalConsole): def __init__(self, console: t.Optional[RichConsole] = None, **kwargs: t.Any) -> None: super().__init__(console=console, **kwargs) self._captured_outputs: t.List[str] = [] + self._warnings: t.List[str] = [] self._errors: t.List[str] = [] @property def captured_output(self) -> str: return "".join(self._captured_outputs) + @property + def captured_warnings(self) -> str: + return "".join(self._warnings) + @property def captured_errors(self) -> str: return "".join(self._errors) def consume_captured_output(self) -> str: - output = self.captured_output - self.clear_captured_outputs() - return output + try: + return self.captured_output + finally: + self._captured_outputs = [] - def consume_captured_errors(self) -> str: - errors = self.captured_errors - self.clear_captured_errors() - return errors + def consume_captured_warnings(self) -> str: + try: + return self.captured_warnings + finally: + self._warnings = [] - def clear_captured_outputs(self) -> None: - self._captured_outputs = [] + def consume_captured_errors(self) -> str: + try: + return self.captured_errors + finally: + self._errors = [] - def clear_captured_errors(self) -> None: - self._errors = [] + def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: + if short_message not in self._warnings: + self._warnings.append(short_message) + super().log_warning(short_message, long_message) def log_error(self, message: str) -> None: - self._errors.append(message) + if message not in self._errors: + self._errors.append(message) super().log_error(message) def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: @@ -3087,9 +3100,8 @@ def log_skipped_models(self, snapshot_names: t.Set[str]) -> None: super().log_skipped_models(snapshot_names) def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None: - if errors: - self._errors.append("\n".join(str(ex) for ex in errors)) - super().log_failed_models(errors) + self._errors.extend([str(ex) for ex in errors if str(ex) not in self._errors]) + super().log_failed_models(errors) def _print(self, value: t.Any, **kwargs: t.Any) -> None: with self.console.capture() as capture: @@ -3110,6 +3122,11 @@ class MarkdownConsole(CaptureTerminalConsole): AUDIT_PADDING = 7 def __init__(self, **kwargs: t.Any) -> None: + self.alert_block_max_content_length = int(kwargs.pop("alert_block_max_content_length", 500)) + self.alert_block_collapsible_threshold = int( + kwargs.pop("alert_block_collapsible_threshold", 200) + ) + super().__init__( **{**kwargs, "console": RichConsole(no_color=True, width=kwargs.pop("width", None))} ) @@ -3434,18 +3451,40 @@ def show_linter_violations( self._print(msg) self._errors.append(msg) - def log_error(self, message: str) -> None: - super().log_error(f"```\n\\[ERROR] {message}```\n\n") + @property + def captured_warnings(self) -> str: + return self._render_alert_block("WARNING", self._warnings) - def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None: - logger.warning(long_message or short_message) + @property + def captured_errors(self) -> str: + return self._render_alert_block("CAUTION", self._errors) - if not short_message.endswith("\n"): - short_message += ( - "\n" # so that the closing ``` ends up on a newline which is important for GitHub - ) + def _render_alert_block(self, block_type: str, items: t.List[str]) -> str: + # GitHub Markdown alert syntax, https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts + if items: + item_contents = "" + list_indicator = "- " if len(items) > 1 else "" + + for item in items: + item = item.replace("\n", "\n> ") + item_contents += f">\n> {list_indicator}{item}\n" + + if len(item_contents) > self.alert_block_max_content_length: + truncation_msg = ( + "...\n>\n> Truncated. Please check the console for full information.\n" + ) + item_contents = item_contents[ + 0 : self.alert_block_max_content_length - len(truncation_msg) + ] + item_contents += truncation_msg + break + + if len(item_contents) > self.alert_block_collapsible_threshold: + item_contents = f">
\n{item_contents}>
" + + return f"> [!{block_type}]\n{item_contents}\n" - self._print(f"```\n\\[WARNING] {short_message}```\n\n") + return "" def _print(self, value: t.Any, **kwargs: t.Any) -> None: self.console.print(value, **kwargs) @@ -3472,7 +3511,7 @@ def _print(self, value: t.Any, **kwargs: t.Any) -> None: super()._print(value, **kwargs) for captured_output in self._captured_outputs: print(captured_output) - self.clear_captured_outputs() + self.consume_captured_output() def _prompt(self, message: str, **kwargs: t.Any) -> t.Any: self._print(message) diff --git a/sqlmesh/integrations/github/cicd/command.py b/sqlmesh/integrations/github/cicd/command.py index 04f5757b50..1a4982e9e1 100644 --- a/sqlmesh/integrations/github/cicd/command.py +++ b/sqlmesh/integrations/github/cicd/command.py @@ -119,10 +119,7 @@ def _update_pr_environment(controller: GithubController) -> bool: except Exception as e: logger.exception("Error occurred when updating PR environment") conclusion = controller.update_pr_environment_check( - status=GithubCheckStatus.COMPLETED, - exception=e, - plan=controller.pr_plan_or_none, - plan_flags=controller.pr_plan_flags, + status=GithubCheckStatus.COMPLETED, exception=e ) return ( conclusion is not None diff --git a/sqlmesh/integrations/github/cicd/controller.py b/sqlmesh/integrations/github/cicd/controller.py index 19c494a979..7b03868243 100644 --- a/sqlmesh/integrations/github/cicd/controller.py +++ b/sqlmesh/integrations/github/cicd/controller.py @@ -494,7 +494,7 @@ def get_plan_summary(self, plan: Plan) -> str: try: # Clear out any output that might exist from prior steps - self._console.clear_captured_outputs() + self._console.consume_captured_output() if plan.restatements: self._console._print("\n**Restating models**\n") else: @@ -522,13 +522,115 @@ def get_plan_summary(self, plan: Plan) -> str: if not difference_summary and not missing_dates: return f"No changes to apply.{plan_flags_section}" - return f"{difference_summary}\n{missing_dates}{plan_flags_section}" + warnings_block = self._console.consume_captured_warnings() + errors_block = self._console.consume_captured_errors() + + return f"{warnings_block}{errors_block}{difference_summary}\n{missing_dates}{plan_flags_section}" except PlanError as e: logger.exception("Plan failed to generate") return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}" finally: self._console.verbosity = orig_verbosity + def get_pr_environment_summary( + self, conclusion: GithubCheckConclusion, exception: t.Optional[Exception] = None + ) -> str: + heading = "" + summary = "" + + if conclusion.is_success: + summary = self._get_pr_environment_summary_success() + elif conclusion.is_action_required: + heading = f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:" + summary = self._get_pr_environment_summary_action_required(exception) + elif conclusion.is_failure: + heading = ( + f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:" + ) + summary = self._get_pr_environment_summary_failure(exception) + elif conclusion.is_skipped: + heading = f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}` :next_track_button:" + summary = self._get_pr_environment_summary_skipped(exception) + else: + heading = f":interrobang: Got an unexpected conclusion: {conclusion.value}" + + # note: we just add warnings here, errors will be covered by the "failure" conclusion + if warnings := self._console.consume_captured_warnings(): + summary = f"{warnings}\n{summary}" + + return f"{heading}\n\n{summary}".strip() + + def _get_pr_environment_summary_success(self) -> str: + prod_plan = self.prod_plan_with_gaps + + if not prod_plan.has_changes: + summary = "No models were modified in this PR.\n" + else: + intro = self._generate_pr_environment_summary_intro() + summary = intro + self._generate_pr_environment_summary_list(prod_plan) + + if prod_plan.user_provided_flags: + summary += self._generate_plan_flags_section(prod_plan.user_provided_flags) + + return summary + + def _get_pr_environment_summary_skipped(self, exception: t.Optional[Exception] = None) -> str: + if isinstance(exception, NoChangesPlanError): + skip_reason = "No changes were detected compared to the prod environment." + elif isinstance(exception, TestFailure): + skip_reason = "Unit Test(s) Failed so skipping PR creation" + else: + skip_reason = "A prior stage failed resulting in skipping PR creation." + + return skip_reason + + def _get_pr_environment_summary_action_required( + self, exception: t.Optional[Exception] = None + ) -> str: + plan = self.pr_plan_or_none + if isinstance(exception, UncategorizedPlanError) and plan: + failure_msg = f"The following models could not be categorized automatically:\n" + for snapshot in plan.uncategorized: + failure_msg += f"- {snapshot.name}\n" + failure_msg += ( + f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n" + "If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." + ) + else: + failure_msg = "Please check the Actions Workflow logs for more information." + + return failure_msg + + def _get_pr_environment_summary_failure(self, exception: t.Optional[Exception] = None) -> str: + console_output = self._console.consume_captured_output() + + if isinstance(exception, PlanError): + failure_msg = f"Plan application failed.\n" + if exception.args and (msg := exception.args[0]) and isinstance(msg, str): + failure_msg += f"\n{msg}\n" + if console_output: + failure_msg += f"\n{console_output}" + elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): + # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message + # so cant be re-used here because it bypasses the Console + failure_msg = f"**Error:** {str(exception)}" + elif exception: + logger.debug( + "Got unexpected error. Error Type: " + + str(type(exception)) + + " Stack trace: " + + traceback.format_exc() + ) + failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```" + + if captured_errors := self._console.consume_captured_errors(): + failure_msg = f"{captured_errors}\n{failure_msg}" + + if plan_flags := self.pr_plan_flags: + failure_msg += f"\n\n{self._generate_plan_flags_section(plan_flags)}" + + return failure_msg + def run_tests(self) -> t.Tuple[ModelTextTestResult, str]: """ Run tests for the PR @@ -539,7 +641,7 @@ def run_linter(self) -> None: """ Run linter for the PR """ - self._console.clear_captured_outputs() + self._console.consume_captured_output() self._context.lint_models() def _get_or_create_comment(self, header: str = BOT_HEADER_MSG) -> IssueComment: @@ -611,7 +713,22 @@ def update_pr_environment(self) -> None: Creates a PR environment from the logic present in the PR. If the PR contains changes that are uncategorized, then an error will be raised. """ - self._context.apply(self.pr_plan) + self._context.apply(self.pr_plan) # will raise if PR environment creation fails + + # update PR info comment + vde_title = "- :eyes: To **review** this PR's changes, use virtual data environment:" + comment_value = f"{vde_title}\n - `{self.pr_environment_name}`" + if self.bot_config.enable_deploy_command: + comment_value += ( + "\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`" + ) + dedup_regex = vde_title.replace("*", r"\*") + r".*" + updated_comment, _ = self.update_sqlmesh_comment_info( + value=comment_value, + dedup_regex=dedup_regex, + ) + if updated_comment: + self._append_output("created_pr_environment", "true") def deploy_to_prod(self) -> None: """ @@ -855,13 +972,7 @@ def conclusion_handler( ) def update_pr_environment_check( - self, - status: GithubCheckStatus, - exception: t.Optional[Exception] = None, - plan: t.Optional[Plan] = None, - plan_flags: t.Optional[ - t.Dict[str, UserProvidedFlags] - ] = None, # note: the plan flags are passed separately in case the plan fails to build + self, status: GithubCheckStatus, exception: t.Optional[Exception] = None ) -> t.Optional[GithubCheckConclusion]: """ Updates the status of the merge commit for the PR environment. @@ -882,87 +993,7 @@ def update_pr_environment_check( def conclusion_handler( conclusion: GithubCheckConclusion, exception: t.Optional[Exception] ) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]: - if conclusion.is_success: - prod_plan = self.prod_plan_with_gaps - - if not prod_plan.has_changes: - summary = "No models were modified in this PR.\n" - else: - intro = self._generate_pr_environment_summary_intro() - summary = intro + self._generate_pr_environment_summary_list(prod_plan) - if prod_plan.user_provided_flags: - summary += self._generate_plan_flags_section(prod_plan.user_provided_flags) - - vde_title = ( - "- :eyes: To **review** this PR's changes, use virtual data environment:" - ) - comment_value = f"{vde_title}\n - `{self.pr_environment_name}`" - if self.bot_config.enable_deploy_command: - comment_value += "\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`" - dedup_regex = vde_title.replace("*", r"\*") + r".*" - updated_comment, _ = self.update_sqlmesh_comment_info( - value=comment_value, - dedup_regex=dedup_regex, - ) - if updated_comment: - self._append_output("created_pr_environment", "true") - else: - if isinstance(exception, NoChangesPlanError): - skip_reason = "No changes were detected compared to the prod environment." - elif isinstance(exception, TestFailure): - skip_reason = "Unit Test(s) Failed so skipping PR creation" - else: - skip_reason = "A prior stage failed resulting in skipping PR creation." - - if not skip_reason and exception: - logger.debug( - f"Got {type(exception).__name__}. Stack trace: " + traceback.format_exc() - ) - - captured_errors = self._console.consume_captured_errors() - if captured_errors: - logger.debug(f"Captured errors: {captured_errors}") - failure_msg = f"**Errors:**\n{captured_errors}\n" - elif isinstance(exception, UncategorizedPlanError) and plan: - failure_msg = f"The following models could not be categorized automatically:\n" - for snapshot in plan.uncategorized: - failure_msg += f"- {snapshot.name}\n" - failure_msg += ( - f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n" - "If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." - ) - elif isinstance(exception, PlanError): - failure_msg = f"Plan application failed.\n" - if exception.args and (msg := exception.args[0]) and isinstance(msg, str): - failure_msg += f"\n{msg}\n" - if self._console.captured_output: - failure_msg += f"\n{self._console.captured_output}" - elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)): - # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message - # so cant be re-used here because it bypasses the Console - failure_msg = f"**Error:** {str(exception)}" - else: - logger.debug( - "Got unexpected error. Error Type: " - + str(type(exception)) - + " Stack trace: " - + traceback.format_exc() - ) - failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```" - - conclusion_to_summary = { - GithubCheckConclusion.SKIPPED: f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}`. {skip_reason}", - GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:\n\n{failure_msg}", - GithubCheckConclusion.CANCELLED: f":stop_sign: Cancelled creating or updating PR Environment `{self.pr_environment_name}`", - GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:\n\n{failure_msg}", - } - summary = conclusion_to_summary.get( - conclusion, f":interrobang: Got an unexpected conclusion: {conclusion.value}" - ) - if plan_flags: - plan_flags_section = self._generate_plan_flags_section(plan_flags) - summary += f"\n\n{plan_flags_section}" - + summary = self.get_pr_environment_summary(conclusion, exception) self._append_output("pr_environment_name", self.pr_environment_name) return conclusion, check_title, summary diff --git a/tests/core/test_console.py b/tests/core/test_console.py new file mode 100644 index 0000000000..f899713235 --- /dev/null +++ b/tests/core/test_console.py @@ -0,0 +1,131 @@ +from sqlmesh.core.console import MarkdownConsole + + +def test_markdown_console_warning_block(): + console = MarkdownConsole( + alert_block_max_content_length=100, alert_block_collapsible_threshold=45 + ) + assert console.consume_captured_warnings() == "" + + # single warning, within threshold + console.log_warning("First warning") + assert console.consume_captured_warnings() == "> [!WARNING]\n>\n> First warning\n\n" + + # multiple warnings, within threshold (list syntax) + console.log_warning("First warning") + console.log_warning("Second warning") + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>\n> - First warning\n>\n> - Second warning\n\n" + ) + + # single warning, within max threshold but over collapsible section threshold + warning = "The snowflake engine is not recommended for storing SQLMesh state in production deployments" + assert len(warning) > console.alert_block_collapsible_threshold + assert len(warning) < console.alert_block_max_content_length + console.log_warning(warning) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
\n>\n> The snowflake engine is not recommended for storing SQLMesh state in production deployments\n>
\n" + ) + + # single warning, over max threshold + warning = "The snowflake engine is not recommended for storing SQLMesh state in production deployments. Please see for a list of recommended engines and more information." + assert len(warning) > console.alert_block_collapsible_threshold + assert len(warning) > console.alert_block_max_content_length + console.log_warning(warning) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
\n>\n> The snowflake engine is not re...\n>\n> Truncated. Please check the console for full information.\n>
\n" + ) + + # multiple warnings, within max threshold but over collapsible section threshold + warning_1 = "This is the first warning" + warning_2 = "This is the second warning" + assert (len(warning_1) + len(warning_2)) > console.alert_block_collapsible_threshold + assert (len(warning_1) + len(warning_2)) < console.alert_block_max_content_length + console.log_warning(warning_1) + console.log_warning(warning_2) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
\n>\n> - This is the first warning\n>\n> - This is the second warning\n>
\n" + ) + + # multiple warnings, over max threshold + warning_1 = "This is the first warning and its really really long" + warning_2 = "This is the second warning and its also really really long" + assert (len(warning_1) + len(warning_2)) > console.alert_block_collapsible_threshold + assert (len(warning_1) + len(warning_2)) > console.alert_block_max_content_length + console.log_warning(warning_1) + console.log_warning(warning_2) + assert ( + console.consume_captured_warnings() + == "> [!WARNING]\n>
\n>\n> - This is the first warning an...\n>\n> Truncated. Please check the console for full information.\n>
\n" + ) + + assert console.consume_captured_warnings() == "" + + +def test_markdown_console_error_block(): + console = MarkdownConsole( + alert_block_max_content_length=100, alert_block_collapsible_threshold=40 + ) + assert console.consume_captured_errors() == "" + + # single error, within threshold + console.log_error("First error") + assert console.consume_captured_errors() == "> [!CAUTION]\n>\n> First error\n\n" + + # multiple errors, within threshold (list syntax) + console.log_error("First error") + console.log_error("Second error") + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>\n> - First error\n>\n> - Second error\n\n" + ) + + # single error, within max threshold but over collapsible section threshold + error = "The snowflake engine is not recommended for storing SQLMesh state in production deployments" + assert len(error) > console.alert_block_collapsible_threshold + assert len(error) < console.alert_block_max_content_length + console.log_error(error) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
\n>\n> The snowflake engine is not recommended for storing SQLMesh state in production deployments\n>
\n" + ) + + # single error, over max threshold + error = "The snowflake engine is not recommended for storing SQLMesh state in production deployments. Please see for a list of recommended engines and more information." + assert len(error) > console.alert_block_collapsible_threshold + assert len(error) > console.alert_block_max_content_length + console.log_error(error) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
\n>\n> The snowflake engine is not re...\n>\n> Truncated. Please check the console for full information.\n>
\n" + ) + + # multiple errors, within max threshold but over collapsible section threshold + error_1 = "This is the first error" + error_2 = "This is the second error" + assert (len(error_1) + len(error_2)) > console.alert_block_collapsible_threshold + assert (len(error_1) + len(error_2)) < console.alert_block_max_content_length + console.log_error(error_1) + console.log_error(error_2) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
\n>\n> - This is the first error\n>\n> - This is the second error\n>
\n" + ) + + # multiple errors, over max threshold + error_1 = "This is the first error and its really really long" + error_2 = "This is the second error and its also really really long" + assert (len(error_1) + len(error_2)) > console.alert_block_collapsible_threshold + assert (len(error_1) + len(error_2)) > console.alert_block_max_content_length + console.log_error(error_1) + console.log_error(error_2) + assert ( + console.consume_captured_errors() + == "> [!CAUTION]\n>
\n>\n> - This is the first error and ...\n>\n> Truncated. Please check the console for full information.\n>
\n" + ) + + assert console.consume_captured_errors() == "" diff --git a/tests/integrations/github/cicd/test_github_controller.py b/tests/integrations/github/cicd/test_github_controller.py index fafc6f6291..099ed6d9ef 100644 --- a/tests/integrations/github/cicd/test_github_controller.py +++ b/tests/integrations/github/cicd/test_github_controller.py @@ -19,11 +19,13 @@ from sqlmesh.integrations.github.cicd.controller import ( BotCommand, MergeStateStatus, + GithubCheckConclusion, ) from sqlmesh.integrations.github.cicd.controller import GithubController from sqlmesh.integrations.github.cicd.command import _update_pr_environment from sqlmesh.utils.date import to_datetime, now from tests.integrations.github.cicd.conftest import MockIssueComment +from sqlmesh.utils.errors import SQLMeshError pytestmark = pytest.mark.github @@ -644,3 +646,54 @@ def test_get_plan_summary_doesnt_truncate_backfill_list( * `memory.sushi.waiter_revenue_by_day`: [2025-06-30 - 2025-07-06]""" in summary ) + + +def test_get_plan_summary_includes_warnings_and_errors( + github_client, make_controller: t.Callable[..., GithubController] +): + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, + ) + + controller._console.log_warning("Warning 1\nWith multiline") + controller._console.log_warning("Warning 2") + controller._console.log_error("Error 1") + + summary = controller.get_plan_summary(controller.prod_plan) + + assert ("> [!WARNING]\n>\n> - Warning 1\n> With multiline\n>\n> - Warning 2\n\n") in summary + + assert ("> [!CAUTION]\n>\n> Error 1\n\n") in summary + + +def test_get_pr_environment_summary_includes_warnings_and_errors( + github_client, make_controller: t.Callable[..., GithubController] +): + controller = make_controller( + "tests/fixtures/github/pull_request_synchronized.json", + github_client, + mock_out_context=False, + ) + + controller._console.log_warning("Warning 1") + controller._console.log_error("Error 1") + + # completed with no exception triggers a SUCCESS conclusion and only shows warnings + success_summary = controller.get_pr_environment_summary( + conclusion=GithubCheckConclusion.SUCCESS + ) + assert "> [!WARNING]\n>\n> Warning 1\n" in success_summary + assert "> [!CAUTION]\n>\n> Error 1" not in success_summary + + # since they got consumed in the previous call + controller._console.log_warning("Warning 1") + controller._console.log_error("Error 1") + + # completed with an exception triggers a FAILED conclusion and shows errors + error_summary = controller.get_pr_environment_summary( + conclusion=GithubCheckConclusion.FAILURE, exception=SQLMeshError("Something broke") + ) + assert "> [!WARNING]\n>\n> Warning 1\n" in error_summary + assert "> [!CAUTION]\n>\n> Error 1" in error_summary diff --git a/tests/integrations/github/cicd/test_integration.py b/tests/integrations/github/cicd/test_integration.py index ff9856993a..17e495fbc3 100644 --- a/tests/integrations/github/cicd/test_integration.py +++ b/tests/integrations/github/cicd/test_integration.py @@ -846,7 +846,7 @@ def test_merge_pr_has_no_changes( assert GithubCheckConclusion(pr_checks_runs[2]["conclusion"]).is_skipped assert pr_checks_runs[2]["output"]["title"] == "PR Virtual Data Environment: hello_world_2" assert ( - ":next_track_button: Skipped creating or updating PR Environment `hello_world_2`. No changes were detected compared to the prod environment." + ":next_track_button: Skipped creating or updating PR Environment `hello_world_2` :next_track_button:\n\nNo changes were detected compared to the prod environment." in pr_checks_runs[2]["output"]["summary"] )