fplus_lib/external_services/
github.rs

1#![allow(dead_code)]
2use http::header::USER_AGENT;
3use http::{Request, Uri};
4use hyper_rustls::HttpsConnectorBuilder;
5
6use octocrab::auth::AppAuth;
7use octocrab::models::issues::{Comment, Issue};
8use octocrab::models::pulls::PullRequest;
9use octocrab::models::repos::{Branch, ContentItems, FileDeletion, FileUpdate};
10use octocrab::models::{InstallationId, IssueState, Label};
11use octocrab::params::{pulls::State as PullState, State};
12use octocrab::service::middleware::base_uri::BaseUriLayer;
13use octocrab::service::middleware::extra_headers::ExtraHeadersLayer;
14use octocrab::{AuthState, Error as OctocrabError, Octocrab, OctocrabBuilder, Page};
15use serde::{Deserialize, Serialize};
16use std::sync::Arc;
17
18use crate::config::get_env_var_or_default;
19
20const GITHUB_API_URL: &str = "https://api.github.com";
21
22#[derive(Serialize, Deserialize, Debug, Clone)]
23struct RefObject {
24    sha: String,
25}
26
27#[derive(Serialize, Deserialize, Debug, Clone)]
28struct RefData {
29    #[serde(rename = "ref")]
30    _ref: String,
31    object: RefObject,
32}
33#[derive(Serialize, Deserialize, Debug, Clone)]
34struct RefList(pub Vec<RefData>);
35
36struct GithubParams {
37    pub owner: String,
38    pub repo: String,
39    pub app_id: u64,
40    pub installation_id: u64,
41}
42
43#[derive(Debug)]
44pub struct CreateRefillMergeRequestData {
45    pub application_id: String,
46    pub owner_name: String,
47    pub ref_request: Request<String>,
48    pub file_content: String,
49    pub file_name: String,
50    pub branch_name: String,
51    pub commit: String,
52    pub file_sha: String,
53}
54
55#[derive(Debug)]
56pub struct CreateMergeRequestData {
57    pub application_id: String,
58    pub owner_name: String,
59    pub ref_request: Request<String>,
60    pub file_content: String,
61    pub file_name: String,
62    pub branch_name: String,
63    pub commit: String,
64}
65
66#[derive(Debug)]
67pub struct GithubWrapper {
68    pub inner: Arc<Octocrab>,
69    pub owner: String,
70    pub repo: String,
71}
72
73#[derive(Debug, Deserialize, Serialize)]
74struct CommitData {
75    commit: Commit,
76}
77
78#[derive(Debug, Deserialize, Serialize)]
79struct Commit {
80    author: Author,
81}
82
83#[derive(Debug, Deserialize, Serialize)]
84struct Author {
85    name: String,
86}
87
88impl GithubWrapper {
89    pub fn new() -> Self {
90        let owner = get_env_var_or_default("GITHUB_OWNER", "filecoin-project");
91        let repo = get_env_var_or_default("GITHUB_REPO", "filplus-tooling-backend-test");
92        let app_id = get_env_var_or_default("GITHUB_APP_ID", "373258")
93            .parse::<u64>()
94            .unwrap_or_else(|_| {
95                log::error!("Failed to parse GITHUB_APP_ID, using default");
96                373258
97            });
98        let installation_id = get_env_var_or_default("GITHUB_INSTALLATION_ID", "40514592")
99            .parse::<u64>()
100            .unwrap_or_else(|_| {
101                log::error!("Failed to parse GITHUB_INSTALLATION_ID, using default");
102                40514592
103            });
104        let gh_private_key = std::env::var("GH_PRIVATE_KEY").unwrap_or_else(|_| {
105            log::warn!(
106                "GH_PRIVATE_KEY not found in .env file, attempting to read from gh-private-key.pem"
107            );
108            std::fs::read_to_string("gh-private-key.pem").unwrap_or_else(|e| {
109                log::error!("Failed to read gh-private-key.pem. Error: {:?}", e);
110                std::process::exit(1);
111            })
112        });
113
114        let connector = HttpsConnectorBuilder::new()
115            .with_native_roots() // enabled the `rustls-native-certs` feature in hyper-rustls
116            .https_only()
117            .enable_http1()
118            .build();
119
120        let client = hyper::Client::builder()
121            .pool_idle_timeout(std::time::Duration::from_secs(15))
122            .build(connector);
123        let key = jsonwebtoken::EncodingKey::from_rsa_pem(gh_private_key.as_bytes()).unwrap();
124        let octocrab = OctocrabBuilder::new_empty()
125            .with_service(client)
126            .with_layer(&BaseUriLayer::new(Uri::from_static(GITHUB_API_URL)))
127            .with_layer(&ExtraHeadersLayer::new(Arc::new(vec![(
128                USER_AGENT,
129                "octocrab".parse().unwrap(),
130            )])))
131            .with_auth(AuthState::App(AppAuth {
132                app_id: app_id.into(),
133                key,
134            }))
135            .build()
136            .expect("Could not create Octocrab instance");
137        let iod: InstallationId = installation_id.try_into().expect("Invalid installation id");
138        let installation = octocrab.installation(iod);
139        Self {
140            owner,
141            repo,
142            inner: Arc::new(installation),
143        }
144    }
145
146    pub async fn list_issues(&self) -> Result<Vec<Issue>, OctocrabError> {
147        let iid = self
148            .inner
149            .issues(&self.owner, &self.repo)
150            .list()
151            .state(State::Open)
152            .send()
153            .await?;
154        Ok(iid.into_iter().map(|i: Issue| i).collect())
155    }
156
157    pub async fn list_issue(&self, number: u64) -> Result<Issue, OctocrabError> {
158        let iid = self
159            .inner
160            .issues(&self.owner, &self.repo)
161            .get(number)
162            .await?;
163        Ok(iid)
164    }
165
166    pub async fn add_comment_to_issue(
167        &self,
168        number: u64,
169        body: &str,
170    ) -> Result<Comment, OctocrabError> {
171        let iid = self
172            .inner
173            .issues(&self.owner, &self.repo)
174            .create_comment(number, body)
175            .await?;
176        Ok(iid)
177    }
178
179    pub async fn replace_issue_labels(
180        &self,
181        number: u64,
182        labels: &[String],
183    ) -> Result<Vec<Label>, OctocrabError> {
184        let iid = self
185            .inner
186            .issues(&self.owner, &self.repo)
187            .replace_all_labels(number, labels)
188            .await?;
189        Ok(iid)
190    }
191
192    pub async fn list_pull_requests(&self) -> Result<Vec<PullRequest>, OctocrabError> {
193        let iid = self
194            .inner
195            .pulls(&self.owner, &self.repo)
196            .list()
197            .state(State::Open)
198            .send()
199            .await?;
200        Ok(iid.into_iter().collect())
201    }
202
203    pub async fn create_commit_in_branch(
204        &self,
205        branch_name: String,
206        commit_body: String,
207    ) -> Result<octocrab::models::commits::Comment, OctocrabError> {
208        let iid = self
209            .inner
210            .commits(&self.owner, &self.repo)
211            .create_comment(branch_name, commit_body)
212            .send()
213            .await?;
214        Ok(iid)
215    }
216
217    pub async fn get_pull_request_files(
218        &self,
219        pr_number: u64,
220    ) -> Result<(u64, Vec<octocrab::models::pulls::FileDiff>), OctocrabError> {
221        let iid: Page<octocrab::models::pulls::FileDiff> = self
222            .inner
223            .pulls(&self.owner, &self.repo)
224            .media_type(octocrab::params::pulls::MediaType::Full)
225            .list_files(pr_number)
226            .await?;
227        Ok((pr_number, iid.items.into_iter().map(|i| i.into()).collect()))
228    }
229
230    pub async fn list_branches(&self) -> Result<Vec<Branch>, OctocrabError> {
231        let iid = self
232            .inner
233            .repos(&self.owner, &self.repo)
234            .list_branches()
235            .send()
236            .await?;
237        Ok(iid.items)
238    }
239
240    /// creates new branch under head on github
241    /// you should use build_create_ref_request function to construct request
242    pub async fn create_branch(&self, request: Request<String>) -> Result<bool, OctocrabError> {
243        match self.inner.execute(request).await {
244            Ok(_) => {}
245            Err(e) => {
246                println!("Error creating branch: {:?}", e);
247                return Ok(false);
248            }
249        };
250        Ok(true)
251    }
252
253    /// remove branch from github
254    /// you should use build_remove_ref_request function to construct request
255    pub async fn remove_branch(&self, request: Request<String>) -> Result<bool, OctocrabError> {
256        match self.inner.execute(request).await {
257            Ok(_) => {}
258            Err(e) => {
259                println!("Error creating branch: {:?}", e);
260                return Ok(false);
261            }
262        };
263        Ok(true)
264    }
265
266    pub async fn list_pull_request(&self, number: u64) -> Result<PullRequest, OctocrabError> {
267        let iid = self
268            .inner
269            .pulls(&self.owner, &self.repo)
270            .get(number)
271            .await?;
272        Ok(iid)
273    }
274
275    pub async fn create_pull_request(
276        &self,
277        title: &str,
278        head: &str,
279        body: impl Into<String>,
280    ) -> Result<PullRequest, OctocrabError> {
281        let iid = self
282            .inner
283            .pulls(&self.owner, &self.repo)
284            .create(title, head, "main")
285            .body(body)
286            .maintainer_can_modify(true)
287            .send()
288            .await?;
289        Ok(iid)
290    }
291
292    pub async fn update_pull_request(
293        &self,
294        body: &str,
295        number: u64,
296    ) -> Result<PullRequest, OctocrabError> {
297        let iid = self
298            .inner
299            .pulls(&self.owner, &self.repo)
300            .update(number)
301            .body(body)
302            .send()
303            .await?;
304        Ok(iid)
305    }
306
307    pub async fn delete_file(
308        &self,
309        path: &str,
310        branch: &str,
311        message: &str,
312        sha: &str,
313    ) -> Result<FileDeletion, OctocrabError> {
314        let iid = self
315            .inner
316            .repos(&self.owner, &self.repo)
317            .delete_file(path, message, sha)
318            .branch(branch)
319            .send()
320            .await?;
321        Ok(iid)
322    }
323
324    pub async fn add_file(
325        &self,
326        path: &str,
327        content: &str,
328        message: &str,
329        branch: &str,
330    ) -> Result<FileUpdate, OctocrabError> {
331        let iid = self
332            .inner
333            .repos(&self.owner, &self.repo)
334            .create_file(path, message, content)
335            .branch(branch)
336            .send()
337            .await?;
338        Ok(iid)
339    }
340
341    pub async fn get_pull_request_by_number(
342        &self,
343        number: u64,
344    ) -> Result<octocrab::models::pulls::PullRequest, OctocrabError> {
345        let iid = self
346            .inner
347            .pulls(&self.owner, &self.repo)
348            .get(number)
349            .await?;
350        Ok(iid)
351    }
352
353    pub async fn get_file(
354        &self,
355        path: &str,
356        branch: &str,
357    ) -> Result<ContentItems, octocrab::Error> {
358        let iid = self
359            .inner
360            .repos(&self.owner, &self.repo)
361            .get_content()
362            .r#ref(branch)
363            .path(path)
364            .send()
365            .await;
366        iid
367    }
368
369    pub async fn update_file_content(
370        &self,
371        path: &str,
372        message: &str,
373        content: &str,
374        branch: &str,
375        file_sha: &str,
376    ) -> Result<FileUpdate, octocrab::Error> {
377        let iid = self
378            .inner
379            .repos(&self.owner, &self.repo)
380            .update_file(path, message, content, file_sha)
381            .branch(branch)
382            .send()
383            .await?;
384        Ok(iid)
385    }
386
387    pub fn build_remove_ref_request(&self, name: String) -> Result<Request<String>, http::Error> {
388        let request = Request::builder()
389            .method("DELETE")
390            .uri(format!(
391                "https://api.github.com/repos/{}/{}/git/refs/heads/{}",
392                self.owner, self.repo, name
393            ))
394            .body("".to_string())?;
395        Ok(request)
396    }
397
398    pub async fn get_main_branch_sha(&self) -> Result<String, http::Error> {
399        let url = format!(
400            "https://api.github.com/repos/{}/{}/git/refs",
401            self.owner, self.repo
402        );
403        let request = http::request::Builder::new()
404            .method(http::Method::GET)
405            .uri(url);
406        let request = self.inner.build_request::<String>(request, None).unwrap();
407
408        let mut response = match self.inner.execute(request).await {
409            Ok(r) => r,
410            Err(e) => {
411                println!("Error getting main branch sha: {:?}", e);
412                return Ok("".to_string());
413            }
414        };
415        let response = response.body_mut();
416        let body = hyper::body::to_bytes(response).await.unwrap();
417        let shas = body.into_iter().map(|b| b as char).collect::<String>();
418        let shas: RefList = serde_json::from_str(&shas).unwrap();
419        for sha in shas.0 {
420            if sha._ref == "refs/heads/main" {
421                return Ok(sha.object.sha);
422            }
423        }
424        Ok("".to_string())
425    }
426
427    pub fn build_create_ref_request(
428        &self,
429        name: String,
430        head_hash: String,
431    ) -> Result<Request<String>, http::Error> {
432        let request = Request::builder()
433            .method("POST")
434            .uri(format!(
435                "https://api.github.com/repos/{}/{}/git/refs",
436                self.owner, self.repo
437            ))
438            .body(format!(
439                r#"{{"ref": "refs/heads/{}","sha": "{}" }}"#,
440                name, head_hash
441            ))?;
442        Ok(request)
443    }
444
445    pub async fn create_issue(&self, title: &str, body: &str) -> Result<Issue, OctocrabError> {
446        Ok(self
447            .inner
448            .issues(&self.owner, &self.repo)
449            .create(title)
450            .body(body)
451            .send()
452            .await?)
453    }
454
455    pub async fn close_issue(&self, issue_number: u64) -> Result<Issue, OctocrabError> {
456        Ok(self
457            .inner
458            .issues(&self.owner, &self.repo)
459            .update(issue_number)
460            .state(IssueState::Closed)
461            .send()
462            .await?)
463    }
464
465    pub async fn get_pull_request_by_head(
466        &self,
467        head: &str,
468    ) -> Result<Vec<PullRequest>, OctocrabError> {
469        let mut pull_requests: Page<octocrab::models::pulls::PullRequest> = self
470            .inner
471            .pulls(&self.owner, &self.repo)
472            .list()
473            .state(State::Open)
474            .head(head)
475            .per_page(1)
476            .send()
477            .await?;
478        let pull_requests_vec: Vec<PullRequest> = pull_requests.take_items();
479        Ok(pull_requests_vec)
480    }
481
482    pub async fn close_pull_request(&self, number: u64) -> Result<PullRequest, OctocrabError> {
483        Ok(self
484            .inner
485            .pulls(&self.owner, &self.repo)
486            .update(number)
487            .state(PullState::Closed)
488            .send()
489            .await?)
490    }
491
492    pub async fn create_refill_merge_request(
493        &self,
494        data: CreateRefillMergeRequestData,
495    ) -> Result<(PullRequest, String), OctocrabError> {
496        let CreateRefillMergeRequestData {
497            application_id: _,
498            ref_request,
499            owner_name,
500            file_content,
501            file_name,
502            branch_name,
503            commit,
504            file_sha,
505        } = data;
506        let _create_branch_res = self.create_branch(ref_request).await?;
507        self.update_file_content(&file_name, &commit, &file_content, &branch_name, &file_sha)
508            .await?;
509        let pr = self
510            .create_pull_request(
511                &format!("Datacap for {}", owner_name),
512                &branch_name,
513                &format!("BODY"),
514            )
515            .await?;
516
517        Ok((pr, file_sha))
518    }
519
520    pub async fn create_merge_request(
521        &self,
522        data: CreateMergeRequestData,
523    ) -> Result<(PullRequest, String), OctocrabError> {
524        let CreateMergeRequestData {
525            application_id: _,
526            ref_request,
527            owner_name,
528            file_content,
529            file_name,
530            branch_name,
531            commit,
532        } = data;
533        let _create_branch_res = self.create_branch(ref_request).await?;
534        let add_file_res = self
535            .add_file(&file_name, &file_content, &commit, &branch_name)
536            .await?;
537        let file_sha = add_file_res.content.sha;
538        let pr = self
539            .create_pull_request(
540                &format!("Datacap for {}", owner_name),
541                &branch_name,
542                &format!("BODY"),
543            )
544            .await?;
545
546        Ok((pr, file_sha))
547    }
548
549    pub async fn merge_pull_request(&self, number: u64) -> Result<(), OctocrabError> {
550        let _merge_res = self
551            .inner
552            .pulls(&self.owner, &self.repo)
553            .merge(number)
554            .send()
555            .await?;
556        Ok(())
557    }
558
559    // If provided with empty string, will take all files from root
560    pub async fn get_files(&self, path: &str) -> Result<ContentItems, OctocrabError> {
561        let contents_items = self
562            .inner
563            .repos(&self.owner, &self.repo)
564            .get_content()
565            .path(path)
566            .r#ref("main")
567            .send()
568            .await?;
569
570        Ok(contents_items)
571    }
572
573    pub async fn get_all_files_from_branch(
574        &self,
575        branch: &str,
576    ) -> Result<ContentItems, OctocrabError> {
577        let contents_items = self
578            .inner
579            .repos(&self.owner, &self.repo)
580            .get_content()
581            .r#ref(branch)
582            .send()
583            .await?;
584
585        Ok(contents_items)
586    }
587
588    pub async fn get_last_commit_author(&self, pr_number: u64) -> Result<String, http::Error> {
589        let url = format!(
590            "https://api.github.com/repos/{}/{}/pulls/{}/commits",
591            self.owner, self.repo, pr_number
592        );
593    
594        let request = http::request::Builder::new()
595            .method(http::Method::GET)
596            .uri(url);
597    
598        let request = self.inner.build_request::<String>(request, None).unwrap();
599    
600        let mut response = match self.inner.execute(request).await {
601            Ok(r) => r,
602            Err(e) => {
603                println!("Error fetching last commit author: {:?}", e);
604                return Ok("".to_string());
605            }
606        };
607    
608        let response_body = response.body_mut();
609        let body = hyper::body::to_bytes(response_body).await.unwrap();
610        let body_str = String::from_utf8(body.to_vec()).unwrap();
611        let commits: Vec<CommitData> = serde_json::from_str(&body_str).unwrap();
612
613    
614        let last_commit: &CommitData = commits.last().unwrap();
615        let author = last_commit.commit.author.name.clone();
616    
617        Ok(author)
618    }
619
620    pub async fn get_branch_name_from_pr(&self, pr_number: u64) -> Result<String, OctocrabError> {
621        let pull_request = self
622            .inner
623            .pulls(&self.owner, &self.repo)
624            .get(pr_number)
625            .await?;
626        Ok(pull_request.head.ref_field.clone())
627    }
628
629
630}