1111import unittest
1212from enum import Enum
1313from typing import List
14+ from pathlib import Path
1415
1516import requests
1617from hyperscript import Element , h
2829 SnapshotTableInfo ,
2930 format_intervals ,
3031)
32+ from sqlglot .errors import SqlglotError
3133from sqlmesh .core .user import User
3234from sqlmesh .core .config import Config
3335from sqlmesh .integrations .github .cicd .config import GithubCICDBotConfig
3436from sqlmesh .utils import word_characters_only , Verbosity
35- from sqlmesh .utils .concurrency import NodeExecutionFailedError
3637from sqlmesh .utils .date import now
3738from sqlmesh .utils .errors import (
3839 CICDBotError ,
3940 NoChangesPlanError ,
4041 PlanError ,
4142 UncategorizedPlanError ,
4243 LinterError ,
44+ SQLMeshError ,
4345)
4446from sqlmesh .utils .pydantic import PydanticModel
4547
@@ -283,7 +285,7 @@ class GithubController:
283285
284286 def __init__ (
285287 self ,
286- paths : t .Union [str , t .Iterable [str ]],
288+ paths : t .Union [Path , t .Iterable [Path ]],
287289 token : str ,
288290 config : t .Optional [t .Union [Config , str ]] = None ,
289291 event : t .Optional [GithubEvent ] = None ,
@@ -307,10 +309,13 @@ def __init__(
307309 raise CICDBotError ("Console must be a markdown console." )
308310 self ._console = t .cast (MarkdownConsole , get_console ())
309311
312+ from github .Consts import DEFAULT_BASE_URL
313+ from github .Auth import Token
314+
310315 self ._client : Github = client or Github (
311- base_url = os .environ ["GITHUB_API_URL" ],
312- login_or_token = self ._token ,
316+ base_url = os .environ .get ("GITHUB_API_URL" , DEFAULT_BASE_URL ), auth = Token (self ._token )
313317 )
318+
314319 self ._repo : Repository = self ._client .get_repo (
315320 self ._event .pull_request_info .full_repo_path , lazy = True
316321 )
@@ -328,6 +333,9 @@ def __init__(
328333 logger .debug (f"Approvers: { ', ' .join (self ._approvers )} " )
329334 self ._context : Context = Context (paths = self ._paths , config = self .config )
330335
336+ # Bot config needs the context to be initialized
337+ logger .debug (f"Bot config: { self .bot_config .json (indent = 2 )} " )
338+
331339 @property
332340 def deploy_command_enabled (self ) -> bool :
333341 return self .bot_config .enable_deploy_command
@@ -433,7 +441,6 @@ def bot_config(self) -> GithubCICDBotConfig:
433441 bot_config = self ._context .config .cicd_bot or GithubCICDBotConfig (
434442 auto_categorize_changes = self ._context .auto_categorize_changes
435443 )
436- logger .debug (f"Bot config: { bot_config .json (indent = 2 )} " )
437444 return bot_config
438445
439446 @property
@@ -454,8 +461,11 @@ def _append_output(cls, key: str, value: str) -> None:
454461 Appends the given key/value to output so they can be read by following steps
455462 """
456463 logger .debug (f"Setting output. Key: { key } , Value: { value } " )
457- with open (os .environ ["GITHUB_OUTPUT" ], "a" , encoding = "utf-8" ) as fh :
458- print (f"{ key } ={ value } " , file = fh )
464+
465+ # GitHub Actions sets this environment variable
466+ if output_file := os .environ .get ("GITHUB_OUTPUT" ):
467+ with open (output_file , "a" , encoding = "utf-8" ) as fh :
468+ print (f"{ key } ={ value } " , file = fh )
459469
460470 def get_plan_summary (self , plan : Plan ) -> str :
461471 try :
@@ -637,15 +647,31 @@ def _update_check(
637647 if text :
638648 kwargs ["output" ]["text" ] = text
639649 logger .debug (f"Updating check with kwargs: { kwargs } " )
640- if name in self ._check_run_mapping :
641- logger .debug (f"Found check run in mapping so updating it. Name: { name } " )
642- check_run = self ._check_run_mapping [name ]
643- check_run .edit (
644- ** {k : v for k , v in kwargs .items () if k not in ("name" , "head_sha" , "started_at" )}
645- )
650+
651+ if self .running_in_github_actions :
652+ # Only make the API call to update the checks if we are running within GitHub Actions
653+ # One very annoying limitation of the Pull Request Checks API is that its only available to GitHub Apps
654+ # and not personal access tokens, which makes it unable to be utilized during local development
655+ if name in self ._check_run_mapping :
656+ logger .debug (f"Found check run in mapping so updating it. Name: { name } " )
657+ check_run = self ._check_run_mapping [name ]
658+ check_run .edit (
659+ ** {
660+ k : v
661+ for k , v in kwargs .items ()
662+ if k not in ("name" , "head_sha" , "started_at" )
663+ }
664+ )
665+ else :
666+ logger .debug (f"Did not find check run in mapping so creating it. Name: { name } " )
667+ self ._check_run_mapping [name ] = self ._repo .create_check_run (** kwargs )
646668 else :
647- logger .debug (f"Did not find check run in mapping so creating it. Name: { name } " )
648- self ._check_run_mapping [name ] = self ._repo .create_check_run (** kwargs )
669+ # Output the summary using print() so the newlines are resolved and the result can easily
670+ # be disambiguated from the rest of the console output and copy+pasted into a Markdown renderer
671+ print (
672+ f"---CHECK OUTPUT START: { kwargs ['output' ]['title' ]} ---\n { kwargs ['output' ]['summary' ]} \n ---CHECK OUTPUT END---\n "
673+ )
674+
649675 if conclusion :
650676 self ._append_output (
651677 word_characters_only (name .replace ("SQLMesh - " , "" ).lower ()), conclusion .value
@@ -890,15 +916,21 @@ def conclusion_handler(
890916 else :
891917 skip_reason = "A prior stage failed resulting in skipping PR creation."
892918
919+ if not skip_reason and exception :
920+ logger .debug (
921+ f"Got { type (exception ).__name__ } . Stack trace: " + traceback .format_exc ()
922+ )
923+
893924 captured_errors = self ._console .consume_captured_errors ()
894925 if captured_errors :
895926 logger .debug (f"Captured errors: { captured_errors } " )
896927 failure_msg = f"**Errors:**\n { captured_errors } \n "
897- elif isinstance (exception , NodeExecutionFailedError ):
898- logger .debug (
899- "Got Node Execution Failed Error. Stack trace: " + traceback .format_exc ()
900- )
901- failure_msg = f"Node `{ exception .node .name } ` failed to apply.\n \n **Stack Trace:**\n ```\n { traceback .format_exc ()} \n ```"
928+ elif isinstance (exception , PlanError ):
929+ failure_msg = f"Plan application failed.\n \n { self ._console .captured_output } "
930+ elif isinstance (exception , (SQLMeshError , SqlglotError , ValueError )):
931+ # this logic is taken from the global error handler attached to the CLI, which uses `click.echo()` to output the message
932+ # so cant be re-used here because it bypasses the Console
933+ failure_msg = f"**Error:** { str (exception )} "
902934 else :
903935 logger .debug (
904936 "Got unexpected error. Error Type: "
@@ -909,7 +941,7 @@ def conclusion_handler(
909941 failure_msg = f"This is an unexpected error.\n \n **Exception:**\n ```\n { traceback .format_exc ()} \n ```"
910942 conclusion_to_summary = {
911943 GithubCheckConclusion .SKIPPED : f":next_track_button: Skipped creating or updating PR Environment `{ self .pr_environment_name } `. { skip_reason } " ,
912- GithubCheckConclusion .FAILURE : f":x: Failed to create or update PR Environment `{ self .pr_environment_name } `.\n { failure_msg } " ,
944+ GithubCheckConclusion .FAILURE : f":x: Failed to create or update PR Environment `{ self .pr_environment_name } `.\n \n { failure_msg } " ,
913945 GithubCheckConclusion .CANCELLED : f":stop_sign: Cancelled creating or updating PR Environment `{ self .pr_environment_name } `" ,
914946 GithubCheckConclusion .ACTION_REQUIRED : f":warning: Action Required to create or update PR Environment `{ self .pr_environment_name } `. There are likely uncateogrized changes. Run `plan` locally to apply these changes. If you want the bot to automatically categorize changes, then check documentation (https://sqlmesh.readthedocs.io/en/stable/integrations/github/) for more information." ,
915947 }
@@ -1061,3 +1093,7 @@ def _chunk_up_api_message(self, message: str) -> t.List[str]:
10611093 message_encoded [i : i + self .MAX_BYTE_LENGTH ].decode ("utf-8" , "ignore" )
10621094 for i in range (0 , len (message_encoded ), self .MAX_BYTE_LENGTH )
10631095 ]
1096+
1097+ @property
1098+ def running_in_github_actions (self ) -> bool :
1099+ return os .environ .get ("GITHUB_ACTIONS" , None ) == "true"
0 commit comments