elizaos_plugin_github/actions/
review_pull_request.rs1#![allow(missing_docs)]
2
3use async_trait::async_trait;
4use serde_json::json;
5
6use super::{ActionContext, ActionResult, GitHubAction};
7use crate::error::Result;
8use crate::types::{CreateReviewParams, ReviewEvent, ReviewState};
9use crate::GitHubService;
10
11pub struct ReviewPullRequestAction;
12
13impl ReviewPullRequestAction {
14 fn determine_review_event(text: &str) -> ReviewEvent {
15 let lower = text.to_lowercase();
16
17 if lower.contains("approve") || lower.contains("lgtm") || lower.contains("looks good") {
18 ReviewEvent::Approve
19 } else if lower.contains("request changes")
20 || lower.contains("needs work")
21 || lower.contains("fix")
22 {
23 ReviewEvent::RequestChanges
24 } else {
25 ReviewEvent::Comment
26 }
27 }
28}
29
30#[async_trait]
31impl GitHubAction for ReviewPullRequestAction {
32 fn name(&self) -> &str {
33 "REVIEW_GITHUB_PULL_REQUEST"
34 }
35
36 fn description(&self) -> &str {
37 "Creates a review on a GitHub pull request. Can approve, request changes, or add comments."
38 }
39
40 fn similes(&self) -> Vec<&str> {
41 vec![
42 "APPROVE_PR",
43 "REQUEST_CHANGES",
44 "COMMENT_ON_PR",
45 "REVIEW_PR",
46 "PR_REVIEW",
47 "CODE_REVIEW",
48 ]
49 }
50
51 async fn validate(&self, context: &ActionContext) -> Result<bool> {
52 let text = context
53 .message
54 .get("content")
55 .and_then(|c| c.get("text"))
56 .and_then(|t| t.as_str())
57 .unwrap_or("")
58 .to_lowercase();
59
60 Ok(text.contains("review")
61 || text.contains("approve")
62 || text.contains("request changes")
63 || text.contains("lgtm"))
64 }
65
66 async fn handler(
67 &self,
68 context: &ActionContext,
69 service: &GitHubService,
70 ) -> Result<ActionResult> {
71 let text = context
72 .message
73 .get("content")
74 .and_then(|c| c.get("text"))
75 .and_then(|t| t.as_str())
76 .unwrap_or("");
77
78 let pull_number = context
79 .state
80 .get("pullNumber")
81 .and_then(|p| p.as_u64())
82 .unwrap_or(0);
83
84 if pull_number == 0 {
85 return Ok(ActionResult::error("Pull request number is required"));
86 }
87
88 let body = context
90 .state
91 .get("body")
92 .and_then(|b| b.as_str())
93 .map(|s| s.to_string())
94 .unwrap_or_else(|| text.to_string());
95
96 let event = context
97 .state
98 .get("event")
99 .and_then(|e| e.as_str())
100 .map(|e| match e.to_uppercase().as_str() {
101 "APPROVE" => ReviewEvent::Approve,
102 "REQUEST_CHANGES" => ReviewEvent::RequestChanges,
103 _ => ReviewEvent::Comment,
104 })
105 .unwrap_or_else(|| Self::determine_review_event(text));
106
107 let params = CreateReviewParams {
108 owner: context.owner.clone(),
109 repo: context.repo.clone(),
110 pull_number,
111 body: Some(body),
112 event,
113 comments: Vec::new(),
114 commit_id: None,
115 };
116
117 let review = service.create_review(params).await?;
118
119 let event_label = match review.state {
120 ReviewState::Approved => "approved",
121 ReviewState::ChangesRequested => "requested changes on",
122 ReviewState::Commented | ReviewState::Dismissed | ReviewState::Pending => {
123 "commented on"
124 }
125 };
126
127 Ok(ActionResult::success(
128 format!("Created {} review on PR #{}", event_label, pull_number),
129 json!({
130 "id": review.id,
131 "state": review.state,
132 "html_url": review.html_url,
133 "body": review.body,
134 }),
135 ))
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use super::*;
142 use serde_json::json;
143
144 #[tokio::test]
145 async fn test_validate_with_review_keyword() {
146 let action = ReviewPullRequestAction;
147 let context = ActionContext {
148 message: json!({
149 "content": { "text": "Review pull request #42" }
150 }),
151 owner: "test".to_string(),
152 repo: "test".to_string(),
153 state: json!({}),
154 };
155
156 assert!(action.validate(&context).await.unwrap());
157 }
158
159 #[tokio::test]
160 async fn test_validate_with_approve_keyword() {
161 let action = ReviewPullRequestAction;
162 let context = ActionContext {
163 message: json!({
164 "content": { "text": "Approve the pull request" }
165 }),
166 owner: "test".to_string(),
167 repo: "test".to_string(),
168 state: json!({}),
169 };
170
171 assert!(action.validate(&context).await.unwrap());
172 }
173
174 #[tokio::test]
175 async fn test_validate_with_lgtm_keyword() {
176 let action = ReviewPullRequestAction;
177 let context = ActionContext {
178 message: json!({
179 "content": { "text": "LGTM on this PR" }
180 }),
181 owner: "test".to_string(),
182 repo: "test".to_string(),
183 state: json!({}),
184 };
185
186 assert!(action.validate(&context).await.unwrap());
187 }
188
189 #[tokio::test]
190 async fn test_validate_without_keywords() {
191 let action = ReviewPullRequestAction;
192 let context = ActionContext {
193 message: json!({
194 "content": { "text": "Hello world" }
195 }),
196 owner: "test".to_string(),
197 repo: "test".to_string(),
198 state: json!({}),
199 };
200
201 assert!(!action.validate(&context).await.unwrap());
202 }
203
204 #[test]
205 fn test_determine_review_event_approve() {
206 assert!(matches!(
207 ReviewPullRequestAction::determine_review_event("Approve this PR"),
208 ReviewEvent::Approve
209 ));
210 assert!(matches!(
211 ReviewPullRequestAction::determine_review_event("LGTM"),
212 ReviewEvent::Approve
213 ));
214 assert!(matches!(
215 ReviewPullRequestAction::determine_review_event("Looks good to me"),
216 ReviewEvent::Approve
217 ));
218 }
219
220 #[test]
221 fn test_determine_review_event_request_changes() {
222 assert!(matches!(
223 ReviewPullRequestAction::determine_review_event("Request changes on this"),
224 ReviewEvent::RequestChanges
225 ));
226 assert!(matches!(
227 ReviewPullRequestAction::determine_review_event("Needs work"),
228 ReviewEvent::RequestChanges
229 ));
230 assert!(matches!(
231 ReviewPullRequestAction::determine_review_event("Please fix this issue"),
232 ReviewEvent::RequestChanges
233 ));
234 }
235
236 #[test]
237 fn test_determine_review_event_comment() {
238 assert!(matches!(
239 ReviewPullRequestAction::determine_review_event("Just a comment"),
240 ReviewEvent::Comment
241 ));
242 assert!(matches!(
243 ReviewPullRequestAction::determine_review_event("What about this?"),
244 ReviewEvent::Comment
245 ));
246 }
247
248 #[test]
249 fn test_action_properties() {
250 let action = ReviewPullRequestAction;
251 assert_eq!(action.name(), "REVIEW_GITHUB_PULL_REQUEST");
252 assert!(!action.description().is_empty());
253 assert!(action.similes().contains(&"APPROVE_PR"));
254 assert!(action.similes().contains(&"CODE_REVIEW"));
255 }
256}