Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93becd5020 | ||
|
|
af967210c6 | ||
|
|
6c3deb1c6e | ||
|
|
e3e7e32a05 | ||
|
|
a8ae4c20b0 | ||
|
|
5837275581 | ||
|
|
bd07e05691 | ||
|
|
066d38d738 | ||
|
|
106a44571c |
@ -4,7 +4,7 @@ on:
|
|||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [opened, synchronize, ready_for_review, reopened]
|
types: [opened, reopened, ready_for_review]
|
||||||
pull_request_review:
|
pull_request_review:
|
||||||
types: [submitted]
|
types: [submitted]
|
||||||
pull_request_review_comment:
|
pull_request_review_comment:
|
||||||
@ -14,10 +14,8 @@ jobs:
|
|||||||
claude-bot:
|
claude-bot:
|
||||||
runs-on: native
|
runs-on: native
|
||||||
if: >-
|
if: >-
|
||||||
(github.event_name == 'pull_request') ||
|
github.actor != 'obsidiandeploy' &&
|
||||||
(github.event_name == 'pull_request_review') ||
|
(github.event_name != 'issue_comment' || github.event.issue.pull_request)
|
||||||
(github.event_name == 'pull_request_review_comment') ||
|
|
||||||
(github.event_name == 'issue_comment' && github.event.issue.pull_request)
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Run Claude Bot
|
- name: Run Claude Bot
|
||||||
|
|||||||
@ -11,10 +11,7 @@ import Data.ByteString.Lazy qualified as BL
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Data.Text.Encoding qualified as TE
|
import Data.Text.Encoding qualified as TE
|
||||||
import Data.Text.IO qualified as TIO
|
|
||||||
import System.Directory (getTemporaryDirectory, removeFile)
|
|
||||||
import System.Exit (ExitCode (..))
|
import System.Exit (ExitCode (..))
|
||||||
import System.FilePath ((</>))
|
|
||||||
import System.Process (readProcessWithExitCode)
|
import System.Process (readProcessWithExitCode)
|
||||||
|
|
||||||
data ReviewOutput = ReviewOutput
|
data ReviewOutput = ReviewOutput
|
||||||
@ -68,20 +65,15 @@ reviewJsonSchema = unlines
|
|||||||
|
|
||||||
invokeClaudeReview :: Text -> IO (Either String ReviewOutput)
|
invokeClaudeReview :: Text -> IO (Either String ReviewOutput)
|
||||||
invokeClaudeReview prompt = do
|
invokeClaudeReview prompt = do
|
||||||
tmpDir <- getTemporaryDirectory
|
|
||||||
let promptFile = tmpDir </> "claude-review-prompt.txt"
|
|
||||||
schemaFile = tmpDir </> "claude-review-schema.json"
|
|
||||||
TIO.writeFile promptFile prompt
|
|
||||||
writeFile schemaFile reviewJsonSchema
|
|
||||||
(exitCode, stdout, stderr_) <- readProcessWithExitCode "claude"
|
(exitCode, stdout, stderr_) <- readProcessWithExitCode "claude"
|
||||||
[ "-p"
|
[ "-p", T.unpack prompt
|
||||||
, "--output-format", "json"
|
, "--output-format", "json"
|
||||||
, "--max-turns", "1"
|
, "--max-turns", "3"
|
||||||
, "--prompt-file", promptFile
|
, "--tools", ""
|
||||||
, "--json-schema", schemaFile
|
, "--json-schema", reviewJsonSchema
|
||||||
] ""
|
] ""
|
||||||
removeFile promptFile
|
putStrLn $ "Claude stdout: " <> take 2000 stdout
|
||||||
removeFile schemaFile
|
putStrLn $ "Claude stderr: " <> take 2000 stderr_
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> pure $ parseClaudeJsonOutput stdout
|
ExitSuccess -> pure $ parseClaudeJsonOutput stdout
|
||||||
ExitFailure code ->
|
ExitFailure code ->
|
||||||
@ -89,54 +81,61 @@ invokeClaudeReview prompt = do
|
|||||||
|
|
||||||
invokeClaudeReply :: Text -> IO (Either String Text)
|
invokeClaudeReply :: Text -> IO (Either String Text)
|
||||||
invokeClaudeReply prompt = do
|
invokeClaudeReply prompt = do
|
||||||
tmpDir <- getTemporaryDirectory
|
|
||||||
let promptFile = tmpDir </> "claude-reply-prompt.txt"
|
|
||||||
TIO.writeFile promptFile prompt
|
|
||||||
(exitCode, stdout, stderr_) <- readProcessWithExitCode "claude"
|
(exitCode, stdout, stderr_) <- readProcessWithExitCode "claude"
|
||||||
[ "-p"
|
[ "-p", T.unpack prompt
|
||||||
, "--output-format", "json"
|
, "--output-format", "json"
|
||||||
, "--max-turns", "1"
|
, "--max-turns", "3"
|
||||||
, "--prompt-file", promptFile
|
, "--tools", ""
|
||||||
] ""
|
] ""
|
||||||
removeFile promptFile
|
|
||||||
case exitCode of
|
case exitCode of
|
||||||
ExitSuccess -> pure $ parseClaudeTextOutput stdout
|
ExitSuccess -> pure $ parseClaudeTextOutput stdout
|
||||||
ExitFailure code ->
|
ExitFailure code ->
|
||||||
pure $ Left $ "claude exited with code " <> show code <> ": " <> stderr_
|
pure $ Left $ "claude exited with code " <> show code <> ": " <> stderr_
|
||||||
|
|
||||||
-- Parse Claude's JSON envelope: { "result": <structured output> }
|
-- Parse Claude's JSON envelope: { "structured_output": <structured output> }
|
||||||
parseClaudeJsonOutput :: String -> Either String ReviewOutput
|
parseClaudeJsonOutput :: String -> Either String ReviewOutput
|
||||||
parseClaudeJsonOutput raw =
|
parseClaudeJsonOutput raw =
|
||||||
case eitherDecode (strToLBS raw) of
|
case eitherDecode (strToLBS raw) of
|
||||||
Right val ->
|
Right val ->
|
||||||
case parseMaybe extractResult val of
|
case parseMaybe extractField val of
|
||||||
Just (String resultText) ->
|
Just (String resultText) ->
|
||||||
-- result is a JSON string that needs to be parsed again
|
-- structured_output is a JSON string that needs to be parsed again
|
||||||
eitherDecode (BL.fromStrict $ TE.encodeUtf8 resultText)
|
eitherDecode (BL.fromStrict $ TE.encodeUtf8 resultText)
|
||||||
Just resultVal ->
|
Just resultVal ->
|
||||||
-- result is already a JSON object
|
-- structured_output is already a JSON object
|
||||||
case fromJSON resultVal of
|
case fromJSON resultVal of
|
||||||
Success v -> Right v
|
Success v -> Right v
|
||||||
Error e -> Left $ "Failed to parse result object: " <> e
|
Error e -> Left $ "Failed to parse structured_output: " <> e
|
||||||
Nothing -> Left "Missing 'result' field in Claude output"
|
Nothing ->
|
||||||
|
-- Check if Claude reported an error (e.g. max turns reached)
|
||||||
|
case parseMaybe extractSubtype val of
|
||||||
|
Just subtype -> Left $ "Claude failed with: " <> T.unpack subtype
|
||||||
|
Nothing -> Left "Missing 'structured_output' field in Claude output"
|
||||||
Left err -> Left $ "Failed to parse Claude JSON: " <> err
|
Left err -> Left $ "Failed to parse Claude JSON: " <> err
|
||||||
where
|
where
|
||||||
extractResult :: Value -> Parser Value
|
extractField :: Value -> Parser Value
|
||||||
extractResult = withObject "envelope" (.: "result")
|
extractField = withObject "envelope" (.: "structured_output")
|
||||||
|
extractSubtype :: Value -> Parser Text
|
||||||
|
extractSubtype = withObject "envelope" (.: "subtype")
|
||||||
|
|
||||||
-- Parse Claude's JSON envelope for freeform text: { "result": "..." }
|
-- Parse Claude's JSON envelope for freeform text: { "result": "..." }
|
||||||
parseClaudeTextOutput :: String -> Either String Text
|
parseClaudeTextOutput :: String -> Either String Text
|
||||||
parseClaudeTextOutput raw =
|
parseClaudeTextOutput raw =
|
||||||
case eitherDecode (strToLBS raw) of
|
case eitherDecode (strToLBS raw) of
|
||||||
Right val ->
|
Right val ->
|
||||||
case parseMaybe extractResult val of
|
case parseMaybe extractField val of
|
||||||
Just (String resultText) -> Right resultText
|
Just (String resultText) -> Right resultText
|
||||||
Just _ -> Left "Expected string 'result' in Claude output"
|
Just _ -> Left "Expected string 'result' in Claude output"
|
||||||
Nothing -> Left "Missing 'result' field in Claude output"
|
Nothing ->
|
||||||
|
case parseMaybe extractSubtype val of
|
||||||
|
Just subtype -> Left $ "Claude failed with: " <> T.unpack subtype
|
||||||
|
Nothing -> Left "Missing 'result' field in Claude output"
|
||||||
Left err -> Left $ "Failed to parse Claude JSON: " <> err
|
Left err -> Left $ "Failed to parse Claude JSON: " <> err
|
||||||
where
|
where
|
||||||
extractResult :: Value -> Parser Value
|
extractField :: Value -> Parser Value
|
||||||
extractResult = withObject "envelope" (.: "result")
|
extractField = withObject "envelope" (.: "result")
|
||||||
|
extractSubtype :: Value -> Parser Text
|
||||||
|
extractSubtype = withObject "envelope" (.: "subtype")
|
||||||
|
|
||||||
strToLBS :: String -> BL.ByteString
|
strToLBS :: String -> BL.ByteString
|
||||||
strToLBS = BL.fromStrict . TE.encodeUtf8 . T.pack
|
strToLBS = BL.fromStrict . TE.encodeUtf8 . T.pack
|
||||||
|
|||||||
@ -28,7 +28,7 @@ loadConfig = do
|
|||||||
repository <- requireEnv "GITHUB_REPOSITORY"
|
repository <- requireEnv "GITHUB_REPOSITORY"
|
||||||
eventName <- requireEnv "GITHUB_EVENT_NAME"
|
eventName <- requireEnv "GITHUB_EVENT_NAME"
|
||||||
eventPath <- requireEnv "GITHUB_EVENT_PATH"
|
eventPath <- requireEnv "GITHUB_EVENT_PATH"
|
||||||
runId <- lookupEnvText "GITHUB_RUN_ID" ""
|
runId <- lookupEnvText "GITHUB_RUN_NUMBER" ""
|
||||||
trigger <- lookupEnvText "INPUT_TRIGGER_PHRASE" "@claude"
|
trigger <- lookupEnvText "INPUT_TRIGGER_PHRASE" "@claude"
|
||||||
|
|
||||||
let repoText = T.pack repository
|
let repoText = T.pack repository
|
||||||
|
|||||||
@ -34,6 +34,7 @@ data IssueCommentEvent = IssueCommentEvent
|
|||||||
{ iceAction :: Text
|
{ iceAction :: Text
|
||||||
, iceIsPull :: Bool
|
, iceIsPull :: Bool
|
||||||
, iceNumber :: Int
|
, iceNumber :: Int
|
||||||
|
, iceCommentId :: Int
|
||||||
, iceCommentBody :: Text
|
, iceCommentBody :: Text
|
||||||
, iceSender :: Text
|
, iceSender :: Text
|
||||||
}
|
}
|
||||||
@ -42,6 +43,7 @@ data PRReviewEvent = PRReviewEvent
|
|||||||
{ prreAction :: Text
|
{ prreAction :: Text
|
||||||
, prreNumber :: Int
|
, prreNumber :: Int
|
||||||
, prreTitle :: Text
|
, prreTitle :: Text
|
||||||
|
, prreReviewId :: Int
|
||||||
, prreReviewBody :: Text
|
, prreReviewBody :: Text
|
||||||
, prreSender :: Text
|
, prreSender :: Text
|
||||||
}
|
}
|
||||||
@ -50,6 +52,7 @@ data PRReviewCommentEvent = PRReviewCommentEvent
|
|||||||
{ prrceAction :: Text
|
{ prrceAction :: Text
|
||||||
, prrceNumber :: Int
|
, prrceNumber :: Int
|
||||||
, prrceTitle :: Text
|
, prrceTitle :: Text
|
||||||
|
, prrceCommentId :: Int
|
||||||
, prrceCommentBody :: Text
|
, prrceCommentBody :: Text
|
||||||
, prrceSender :: Text
|
, prrceSender :: Text
|
||||||
}
|
}
|
||||||
@ -86,13 +89,15 @@ instance FromJSON IssueCommentEvent where
|
|||||||
pure $ case pr of
|
pure $ case pr of
|
||||||
Just (Object _) -> True
|
Just (Object _) -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
comment <- o .: "comment"
|
comment <- o .: "comment"
|
||||||
body <- comment .: "body"
|
commentId <- comment .: "id"
|
||||||
sender <- parseSender o
|
body <- comment .: "body"
|
||||||
|
sender <- parseSender o
|
||||||
pure IssueCommentEvent
|
pure IssueCommentEvent
|
||||||
{ iceAction = action
|
{ iceAction = action
|
||||||
, iceIsPull = isPull
|
, iceIsPull = isPull
|
||||||
, iceNumber = number
|
, iceNumber = number
|
||||||
|
, iceCommentId = commentId
|
||||||
, iceCommentBody = body
|
, iceCommentBody = body
|
||||||
, iceSender = sender
|
, iceSender = sender
|
||||||
}
|
}
|
||||||
@ -103,13 +108,15 @@ instance FromJSON PRReviewEvent where
|
|||||||
pr <- o .: "pull_request"
|
pr <- o .: "pull_request"
|
||||||
number <- pr .: "number"
|
number <- pr .: "number"
|
||||||
title <- pr .: "title"
|
title <- pr .: "title"
|
||||||
review <- o .: "review"
|
review <- o .: "review"
|
||||||
body <- review .:? "body" .!= ""
|
reviewId <- review .: "id"
|
||||||
sender <- parseSender o
|
body <- review .:? "body" .!= ""
|
||||||
|
sender <- parseSender o
|
||||||
pure PRReviewEvent
|
pure PRReviewEvent
|
||||||
{ prreAction = action
|
{ prreAction = action
|
||||||
, prreNumber = number
|
, prreNumber = number
|
||||||
, prreTitle = title
|
, prreTitle = title
|
||||||
|
, prreReviewId = reviewId
|
||||||
, prreReviewBody = body
|
, prreReviewBody = body
|
||||||
, prreSender = sender
|
, prreSender = sender
|
||||||
}
|
}
|
||||||
@ -120,13 +127,15 @@ instance FromJSON PRReviewCommentEvent where
|
|||||||
pr <- o .: "pull_request"
|
pr <- o .: "pull_request"
|
||||||
number <- pr .: "number"
|
number <- pr .: "number"
|
||||||
title <- pr .: "title"
|
title <- pr .: "title"
|
||||||
comment <- o .: "comment"
|
comment <- o .: "comment"
|
||||||
body <- comment .: "body"
|
commentId <- comment .: "id"
|
||||||
sender <- parseSender o
|
body <- comment .: "body"
|
||||||
|
sender <- parseSender o
|
||||||
pure PRReviewCommentEvent
|
pure PRReviewCommentEvent
|
||||||
{ prrceAction = action
|
{ prrceAction = action
|
||||||
, prrceNumber = number
|
, prrceNumber = number
|
||||||
, prrceTitle = title
|
, prrceTitle = title
|
||||||
|
, prrceCommentId = commentId
|
||||||
, prrceCommentBody = body
|
, prrceCommentBody = body
|
||||||
, prrceSender = sender
|
, prrceSender = sender
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,12 @@ module Bot.Gitea
|
|||||||
, getPrReviews
|
, getPrReviews
|
||||||
, createComment
|
, createComment
|
||||||
, updateComment
|
, updateComment
|
||||||
|
, deleteComment
|
||||||
, postReview
|
, postReview
|
||||||
|
, addCommentReaction
|
||||||
|
, removeCommentReaction
|
||||||
|
, addIssueReaction
|
||||||
|
, removeIssueReaction
|
||||||
, PrInfo (..)
|
, PrInfo (..)
|
||||||
, GiteaComment (..)
|
, GiteaComment (..)
|
||||||
, GiteaReview (..)
|
, GiteaReview (..)
|
||||||
@ -137,6 +142,44 @@ apiPostRaw gc path body = do
|
|||||||
resp <- httpLbs req (gcManager gc)
|
resp <- httpLbs req (gcManager gc)
|
||||||
pure (statusCode $ responseStatus resp, responseBody resp)
|
pure (statusCode $ responseStatus resp, responseBody resp)
|
||||||
|
|
||||||
|
apiDeleteWithBody :: GiteaClient -> Text -> Value -> IO ()
|
||||||
|
apiDeleteWithBody gc path body = do
|
||||||
|
let url = T.unpack (gcBaseUrl gc <> path)
|
||||||
|
initReq <- parseRequest url
|
||||||
|
let req = initReq
|
||||||
|
{ method = "DELETE"
|
||||||
|
, requestHeaders =
|
||||||
|
[ ("Authorization", "token " <> TE.encodeUtf8 (gcToken gc))
|
||||||
|
, ("Accept", "application/json")
|
||||||
|
, ("Content-Type", "application/json")
|
||||||
|
]
|
||||||
|
, requestBody = RequestBodyLBS (encode body)
|
||||||
|
}
|
||||||
|
resp <- httpLbs req (gcManager gc)
|
||||||
|
let sc = statusCode (responseStatus resp)
|
||||||
|
if sc >= 200 && sc < 300
|
||||||
|
then pure ()
|
||||||
|
else fail $ "API DELETE " <> url <> " failed with status " <> show sc
|
||||||
|
<> ": " <> BS.unpack (BL.toStrict $ responseBody resp)
|
||||||
|
|
||||||
|
apiDelete :: GiteaClient -> Text -> IO ()
|
||||||
|
apiDelete gc path = do
|
||||||
|
let url = T.unpack (gcBaseUrl gc <> path)
|
||||||
|
initReq <- parseRequest url
|
||||||
|
let req = initReq
|
||||||
|
{ method = "DELETE"
|
||||||
|
, requestHeaders =
|
||||||
|
[ ("Authorization", "token " <> TE.encodeUtf8 (gcToken gc))
|
||||||
|
, ("Accept", "application/json")
|
||||||
|
]
|
||||||
|
}
|
||||||
|
resp <- httpLbs req (gcManager gc)
|
||||||
|
let sc = statusCode (responseStatus resp)
|
||||||
|
if sc >= 200 && sc < 300
|
||||||
|
then pure ()
|
||||||
|
else fail $ "API DELETE " <> url <> " failed with status " <> show sc
|
||||||
|
<> ": " <> BS.unpack (BL.toStrict $ responseBody resp)
|
||||||
|
|
||||||
-- API operations
|
-- API operations
|
||||||
|
|
||||||
getCurrentUser :: GiteaClient -> IO Text
|
getCurrentUser :: GiteaClient -> IO Text
|
||||||
@ -260,3 +303,29 @@ postReview gc prNum body comments = do
|
|||||||
then pure ()
|
then pure ()
|
||||||
else fail $ "postReview failed with status " <> show sc
|
else fail $ "postReview failed with status " <> show sc
|
||||||
<> ": " <> BS.unpack (BL.toStrict respBody)
|
<> ": " <> BS.unpack (BL.toStrict respBody)
|
||||||
|
|
||||||
|
deleteComment :: GiteaClient -> Int -> IO ()
|
||||||
|
deleteComment gc commentId =
|
||||||
|
apiDelete gc (repoPath gc <> "/issues/comments/" <> T.pack (show commentId))
|
||||||
|
|
||||||
|
addCommentReaction :: GiteaClient -> Int -> Text -> IO ()
|
||||||
|
addCommentReaction gc commentId reaction = do
|
||||||
|
let payload = object ["content" .= reaction]
|
||||||
|
_ <- apiPostRaw gc (repoPath gc <> "/issues/comments/" <> T.pack (show commentId) <> "/reactions") payload
|
||||||
|
pure ()
|
||||||
|
|
||||||
|
removeCommentReaction :: GiteaClient -> Int -> Text -> IO ()
|
||||||
|
removeCommentReaction gc commentId reaction = do
|
||||||
|
let payload = object ["content" .= reaction]
|
||||||
|
apiDeleteWithBody gc (repoPath gc <> "/issues/comments/" <> T.pack (show commentId) <> "/reactions") payload
|
||||||
|
|
||||||
|
addIssueReaction :: GiteaClient -> Int -> Text -> IO ()
|
||||||
|
addIssueReaction gc issueNum reaction = do
|
||||||
|
let payload = object ["content" .= reaction]
|
||||||
|
_ <- apiPostRaw gc (repoPath gc <> "/issues/" <> T.pack (show issueNum) <> "/reactions") payload
|
||||||
|
pure ()
|
||||||
|
|
||||||
|
removeIssueReaction :: GiteaClient -> Int -> Text -> IO ()
|
||||||
|
removeIssueReaction gc issueNum reaction = do
|
||||||
|
let payload = object ["content" .= reaction]
|
||||||
|
apiDeleteWithBody gc (repoPath gc <> "/issues/" <> T.pack (show issueNum) <> "/reactions") payload
|
||||||
|
|||||||
@ -2,7 +2,7 @@ module Bot.Review
|
|||||||
( handleEvent
|
( handleEvent
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import Control.Exception (SomeException, catch)
|
import Control.Exception (SomeException, catch, finally)
|
||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import System.IO (hPutStrLn, stderr)
|
import System.IO (hPutStrLn, stderr)
|
||||||
@ -37,14 +37,29 @@ handleEvent cfg event = do
|
|||||||
|
|
||||||
dispatch :: Config -> GiteaClient -> Event -> Int -> IO ()
|
dispatch :: Config -> GiteaClient -> Event -> Int -> IO ()
|
||||||
dispatch cfg client event prNum = do
|
dispatch cfg client event prNum = do
|
||||||
-- Create tracking comment
|
-- Add eyes reaction to acknowledge
|
||||||
commentId <- createComment client prNum "\x23F3 Thinking..."
|
addReaction event
|
||||||
putStrLn $ "Created tracking comment: " <> show commentId
|
putStrLn "Added eyes reaction"
|
||||||
|
|
||||||
-- Ensure tracking comment is updated even on error
|
|
||||||
let jobUrl = cfgGiteaUrl cfg <> "/" <> cfgRepoOwner cfg <> "/" <> cfgRepoName cfg
|
let jobUrl = cfgGiteaUrl cfg <> "/" <> cfgRepoOwner cfg <> "/" <> cfgRepoName cfg
|
||||||
<> "/actions/runs/" <> cfgRunId cfg
|
<> "/actions/runs/" <> cfgRunId cfg
|
||||||
|
|
||||||
|
let run = do
|
||||||
|
ctx <- gatherContext client prNum
|
||||||
|
|
||||||
|
case event of
|
||||||
|
EvPullRequest _ ->
|
||||||
|
handleReview cfg client event prNum jobUrl ctx
|
||||||
|
|
||||||
|
EvIssueComment ice ->
|
||||||
|
handleReply cfg client (iceCommentBody ice) prNum jobUrl ctx
|
||||||
|
|
||||||
|
EvPRReview prre ->
|
||||||
|
handleReply cfg client (prreReviewBody prre) prNum jobUrl ctx
|
||||||
|
|
||||||
|
EvPRReviewComment prrce ->
|
||||||
|
handleReply cfg client (prrceCommentBody prrce) prNum jobUrl ctx
|
||||||
|
|
||||||
let onError :: SomeException -> IO ()
|
let onError :: SomeException -> IO ()
|
||||||
onError ex = do
|
onError ex = do
|
||||||
let errMsg = T.pack (show ex)
|
let errMsg = T.pack (show ex)
|
||||||
@ -54,27 +69,27 @@ dispatch cfg client event prNum = do
|
|||||||
, ""
|
, ""
|
||||||
, T.take 2000 errMsg
|
, T.take 2000 errMsg
|
||||||
]
|
]
|
||||||
updateComment client commentId body
|
(do _ <- createComment client prNum body; pure ())
|
||||||
`catch` \(_ :: SomeException) -> pure ()
|
`catch` \(_ :: SomeException) -> pure ()
|
||||||
|
|
||||||
let run = do
|
let removeReaction_ =
|
||||||
-- Gather PR context
|
removeReaction event
|
||||||
ctx <- gatherContext client prNum
|
`catch` \(_ :: SomeException) -> pure ()
|
||||||
|
|
||||||
case event of
|
(run `catch` onError) `finally` removeReaction_
|
||||||
EvPullRequest _ -> do
|
|
||||||
handleReview cfg client event prNum commentId jobUrl ctx
|
|
||||||
|
|
||||||
EvIssueComment _ -> do
|
where
|
||||||
handleReview cfg client event prNum commentId jobUrl ctx
|
reaction = "eyes"
|
||||||
|
addReaction = \case
|
||||||
EvPRReview prre -> do
|
EvPullRequest _ -> addIssueReaction client prNum reaction
|
||||||
handleReply cfg client (prreReviewBody prre) prNum commentId jobUrl ctx
|
EvIssueComment ice -> addCommentReaction client (iceCommentId ice) reaction
|
||||||
|
EvPRReview prre -> addCommentReaction client (prreReviewId prre) reaction
|
||||||
EvPRReviewComment prrce -> do
|
EvPRReviewComment prrce -> addCommentReaction client (prrceCommentId prrce) reaction
|
||||||
handleReply cfg client (prrceCommentBody prrce) prNum commentId jobUrl ctx
|
removeReaction = \case
|
||||||
|
EvPullRequest _ -> removeIssueReaction client prNum reaction
|
||||||
run `catch` onError
|
EvIssueComment ice -> removeCommentReaction client (iceCommentId ice) reaction
|
||||||
|
EvPRReview prre -> removeCommentReaction client (prreReviewId prre) reaction
|
||||||
|
EvPRReviewComment prrce -> removeCommentReaction client (prrceCommentId prrce) reaction
|
||||||
|
|
||||||
gatherContext :: GiteaClient -> Int -> IO PRContext
|
gatherContext :: GiteaClient -> Int -> IO PRContext
|
||||||
gatherContext client prNum = do
|
gatherContext client prNum = do
|
||||||
@ -89,8 +104,8 @@ gatherContext client prNum = do
|
|||||||
, ctxReviews = reviews
|
, ctxReviews = reviews
|
||||||
}
|
}
|
||||||
|
|
||||||
handleReview :: Config -> GiteaClient -> Event -> Int -> Int -> Text -> PRContext -> IO ()
|
handleReview :: Config -> GiteaClient -> Event -> Int -> Text -> PRContext -> IO ()
|
||||||
handleReview cfg client event prNum commentId jobUrl ctx = do
|
handleReview cfg client event prNum jobUrl ctx = do
|
||||||
let prompt = buildReviewPrompt cfg event ctx
|
let prompt = buildReviewPrompt cfg event ctx
|
||||||
result <- invokeClaudeReview prompt
|
result <- invokeClaudeReview prompt
|
||||||
case result of
|
case result of
|
||||||
@ -101,28 +116,23 @@ handleReview cfg client event prNum commentId jobUrl ctx = do
|
|||||||
, ""
|
, ""
|
||||||
, T.pack err
|
, T.pack err
|
||||||
]
|
]
|
||||||
updateComment client commentId body
|
_ <- createComment client prNum body
|
||||||
|
pure ()
|
||||||
|
|
||||||
Right review -> do
|
Right review -> do
|
||||||
-- Update tracking comment with summary
|
|
||||||
let sender = eventSender event
|
let sender = eventSender event
|
||||||
body = T.unlines
|
summary = T.unlines
|
||||||
[ "**Claude** reviewed @" <> sender <> "'s PR \x2014 [View job run](" <> jobUrl <> ")"
|
[ "**Claude** reviewed @" <> sender <> "'s PR \x2014 [View job run](" <> jobUrl <> ")"
|
||||||
, ""
|
, ""
|
||||||
, "---"
|
, "---"
|
||||||
, ""
|
, ""
|
||||||
, roSummary review
|
, roSummary review
|
||||||
]
|
]
|
||||||
updateComment client commentId body
|
inlineComments = map toInlineComment (roComments review)
|
||||||
|
postReviewWithFallback client prNum summary inlineComments
|
||||||
|
|
||||||
-- Post review with inline comments if any
|
handleReply :: Config -> GiteaClient -> Text -> Int -> Text -> PRContext -> IO ()
|
||||||
let inlineComments = map toInlineComment (roComments review)
|
handleReply cfg client triggerBody prNum jobUrl ctx = do
|
||||||
if null inlineComments
|
|
||||||
then pure ()
|
|
||||||
else postReviewWithFallback client prNum (roSummary review) inlineComments
|
|
||||||
|
|
||||||
handleReply :: Config -> GiteaClient -> Text -> Int -> Int -> Text -> PRContext -> IO ()
|
|
||||||
handleReply cfg client triggerBody _prNum commentId jobUrl ctx = do
|
|
||||||
let prompt = buildReplyPrompt cfg triggerBody ctx
|
let prompt = buildReplyPrompt cfg triggerBody ctx
|
||||||
result <- invokeClaudeReply prompt
|
result <- invokeClaudeReply prompt
|
||||||
case result of
|
case result of
|
||||||
@ -133,7 +143,8 @@ handleReply cfg client triggerBody _prNum commentId jobUrl ctx = do
|
|||||||
, ""
|
, ""
|
||||||
, T.pack err
|
, T.pack err
|
||||||
]
|
]
|
||||||
updateComment client commentId body
|
_ <- createComment client prNum body
|
||||||
|
pure ()
|
||||||
|
|
||||||
Right replyText -> do
|
Right replyText -> do
|
||||||
let body = T.unlines
|
let body = T.unlines
|
||||||
@ -143,7 +154,8 @@ handleReply cfg client triggerBody _prNum commentId jobUrl ctx = do
|
|||||||
, ""
|
, ""
|
||||||
, replyText
|
, replyText
|
||||||
]
|
]
|
||||||
updateComment client commentId body
|
_ <- createComment client prNum body
|
||||||
|
pure ()
|
||||||
|
|
||||||
toInlineComment :: ClaudeComment -> InlineComment
|
toInlineComment :: ClaudeComment -> InlineComment
|
||||||
toInlineComment cc = InlineComment
|
toInlineComment cc = InlineComment
|
||||||
|
|||||||
@ -52,7 +52,7 @@ matchesTrigger phrase text =
|
|||||||
T.unpack text =~ pattern
|
T.unpack text =~ pattern
|
||||||
where
|
where
|
||||||
pattern :: String
|
pattern :: String
|
||||||
pattern = "(^|\\s)" <> escapeRegex (T.unpack phrase) <> "([\\s.,!?;:]|$)"
|
pattern = "(^|[[:space:]])" <> escapeRegex (T.unpack phrase) <> "([[:space:].,!?;:]|$)"
|
||||||
|
|
||||||
escapeRegex :: String -> String
|
escapeRegex :: String -> String
|
||||||
escapeRegex = concatMap esc
|
escapeRegex = concatMap esc
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user