-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.rs
348 lines (300 loc) · 12.5 KB
/
main.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
//! Fetch the Latest 20 PRs:
//! If PR Status = Open
//! And PR Comments don't exist:
//! Then Call Gemini API to Validate the PR
//! And Post Gemini Response as PR Comment
use std::{
env,
thread::sleep,
time::Duration
};
use clap::Parser;
use log::info;
use google_generative_ai_rs::v1::{
api::Client,
gemini::{request::Request, Content, Model, Part, Role},
};
use octocrab::{
issues::IssueHandler,
models::{reactions::ReactionContent, IssueState, Label},
params,
pulls::PullRequestHandler
};
/// Requirements for PR Review
const REQUIREMENTS: &str =
r#####"
# Here are the requirements for a NuttX PR
## Summary
* Why change is necessary (fix, update, new feature)?
* What functional part of the code is being changed?
* How does the change exactly work (what will change and how)?
* Related [NuttX Issue](https://github.com/apache/nuttx/issues) reference if applicable.
* Related NuttX Apps [Issue](https://github.com/apache/nuttx-apps/issues) / [Pull Request](https://github.com/apache/nuttx-apps/pulls) reference if applicable.
## Impact
* Is new feature added? Is existing feature changed?
* Impact on user (will user need to adapt to change)? NO / YES (please describe if yes).
* Impact on build (will build process change)? NO / YES (please descibe if yes).
* Impact on hardware (will arch(s) / board(s) / driver(s) change)? NO / YES (please describe if yes).
* Impact on documentation (is update required / provided)? NO / YES (please describe if yes).
* Impact on security (any sort of implications)? NO / YES (please describe if yes).
* Impact on compatibility (backward/forward/interoperability)? NO / YES (please describe if yes).
* Anything else to consider?
## Testing
I confirm that changes are verified on local setup and works as intended:
* Build Host(s): OS (Linux,BSD,macOS,Windows,..), CPU(Intel,AMD,ARM), compiler(GCC,CLANG,version), etc.
* Target(s): arch(sim,RISC-V,ARM,..), board:config, etc.
Testing logs before change:
```
your testing logs here
```
Testing logs after change:
```
your testing logs here
```
"#####;
/// Command-Line Arguments
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
struct Args {
/// Owner of the GitHub Repo that will be processed (`apache`)
#[arg(long)]
owner: String,
/// Name of the GitHub Repo that will be processed (`nuttx` or `nuttx-apps`)
#[arg(long)]
repo: String,
}
/// Validate the Latest PRs and post the PR Reviews as PR Comments
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Init the Logger and Command-Line Args
env_logger::init();
let args = Args::parse();
// Init the GitHub Client
let token = std::env::var("GITHUB_TOKEN")
.expect("GITHUB_TOKEN env variable is required");
let octocrab = octocrab::Octocrab::builder()
.personal_token(token)
.build()?;
// Get the Handlers for GitHub Pull Requests and Issues
let pulls = octocrab.pulls(&args.owner, &args.repo);
let issues = octocrab.issues(&args.owner, &args.repo);
// Fetch the 20 Newest Pull Requests that are Open
let pr_list = pulls
.list()
.state(params::State::Open)
.sort(params::pulls::Sort::Created)
.direction(params::Direction::Descending)
.per_page(20)
.send()
.await?;
// Every 5 Seconds: Process the next PR fetched
for pr in pr_list {
let pr_id = pr.number;
process_pr(&pulls, &issues, pr_id)
.await?;
sleep(Duration::from_secs(5));
}
// Return OK
Ok(())
}
/// Validate the PR by calling Gemini API. Then post the PR Review as a PR Comment
async fn process_pr(pulls: &PullRequestHandler<'_>, issues: &IssueHandler<'_>, pr_id: u64) -> Result<(), Box<dyn std::error::Error>> {
// Fetch the PR
let pr = pulls
.get(pr_id)
.await?;
info!("{:#?}", pr.url);
// Skip if PR State is Not Open
if pr.state.unwrap() != IssueState::Open {
info!("Skipping Closed PR: {}", pr_id);
return Ok(());
}
// Skip if PR contains Comments
if pr.comments.unwrap() > 0 {
info!("Skipping PR with comments: {}", pr_id);
return Ok(());
}
// Skip if PR Size is Unknown
let labels = pr.labels.unwrap();
if labels.is_empty() {
info!("Skipping Unknown PR Size: {}", pr_id);
return Ok(());
}
// Skip if PR Size is XS
let size_xs: Vec<Label> = labels
.into_iter()
.filter(|l| l.name == "Size: XS")
.collect();
if size_xs.len() > 0 {
info!("Skipping PR Size XS: {}", pr_id);
return Ok(());
}
// Fetch the PR Commits
// TODO: Change `pull_number` to `pr_commits`
let commits = pulls
.pull_number(pr_id)
.commits()
.await;
let commits = commits.unwrap().items;
let mut precheck = String::new();
// Check for Multiple Commits
// if commits.len() > 1 {
// precheck.push_str(
// &format!("__Squash The Commits:__ This PR contains {} Commits. Please Squash the Multiple Commits into a Single Commit.\n\n", commits.len())
// );
// }
// Check for Empty Commit Message
let mut empty_message = false;
for commit in commits.iter() {
// Message should be "title\n\nbody"
let message = &commit.commit.message;
if message.find("\n").is_some() {
} else {
info!("Missing Commit Message: {:#?}", message);
empty_message = true;
break;
}
}
if empty_message {
precheck.push_str(
"__Fill In The Commit Message:__ This PR contains a Commit with an Empty Commit Message. Please fill in the Commit Message with the PR Summary.\n\n"
);
}
// Get the PR Body
let body = pr.body.unwrap_or("".to_string());
info!("PR Body: {:#?}", body);
// Retry Gemini API up to 3 times, by checking the PR Reactions.
// Fetch the PR Reactions. Quit if Both Reactions are set.
let reactions = get_reactions(issues, pr_id).await?;
if reactions.0.is_some() && reactions.1.is_some() {
info!("Skipping PR after 3 retries: {}", pr_id);
return Ok(());
}
// Bump up the PR Reactions: 00 > 01 > 10 > 11
bump_reactions(issues, pr_id, reactions).await?;
// Init the Gemini Client
let client = Client::new_from_model(
Model::Gemini1_5Pro, // For Production
// Model::GeminiPro, // For Testing
env::var("GEMINI_API_KEY").unwrap().to_string()
);
// Compose the Prompt for Gemini Request: PR Requirements + PR Body
let input =
REQUIREMENTS.to_string() +
"\n\n# Does this PR meet the NuttX Requirements? Please be concise\n\n" +
&body;
// For Testing:
// let input = "# Here are the requirements for a NuttX PR\n\n## Summary\n\n* Why change is necessary (fix, update, new feature)?\n* What functional part of the code is being changed?\n* How does the change exactly work (what will change and how)?\n* Related [NuttX Issue](https://github.com/apache/nuttx/issues) reference if applicable.\n* Related NuttX Apps [Issue](https://github.com/apache/nuttx-apps/issues) / [Pull Request](https://github.com/apache/nuttx-apps/pulls) reference if applicable.\n\n## Impact\n\n* Is new feature added? Is existing feature changed?\n* Impact on user (will user need to adapt to change)? NO / YES (please describe if yes).\n* Impact on build (will build process change)? NO / YES (please descibe if yes).\n* Impact on hardware (will arch(s) / board(s) / driver(s) change)? NO / YES (please describe if yes).\n* Impact on documentation (is update required / provided)? NO / YES (please describe if yes).\n* Impact on security (any sort of implications)? NO / YES (please describe if yes).\n* Impact on compatibility (backward/forward/interoperability)? NO / YES (please describe if yes).\n* Anything else to consider?\n\n## Testing\n\nI confirm that changes are verified on local setup and works as intended:\n* Build Host(s): OS (Linux,BSD,macOS,Windows,..), CPU(Intel,AMD,ARM), compiler(GCC,CLANG,version), etc.\n* Target(s): arch(sim,RISC-V,ARM,..), board:config, etc.\n\nTesting logs before change:\n\n```\nyour testing logs here\n```\n\nTesting logs after change:\n```\nyour testing logs here\n```\n\n# Does this PR meet the NuttX Requirements?\n\n## Summary\nBCH: Add readonly configuration for BCH devices\n## Impact\nNONE\n## Testing\n";
// Compose the Gemini Request
let txt_request = Request {
contents: vec![Content {
role: Role::User,
parts: vec![Part {
text: Some(input.to_string()),
inline_data: None,
file_data: None,
video_metadata: None,
}],
}],
tools: vec![],
safety_settings: vec![],
generation_config: None,
system_instruction: None,
};
// Send the Gemini Request
let response = client
.post(30, &txt_request)
.await?;
info!("Gemini Response: {:#?}", response);
// Get the Gemini Response
let response_text =
response.rest().unwrap()
.candidates.first().unwrap()
.content.parts.first().unwrap()
.text.clone().unwrap();
info!("Response Text: {:#?}", response_text);
// Header for PR Comment
let header = "[**\\[Experimental Bot, please feedback here\\]**](https://github.com/search?q=repo%3Aapache%2Fnuttx+13552&type=issues)";
// Compose the PR Comment
let comment_text =
header.to_string() + "\n\n" +
&precheck + "\n\n" +
&response_text;
// Post the PR Comment
let comment = issues
.create_comment(pr_id, comment_text)
.await?;
info!("PR Comment: {:#?}", comment);
// If successful, delete the PR Reactions
delete_reactions(issues, pr_id).await?;
info!("{:#?}", pr.url);
// Wait 1 minute
sleep(Duration::from_secs(60));
// Return OK
Ok(())
}
/// Return the Reaction IDs for Rocket and Eyes Reactions, created by the Bot
async fn get_reactions(issues: &IssueHandler<'_>, pr_id: u64) ->
Result<(Option<u64>, Option<u64>), Box<dyn std::error::Error>> {
// Fetch the PR Reactions
let reactions = issues
.list_reactions(pr_id)
.send()
.await?;
let reactions = reactions.items;
// Watch for Rocket and Eyes Reactions created by the Bot
// TODO: Change `nuttxpr` to the GitHub User ID of the Bot
let mut result: (Option<u64>, Option<u64>) = (None, None);
for reaction in reactions.iter() {
let content = &reaction.content;
let user = &reaction.user.login;
let reaction_id = &reaction.id.0;
if user == "nuttxpr" {
match content {
ReactionContent::Rocket => { result.0 = Some(*reaction_id) }
ReactionContent::Eyes => { result.1 = Some(*reaction_id) }
_ => {}
}
}
}
Ok(result)
}
/// Bump up the 2 PR Reactions: 00 > 01 > 10 > 11
/// Position 0 is the Rocket Reaction, Position 1 is the Eye Reaction
async fn bump_reactions(issues: &IssueHandler<'_>, pr_id: u64, reactions: (Option<u64>, Option<u64>)) ->
Result<(), Box<dyn std::error::Error>> {
match reactions {
// (Rocket, Eye)
(None, None) => { create_reaction(issues, pr_id, ReactionContent::Rocket).await?; }
(Some(id), None) => { delete_reaction(issues, pr_id, id).await?; create_reaction(issues, pr_id, ReactionContent::Eyes).await?; }
(None, Some(_)) => { create_reaction(issues, pr_id, ReactionContent::Rocket).await?; }
(Some(_), Some(_)) => { panic!("Reaction Overflow") }
}
Ok(())
}
/// Delete the PR Reactions
async fn delete_reactions(issues: &IssueHandler<'_>, pr_id: u64) ->
Result<(), Box<dyn std::error::Error>> {
let reactions = get_reactions(issues, pr_id).await?;
if let Some(reaction_id) = reactions.0 {
delete_reaction(issues, pr_id, reaction_id).await?;
}
if let Some(reaction_id) = reactions.1 {
delete_reaction(issues, pr_id, reaction_id).await?;
}
Ok(())
}
/// Create the PR Reaction
async fn create_reaction(issues: &IssueHandler<'_>, pr_id: u64, content: ReactionContent) ->
Result<(), Box<dyn std::error::Error>> {
issues.create_reaction(pr_id, content)
.await?;
Ok(())
}
/// Delete the PR Reaction
async fn delete_reaction(issues: &IssueHandler<'_>, pr_id: u64, reaction_id: u64) ->
Result<(), Box<dyn std::error::Error>> {
issues.delete_reaction(pr_id, reaction_id)
.await?;
Ok(())
}