Skip to content

Commit 3a11e14

Browse files
committed
Feat: Add CICD bot linter logging
1 parent 35216a2 commit 3a11e14

7 files changed

Lines changed: 225 additions & 22 deletions

File tree

sqlmesh/core/console.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2164,7 +2164,6 @@ def _show_categorized_snapshots(self, plan: Plan, default_catalog: t.Optional[st
21642164
def log_test_results(
21652165
self, result: unittest.result.TestResult, output: t.Optional[str], target_dialect: str
21662166
) -> None:
2167-
# import ipywidgets as widgets
21682167
if result.wasSuccessful():
21692168
self._print(
21702169
f"**Successfully Ran `{str(result.testsRun)}` Tests Against `{target_dialect}`**\n\n"

sqlmesh/core/context.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
load_configs,
6363
)
6464
from sqlmesh.core.config.loader import C
65-
from sqlmesh.core.console import get_console
65+
from sqlmesh.core.console import get_console, Console
6666
from sqlmesh.core.context_diff import ContextDiff
6767
from sqlmesh.core.dialect import (
6868
format_model_expressions,
@@ -2394,7 +2394,7 @@ def _get_models_for_interval_end(
23942394
)
23952395
return models_for_interval_end
23962396

2397-
def lint_models(self, models: t.Optional[t.Iterable[t.Union[str, Model]]] = None) -> None:
2397+
def lint_models(self, models: t.Optional[t.Iterable[t.Union[str, Model]]] = None, console: t.Optional[Console] = None) -> None:
23982398
found_error = False
23992399

24002400
model_list = (
@@ -2403,9 +2403,12 @@ def lint_models(self, models: t.Optional[t.Iterable[t.Union[str, Model]]] = None
24032403
for model in model_list:
24042404
# Linter may be `None` if the context is not loaded yet
24052405
if linter := self._linters.get(model.project):
2406-
found_error = linter.lint_model(model) or found_error
2406+
found_error = linter.lint_model(model, console=console) or found_error
24072407

24082408
if found_error:
2409+
import traceback
2410+
print(f"found error {found_error}")
2411+
traceback.print_stack()
24092412
raise LinterError(
24102413
"Linter detected errors in the code. Please fix them before proceeding."
24112414
)

sqlmesh/core/linter/definition.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from sqlmesh.core.model import Model
88

99
from sqlmesh.utils.errors import raise_config_error
10-
from sqlmesh.core.console import get_console
10+
from sqlmesh.core.console import get_console, Console
1111
from sqlmesh.core.linter.rule import RuleSet
1212

1313

@@ -50,7 +50,7 @@ def from_rules(cls, all_rules: RuleSet, config: LinterConfig) -> Linter:
5050

5151
return Linter(config.enabled, all_rules, rules, warn_rules)
5252

53-
def lint_model(self, model: Model) -> bool:
53+
def lint_model(self, model: Model, console: t.Optional[Console] = None) -> bool:
5454
if not self.enabled:
5555
return False
5656

@@ -62,11 +62,13 @@ def lint_model(self, model: Model) -> bool:
6262
error_violations = rules.check_model(model)
6363
warn_violations = warn_rules.check_model(model)
6464

65+
console = console or get_console()
66+
print(f"lint console {console}")
6567
if warn_violations:
66-
get_console().show_linter_violations(warn_violations, model)
68+
console.show_linter_violations(warn_violations, model)
6769

6870
if error_violations:
69-
get_console().show_linter_violations(error_violations, model, is_error=True)
71+
console.show_linter_violations(error_violations, model, is_error=True)
7072
return True
7173

7274
return False

sqlmesh/integrations/github/cicd/command.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
GithubController,
1414
TestFailure,
1515
)
16-
from sqlmesh.utils.errors import CICDBotError, ConflictingPlanError, PlanError
16+
from sqlmesh.utils.errors import CICDBotError, ConflictingPlanError, PlanError, LinterError
1717

1818
logger = logging.getLogger(__name__)
1919

@@ -79,6 +79,23 @@ def _run_tests(controller: GithubController) -> bool:
7979
)
8080
return False
8181

82+
def _run_linter(controller: GithubController) -> bool:
83+
controller.update_linter_check(status=GithubCheckStatus.IN_PROGRESS)
84+
try:
85+
controller.run_linter()
86+
except LinterError:
87+
controller.update_linter_check(
88+
status=GithubCheckStatus.COMPLETED,
89+
conclusion=GithubCheckConclusion.FAILURE,
90+
)
91+
return False
92+
93+
controller.update_linter_check(
94+
status=GithubCheckStatus.COMPLETED,
95+
conclusion=GithubCheckConclusion.SUCCESS,
96+
)
97+
98+
return True
8299

83100
@github.command()
84101
@click.pass_context
@@ -189,6 +206,7 @@ def deploy_production(ctx: click.Context) -> None:
189206

190207
def _run_all(controller: GithubController) -> None:
191208
has_required_approval = False
209+
192210
is_auto_deploying_prod = (
193211
controller.deploy_command_enabled or controller.do_required_approval_check
194212
)
@@ -204,11 +222,13 @@ def _run_all(controller: GithubController) -> None:
204222
has_required_approval = True
205223
else:
206224
raise CICDBotError(f"Unsupported command: {command}")
225+
controller.update_linter_check(status=GithubCheckStatus.QUEUED)
207226
controller.update_pr_environment_check(status=GithubCheckStatus.QUEUED)
208227
controller.update_prod_plan_preview_check(status=GithubCheckStatus.QUEUED)
209228
controller.update_test_check(status=GithubCheckStatus.QUEUED)
210229
if is_auto_deploying_prod:
211230
controller.update_prod_environment_check(status=GithubCheckStatus.QUEUED)
231+
linter_passed = _run_linter(controller)
212232
tests_passed = _run_tests(controller)
213233
if controller.do_required_approval_check:
214234
if has_required_approval:
@@ -218,23 +238,25 @@ def _run_all(controller: GithubController) -> None:
218238
else:
219239
controller.update_required_approval_check(status=GithubCheckStatus.QUEUED)
220240
has_required_approval = _check_required_approvers(controller)
221-
if not tests_passed:
241+
if not tests_passed or not linter_passed:
222242
controller.update_pr_environment_check(
223243
status=GithubCheckStatus.COMPLETED,
224-
exception=TestFailure(),
244+
exception=LinterError("") if not linter_passed else TestFailure(),
225245
)
226246
controller.update_prod_plan_preview_check(
227247
status=GithubCheckStatus.COMPLETED,
228248
conclusion=GithubCheckConclusion.SKIPPED,
229-
summary="Unit Test(s) Failed so skipping creating prod plan",
249+
summary=f"Linter or unit test(s) failed so skipping creating prod plan",
230250
)
231251
if is_auto_deploying_prod:
232252
controller.update_prod_environment_check(
233253
status=GithubCheckStatus.COMPLETED,
234254
conclusion=GithubCheckConclusion.SKIPPED,
235-
skip_reason="Unit Test(s) Failed so skipping deploying to production",
255+
skip_reason=f"Linter or unit test(s) Failed so skipping deploying to production",
236256
)
237-
raise CICDBotError("Failed to run tests. See check status for more information.")
257+
258+
raise CICDBotError(f"Linter or unit test(s) failed. See check status for more information.")
259+
238260
pr_environment_updated = _update_pr_environment(controller)
239261
prod_plan_generated = False
240262
if pr_environment_updated:

sqlmesh/integrations/github/cicd/controller.py

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
format_intervals,
3030
)
3131
from sqlmesh.core.user import User
32+
from sqlmesh.core.config import Config
3233
from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig
3334
from sqlmesh.utils import word_characters_only, Verbosity
3435
from sqlmesh.utils.concurrency import NodeExecutionFailedError
@@ -38,6 +39,7 @@
3839
NoChangesPlanError,
3940
PlanError,
4041
UncategorizedPlanError,
42+
LinterError,
4143
)
4244
from sqlmesh.utils.pydantic import PydanticModel
4345

@@ -50,8 +52,6 @@
5052
from github.PullRequestReview import PullRequestReview
5153
from github.Repository import Repository
5254

53-
from sqlmesh.core.config import Config
54-
5555
logger = logging.getLogger(__name__)
5656

5757

@@ -302,6 +302,7 @@ def __init__(
302302
self._prod_plan_builder: t.Optional[PlanBuilder] = None
303303
self._prod_plan_with_gaps_builder: t.Optional[PlanBuilder] = None
304304
self._check_run_mapping: t.Dict[str, CheckRun] = {}
305+
self._linter_error = False
305306

306307
if not isinstance(get_console(), MarkdownConsole):
307308
raise CICDBotError("Console must be a markdown console.")
@@ -326,10 +327,8 @@ def __init__(
326327
if review.state.lower() == "approved"
327328
}
328329
logger.debug(f"Approvers: {', '.join(self._approvers)}")
329-
self._context: Context = Context(
330-
paths=self._paths,
331-
config=self.config,
332-
)
330+
331+
self._context: Context = Context(paths=self._paths, config=self.config)
333332

334333
@property
335334
def deploy_command_enabled(self) -> bool:
@@ -394,6 +393,7 @@ def pr_plan(self) -> Plan:
394393
self._pr_plan_builder = self._context.plan_builder(
395394
environment=self.pr_environment_name,
396395
skip_tests=True,
396+
skip_linter=True,
397397
categorizer_config=self.bot_config.auto_categorize_changes,
398398
start=self.bot_config.default_pr_start,
399399
skip_backfill=self.bot_config.skip_pr_backfill,
@@ -408,6 +408,7 @@ def prod_plan(self) -> Plan:
408408
self._prod_plan_builder = self._context.plan_builder(
409409
c.PROD,
410410
no_gaps=True,
411+
skip_backfill=True,
411412
skip_tests=True,
412413
categorizer_config=self.bot_config.auto_categorize_changes,
413414
run=self.bot_config.run_on_deploy_to_prod,
@@ -478,6 +479,13 @@ def run_tests(self) -> t.Tuple[unittest.result.TestResult, str]:
478479
"""
479480
return self._context._run_tests(verbosity=Verbosity.VERBOSE)
480481

482+
def run_linter(self) -> None:
483+
"""
484+
Run linter for the PR
485+
"""
486+
# self._console.clear_captured_outputs()
487+
self._context.lint_models(console=self._console)
488+
481489
def _get_or_create_comment(self, header: str = BOT_HEADER_MSG) -> IssueComment:
482490
comment = seq_get(
483491
[comment for comment in self._issue.get_comments() if header in comment.body],
@@ -654,6 +662,34 @@ def _update_check_handler(
654662
full_summary=summary,
655663
)
656664

665+
def update_linter_check(
666+
self,
667+
status: GithubCheckStatus,
668+
conclusion: t.Optional[GithubCheckConclusion] = None,
669+
) -> None:
670+
def conclusion_handler(
671+
conclusion: GithubCheckConclusion,
672+
) -> t.Tuple[GithubCheckConclusion, str, t.Optional[str]]:
673+
linter_summary = self._console.consume_captured_output() or "Linter Success"
674+
675+
title = "Linter results"
676+
677+
return conclusion, title, linter_summary
678+
679+
self._update_check_handler(
680+
check_name="SQLMesh - Linter",
681+
status=status,
682+
conclusion=conclusion,
683+
status_handler=lambda status: (
684+
{
685+
GithubCheckStatus.IN_PROGRESS: "Running linter",
686+
GithubCheckStatus.QUEUED: "Waiting to Run linter",
687+
}[status],
688+
None,
689+
),
690+
conclusion_handler=conclusion_handler,
691+
)
692+
657693
def update_test_check(
658694
self,
659695
status: GithubCheckStatus,
@@ -751,7 +787,7 @@ def update_pr_environment_check(
751787
Updates the status of the merge commit for the PR environment.
752788
"""
753789
conclusion: t.Optional[GithubCheckConclusion] = None
754-
if isinstance(exception, (NoChangesPlanError, TestFailure)):
790+
if isinstance(exception, (NoChangesPlanError, TestFailure, LinterError)):
755791
conclusion = GithubCheckConclusion.SKIPPED
756792
elif isinstance(exception, UncategorizedPlanError):
757793
conclusion = GithubCheckConclusion.ACTION_REQUIRED

tests/integrations/github/cicd/fixtures.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44
from pytest_mock.plugin import MockerFixture
55

6+
from sqlmesh.core.config import Config
67
from sqlmesh.core.console import set_console, get_console, MarkdownConsole
78
from sqlmesh.integrations.github.cicd.config import GithubCICDBotConfig
89
from sqlmesh.integrations.github.cicd.controller import (
@@ -67,6 +68,7 @@ def _make_function(
6768
merge_state_status: MergeStateStatus = MergeStateStatus.CLEAN,
6869
bot_config: t.Optional[GithubCICDBotConfig] = None,
6970
mock_out_context: bool = True,
71+
config: t.Optional[t.Union[Config, str]] = None,
7072
) -> GithubController:
7173
if mock_out_context:
7274
mocker.patch("sqlmesh.core.context.Context.apply", mocker.MagicMock())
@@ -97,6 +99,7 @@ def _make_function(
9799
else GithubEvent.from_obj(event_path)
98100
),
99101
client=client,
102+
config=config,
100103
)
101104
finally:
102105
set_console(orig_console)

0 commit comments

Comments
 (0)