Skip to content

Commit bc3f02a

Browse files
committed
Fix(cicd_bot): Show warnings in the check output
1 parent 537a311 commit bc3f02a

File tree

5 files changed

+227
-124
lines changed

5 files changed

+227
-124
lines changed

sqlmesh/core/console.py

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3046,34 +3046,47 @@ class CaptureTerminalConsole(TerminalConsole):
30463046
def __init__(self, console: t.Optional[RichConsole] = None, **kwargs: t.Any) -> None:
30473047
super().__init__(console=console, **kwargs)
30483048
self._captured_outputs: t.List[str] = []
3049+
self._warnings: t.List[str] = []
30493050
self._errors: t.List[str] = []
30503051

30513052
@property
30523053
def captured_output(self) -> str:
30533054
return "".join(self._captured_outputs)
30543055

3056+
@property
3057+
def captured_warnings(self) -> str:
3058+
return "".join(self._warnings)
3059+
30553060
@property
30563061
def captured_errors(self) -> str:
30573062
return "".join(self._errors)
30583063

30593064
def consume_captured_output(self) -> str:
3060-
output = self.captured_output
3061-
self.clear_captured_outputs()
3062-
return output
3065+
try:
3066+
return self.captured_output
3067+
finally:
3068+
self._captured_outputs = []
30633069

3064-
def consume_captured_errors(self) -> str:
3065-
errors = self.captured_errors
3066-
self.clear_captured_errors()
3067-
return errors
3070+
def consume_captured_warnings(self) -> str:
3071+
try:
3072+
return self.captured_warnings
3073+
finally:
3074+
self._warnings = []
30683075

3069-
def clear_captured_outputs(self) -> None:
3070-
self._captured_outputs = []
3076+
def consume_captured_errors(self) -> str:
3077+
try:
3078+
return self.captured_errors
3079+
finally:
3080+
self._errors = []
30713081

3072-
def clear_captured_errors(self) -> None:
3073-
self._errors = []
3082+
def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
3083+
if short_message not in self._warnings:
3084+
self._warnings.append(short_message)
3085+
super().log_warning(short_message, long_message)
30743086

30753087
def log_error(self, message: str) -> None:
3076-
self._errors.append(message)
3088+
if message not in self._errors:
3089+
self._errors.append(message)
30773090
super().log_error(message)
30783091

30793092
def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
@@ -3084,9 +3097,8 @@ def log_skipped_models(self, snapshot_names: t.Set[str]) -> None:
30843097
super().log_skipped_models(snapshot_names)
30853098

30863099
def log_failed_models(self, errors: t.List[NodeExecutionFailedError]) -> None:
3087-
if errors:
3088-
self._errors.append("\n".join(str(ex) for ex in errors))
3089-
super().log_failed_models(errors)
3100+
self._errors.extend([str(ex) for ex in errors])
3101+
super().log_failed_models(errors)
30903102

