squawk_github/
lib.rs

1#![allow(clippy::doc_markdown)]
2#![allow(clippy::missing_errors_doc)]
3#![allow(clippy::single_match_else)]
4pub mod actions;
5pub mod app;
6use std::error::Error;
7
8use log::info;
9use serde::{Deserialize, Serialize};
10
11pub(crate) const DEFAULT_GITHUB_API_URL: &str = "https://api.github.com";
12
13#[derive(Debug)]
14pub enum GithubError {
15    JsonWebTokenCreation(jsonwebtoken::errors::Error),
16    HttpError(reqwest::Error),
17    CommentTooLarge(String),
18}
19
20impl Error for GithubError {
21    fn source(&self) -> Option<&(dyn Error + 'static)> {
22        match self {
23            GithubError::JsonWebTokenCreation(err) => Some(err),
24            GithubError::HttpError(err) => Some(err),
25            GithubError::CommentTooLarge(_) => None,
26        }
27    }
28}
29
30#[derive(Debug, Deserialize, Serialize)]
31pub struct Comment {
32    pub id: i64,
33    pub url: String,
34    pub html_url: String,
35    pub body: String,
36    pub user: User,
37}
38
39#[derive(Debug, Deserialize, Serialize)]
40pub struct User {
41    pub id: i64,
42    pub login: String,
43    pub r#type: String,
44}
45
46pub trait GitHubApi {
47    fn app_slug(&self) -> String;
48    fn create_issue_comment(
49        &self,
50        owner: &str,
51        repo: &str,
52        issue_id: i64,
53        body: &str,
54    ) -> Result<(), GithubError>;
55    fn list_issue_comments(
56        &self,
57        owner: &str,
58        repo: &str,
59        issue_id: i64,
60    ) -> Result<Vec<Comment>, GithubError>;
61    fn update_issue_comment(
62        &self,
63        owner: &str,
64        repo: &str,
65        comment_id: i64,
66        body: &str,
67    ) -> Result<(), GithubError>;
68}
69
70pub fn comment_on_pr(
71    gh: &dyn GitHubApi,
72    owner: &str,
73    repo: &str,
74    issue: i64,
75    body: &str,
76    existing_comment_text_includes: &str,
77) -> Result<(), GithubError> {
78    let comments = gh.list_issue_comments(owner, repo, issue)?;
79
80    let bot_name = gh.app_slug();
81
82    info!("checking for existing comment");
83    match comments.iter().find(|x| {
84        x.user.r#type == "Bot"
85            && x.user.login == bot_name
86            // NOTE: We filter comments by their contents so we don't accidentally
87            // overwrite a comment made by some other tool. This happens often in
88            // GitHub repos that reuse the default GHA bot for all linters.
89            //
90            // This only works if `existing_comment_text_includes` is a "stable"
91            // piece of text included in all comments made by squawk!
92            && x.body.contains(existing_comment_text_includes)
93    }) {
94        Some(prev_comment) => {
95            info!("updating comment");
96            gh.update_issue_comment(owner, repo, prev_comment.id, body)
97        }
98        None => {
99            info!("creating comment");
100            gh.create_issue_comment(owner, repo, issue, body)
101        }
102    }
103}