30913103
def _print(self, value: t.Any, **kwargs: t.Any) -> None:
30923104
with self.console.capture() as capture:
@@ -3431,18 +3443,25 @@ def show_linter_violations(
34313443
self._print(msg)
34323444
self._errors.append(msg)
34333445

3434-
def log_error(self, message: str) -> None:
3435-
super().log_error(f"```\n\\[ERROR] {message}```\n\n")
3436-
3437-
def log_warning(self, short_message: str, long_message: t.Optional[str] = None) -> None:
3438-
logger.warning(long_message or short_message)
3439-
3440-
if not short_message.endswith("\n"):
3441-
short_message += (
3442-
"\n" # so that the closing ``` ends up on a newline which is important for GitHub
3443-
)
3446+
@property
3447+
def captured_warnings(self) -> str:
3448+
return self._render_alert_block("WARNING", self._warnings)
34443449

3445-
self._print(f"```\n\\[WARNING] {short_message}```\n\n")
3450+
@property
3451+
def captured_errors(self) -> str:
3452+
return self._render_alert_block("ERROR", self._errors)
3453+
3454+
def _render_alert_block(self, block_type: str, items: t.List[str]) -> str:
3455+
# 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
3456+
if items:
3457+
block = f"> [!{block_type}]\n"
3458+
list_indicator = "- " if len(items) > 1 else ""
3459+
for item in items:
3460+
item = item.replace("\n", "\n> ")
3461+
block += f"> {list_indicator}{item}\n"
3462+
block += "\n"
3463+
return block
3464+
return ""
34463465

34473466
def _print(self, value: t.Any, **kwargs: t.Any) -> None:
34483467
self.console.print(value, **kwargs)
@@ -3469,7 +3488,7 @@ def _print(self, value: t.Any, **kwargs: t.Any) -> None:
34693488
super()._print(value, **kwargs)
34703489
for captured_output in self._captured_outputs:
34713490
print(captured_output)
3472-
self.clear_captured_outputs()
3491+
self.consume_captured_output()
34733492

34743493
def _prompt(self, message: str, **kwargs: t.Any) -> t.Any:
34753494
self._print(message)

sqlmesh/integrations/github/cicd/command.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,7 @@ def _update_pr_environment(controller: GithubController) -> bool:
119119
except Exception as e:
120120
logger.exception("Error occurred when updating PR environment")
121121
conclusion = controller.update_pr_environment_check(
122-
status=GithubCheckStatus.COMPLETED,
123-
exception=e,
124-
plan=controller.pr_plan_or_none,
125-
plan_flags=controller.pr_plan_flags,
122+
status=GithubCheckStatus.COMPLETED, exception=e
126123
)
127124
return (
128125
conclusion is not None

sqlmesh/integrations/github/cicd/controller.py

Lines changed: 126 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ def get_plan_summary(self, plan: Plan) -> str:
494494

495495
try:
496496
# Clear out any output that might exist from prior steps
497-
self._console.clear_captured_outputs()
497+
self._console.consume_captured_output()
498498
if plan.restatements:
499499
self._console._print("\n**Restating models**\n")
500500
else:
@@ -522,13 +522,118 @@ def get_plan_summary(self, plan: Plan) -> str:
522522
if not difference_summary and not missing_dates:
523523
return f"No changes to apply.{plan_flags_section}"
524524

525-
return f"{difference_summary}\n{missing_dates}{plan_flags_section}"
525+
warnings_block = self._console.captured_warnings
526+
errors_block = self._console.captured_errors
527+
528+
return f"{warnings_block}{errors_block}{difference_summary}\n{missing_dates}{plan_flags_section}"
526529
except PlanError as e:
527530
logger.exception("Plan failed to generate")
528531
return f"Plan failed to generate. Check for pending or unresolved changes. Error: {e}"
529532
finally:
530533
self._console.verbosity = orig_verbosity
531534

535+
def get_pr_environment_summary(
536+
self, conclusion: GithubCheckConclusion, exception: t.Optional[Exception] = None
537+
) -> str:
538+
heading = ""
539+
summary = ""
540+
541+
if conclusion.is_success:
542+
summary = self._get_pr_environment_summary_success()
543+
elif conclusion.is_action_required:
544+
heading = f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:"
545+
summary = self._get_pr_environment_summary_action_required(exception)
546+
elif conclusion.is_failure:
547+
heading = (
548+
f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:"
549+
)
550+
summary = self._get_pr_environment_summary_failure(exception)
551+
elif conclusion.is_skipped:
552+
heading = f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}` :next_track_button:"
553+
summary = self._get_pr_environment_summary_skipped(exception)
554+
else:
555+
heading = f":interrobang: Got an unexpected conclusion: {conclusion.value}"
556+
557+
# note: we just add warnings here, errors will be covered by the "failure" conclusion
558+
if warnings := self._console.consume_captured_warnings():
559+
summary = f"{warnings}\n{summary}"
560+
561+
return f"{heading}\n\n{summary}".strip()
562+
563+
def _get_pr_environment_summary_success(self) -> str:
564+
prod_plan = self.prod_plan_with_gaps
565+
566+
if not prod_plan.has_changes:
567+
summary = "No models were modified in this PR.\n"
568+
else:
569+
intro = self._generate_pr_environment_summary_intro()
570+
summary = intro + self._generate_pr_environment_summary_list(prod_plan)
571+
572+
if prod_plan.user_provided_flags:
573+
summary += self._generate_plan_flags_section(prod_plan.user_provided_flags)
574+
575+
return summary
576+
577+
def _get_pr_environment_summary_skipped(self, exception: t.Optional[Exception] = None) -> str:
578+
if isinstance(exception, NoChangesPlanError):
579+
skip_reason = "No changes were detected compared to the prod environment."
580+
elif isinstance(exception, TestFailure):
581+
skip_reason = "Unit Test(s) Failed so skipping PR creation"
582+
else:
583+
skip_reason = "A prior stage failed resulting in skipping PR creation."
584+
585+
return skip_reason
586+
587+
def _get_pr_environment_summary_action_required(
588+
self, exception: t.Optional[Exception] = None
589+
) -> str:
590+
plan = self.pr_plan_or_none
591+
if isinstance(exception, UncategorizedPlanError) and plan:
592+
failure_msg = f"The following models could not be categorized automatically:\n"
593+
for snapshot in plan.uncategorized:
594+
failure_msg += f"- {snapshot.name}\n"
595+
failure_msg += (
596+
f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n"
597+
"If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information."
598+
)
599+
else:
600+
failure_msg = "Please check the Actions Workflow logs for more information."
601+
602+
return failure_msg
603+
604+
def _get_pr_environment_summary_failure(self, exception: t.Optional[Exception] = None) -> str:
605+
if exception:
606+
logger.debug(f"Got {type(exception).__name__}. Stack trace: " + traceback.format_exc())
607+
608+
console_output = self._console.consume_captured_output()
609+
610+
if isinstance(exception, PlanError):
611+
failure_msg = f"Plan application failed.\n"
612+
if exception.args and (msg := exception.args[0]) and isinstance(msg, str):
613+
failure_msg += f"\n{msg}\n"
614+
if console_output:
615+
failure_msg += f"\n{console_output}"
616+
elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)):
617+
# this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message
618+
# so cant be re-used here because it bypasses the Console
619+
failure_msg = f"**Error:** {str(exception)}"
620+
else:
621+
logger.debug(
622+
"Got unexpected error. Error Type: "
623+
+ str(type(exception))
624+
+ " Stack trace: "
625+
+ traceback.format_exc()
626+
)
627+
failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```"
628+
629+
if captured_errors := self._console.consume_captured_errors():
630+
failure_msg = f"{captured_errors}\n{failure_msg}"
631+
632+
if plan_flags := self.pr_plan_flags:
633+
failure_msg += f"\n\n{self._generate_plan_flags_section(plan_flags)}"
634+
635+
return failure_msg
636+
532637
def run_tests(self) -> t.Tuple[ModelTextTestResult, str]:
533638
"""
534639
Run tests for the PR
@@ -539,7 +644,7 @@ def run_linter(self) -> None:
539644
"""
540645
Run linter for the PR
541646
"""
542-
self._console.clear_captured_outputs()
647+
self._console.consume_captured_output()
543648
self._context.lint_models()
544649

545650
def _get_or_create_comment(self, header: str = BOT_HEADER_MSG) -> IssueComment:
@@ -611,7 +716,22 @@ def update_pr_environment(self) -> None:
611716
Creates a PR environment from the logic present in the PR. If the PR contains changes that are
612717
uncategorized, then an error will be raised.
613718
"""
614-
self._context.apply(self.pr_plan)
719+
self._context.apply(self.pr_plan) # will raise if PR environment creation fails
720+
721+
# update PR info comment
722+
vde_title = "- :eyes: To **review** this PR's changes, use virtual data environment:"
723+
comment_value = f"{vde_title}\n - `{self.pr_environment_name}`"
724+
if self.bot_config.enable_deploy_command:
725+
comment_value += (
726+
"\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`"
727+
)
728+
dedup_regex = vde_title.replace("*", r"\*") + r".*"
729+
updated_comment, _ = self.update_sqlmesh_comment_info(
730+
value=comment_value,
731+
dedup_regex=dedup_regex,
732+
)
733+
if updated_comment:
734+
self._append_output("created_pr_environment", "true")
615735

616736
def deploy_to_prod(self) -> None:
617737
"""
@@ -855,13 +975,7 @@ def conclusion_handler(
855975
)
856976

857977
def update_pr_environment_check(
858-
self,
859-
status: GithubCheckStatus,
860-
exception: t.Optional[Exception] = None,
861-
plan: t.Optional[Plan] = None,
862-
plan_flags: t.Optional[
863-
t.Dict[str, UserProvidedFlags]
864-
] = None, # note: the plan flags are passed separately in case the plan fails to build
978+
self, status: GithubCheckStatus, exception: t.Optional[Exception] = None
865979
) -> t.Optional[GithubCheckConclusion]:
866980
"""
867981
Updates the status of the merge commit for the PR environment.
@@ -882,87 +996,7 @@ def update_pr_environment_check(
882996
def conclusion_handler(
883997
conclusion: GithubCheckConclusion, exception: t.Optional[Exception]
884998
) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]:
885-
if conclusion.is_success:
886-
prod_plan = self.prod_plan_with_gaps
887-
888-
if not prod_plan.has_changes:
889-
summary = "No models were modified in this PR.\n"
890-
else:
891-
intro = self._generate_pr_environment_summary_intro()
892-
summary = intro + self._generate_pr_environment_summary_list(prod_plan)
893-
if prod_plan.user_provided_flags:
894-
summary += self._generate_plan_flags_section(prod_plan.user_provided_flags)
895-
896-
vde_title = (
897-
"- :eyes: To **review** this PR's changes, use virtual data environment:"
898-
)
899-
comment_value = f"{vde_title}\n - `{self.pr_environment_name}`"
900-
if self.bot_config.enable_deploy_command:
901-
comment_value += "\n- :arrow_forward: To **apply** this PR's plan to prod, comment:\n - `/deploy`"
902-
dedup_regex = vde_title.replace("*", r"\*") + r".*"
903-
updated_comment, _ = self.update_sqlmesh_comment_info(
904-
value=comment_value,
905-
dedup_regex=dedup_regex,
906-
)
907-
if updated_comment:
908-
self._append_output("created_pr_environment", "true")
909-
else:
910-
if isinstance(exception, NoChangesPlanError):
911-
skip_reason = "No changes were detected compared to the prod environment."
912-
elif isinstance(exception, TestFailure):
913-
skip_reason = "Unit Test(s) Failed so skipping PR creation"
914-
else:
915-
skip_reason = "A prior stage failed resulting in skipping PR creation."
916-
917-
if not skip_reason and exception:
918-
logger.debug(
919-
f"Got {type(exception).__name__}. Stack trace: " + traceback.format_exc()
920-
)
921-
922-
captured_errors = self._console.consume_captured_errors()
923-
if captured_errors:
924-
logger.debug(f"Captured errors: {captured_errors}")
925-
failure_msg = f"**Errors:**\n{captured_errors}\n"
926-
elif isinstance(exception, UncategorizedPlanError) and plan:
927-
failure_msg = f"The following models could not be categorized automatically:\n"
928-
for snapshot in plan.uncategorized:
929-
failure_msg += f"- {snapshot.name}\n"
930-
failure_msg += (
931-
f"\nRun `sqlmesh plan {self.pr_environment_name}` locally to apply these changes.\n\n"
932-
"If you would like the bot to automatically categorize changes, check the [documentation](https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information."
933-
)
934-
elif isinstance(exception, PlanError):
935-
failure_msg = f"Plan application failed.\n"
936-
if exception.args and (msg := exception.args[0]) and isinstance(msg, str):
937-
failure_msg += f"\n{msg}\n"
938-
if self._console.captured_output:
939-
failure_msg += f"\n{self._console.captured_output}"
940-
elif isinstance(exception, (SQLMeshError, SqlglotError, ValueError)):
941-
# this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message
942-
# so cant be re-used here because it bypasses the Console
943-
failure_msg = f"**Error:** {str(exception)}"
944-
else:
945-
logger.debug(
946-
"Got unexpected error. Error Type: "
947-
+ str(type(exception))
948-
+ " Stack trace: "
949-
+ traceback.format_exc()
950-
)
951-
failure_msg = f"This is an unexpected error.\n\n**Exception:**\n```\n{traceback.format_exc()}\n```"
952-
953-
conclusion_to_summary = {
954-
GithubCheckConclusion.SKIPPED: f":next_track_button: Skipped creating or updating PR Environment `{self.pr_environment_name}`. {skip_reason}",
955-
GithubCheckConclusion.FAILURE: f":x: Failed to create or update PR Environment `{self.pr_environment_name}` :x:\n\n{failure_msg}",
956-
GithubCheckConclusion.CANCELLED: f":stop_sign: Cancelled creating or updating PR Environment `{self.pr_environment_name}`",
957-
GithubCheckConclusion.ACTION_REQUIRED: f":warning: Action Required to create or update PR Environment `{self.pr_environment_name}` :warning:\n\n{failure_msg}",
958-
}
959-
summary = conclusion_to_summary.get(
960-
conclusion, f":interrobang: Got an unexpected conclusion: {conclusion.value}"
961-
)
962-
if plan_flags:
963-
plan_flags_section = self._generate_plan_flags_section(plan_flags)
964-
summary += f"\n\n{plan_flags_section}"
965-
999+
summary = self.get_pr_environment_summary(conclusion, exception)
9661000
self._append_output("pr_environment_name", self.pr_environment_name)
9671001
return conclusion, check_title, summary
9681002

0 commit comments

Comments
 (0)