1
#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2
use clap::{Args, Parser, Subcommand};
3
use tracing::Level;
4

            
5
mod api;
6
mod client;
7
mod config;
8
mod error;
9
mod server;
10
#[cfg(test)]
11
mod test;
12
#[cfg(any(test, feature = "e2e"))]
13
pub mod testutils;
14
mod types;
15
mod version;
16

            
17
/// Guard PRs from merging until all triggered checks have passed.
18
#[derive(Debug, Parser)]
19
#[clap(disable_version_flag = true)]
20
pub struct App {
21
    /// Global cli options
22
    #[clap(flatten)]
23
    pub global_opts: GlobalOpts,
24

            
25
    /// The subcommand to run
26
    #[clap(subcommand)]
27
    pub command: Command,
28
}
29

            
30
impl App {
31
    /// Run the application based on the provided command and options.
32
3
    pub async fn run(self) -> Result<(), error::Error> {
33
3
        if let Command::Version = self.command {
34
            version::print_version_and_exit();
35
3
        }
36

            
37
3
        let config = config::Configuration::load(&self.global_opts.config)?;
38

            
39
3
        let log_level = match self.global_opts.log {
40
            Some(level) => level,
41
3
            None => config.log_level,
42
        };
43
3
        set_log_level(&log_level);
44

            
45
3
        let client = client::Client::build(config.github)?;
46

            
47
3
        match self.command {
48
            Command::Server => {
49
3
                let server = server::Server::new(config.server);
50
3
                server.run(client).await?;
51
            }
52
            Command::Create { cli_opts } => {
53
                return client
54
                    .create_check_run(
55
                        cli_opts.app_installation_id,
56
                        &cli_opts.repo,
57
                        &cli_opts.commit,
58
                    )
59
                    .await;
60
            }
61
            Command::Refresh { cli_opts } => {
62
                let (uncompleted, own_run) = get_and_print_status(&cli_opts, &client).await?;
63
                if uncompleted == 0 {
64
                    println!("All check runs are completed, setting check-run to 'completed'");
65
                }
66
                if own_run.is_none() {
67
                    println!("No cerberus check-run found, creating a new one");
68
                }
69
                client
70
                    .update_check_run(
71
                        cli_opts.app_installation_id,
72
                        &cli_opts.repo,
73
                        &cli_opts.commit,
74
                        uncompleted,
75
                        own_run,
76
                    )
77
                    .await?;
78
                println!("Updated PR status");
79
            }
80
            Command::Status { cli_opts } => {
81
                get_and_print_status(&cli_opts, &client).await?;
82
            }
83
            Command::Version => {
84
                version::print_version_and_exit();
85
            }
86
        }
87
        Ok(())
88
    }
89
}
90

            
91
/// The available subcommands.
92
#[derive(Debug, Subcommand)]
93
pub enum Command {
94
    /// Run the bot and listen for webhook events on /webhook
95
    Server,
96
    /// Create a new pending status check for a commit
97
    Create {
98
        #[clap(flatten)]
99
        cli_opts: CLIOptions,
100
    },
101
    /// Refresh the state of the status check of a commit
102
    Refresh {
103
        #[clap(flatten)]
104
        cli_opts: CLIOptions,
105
    },
106
    /// Check the status of a commit
107
    Status {
108
        #[clap(flatten)]
109
        cli_opts: CLIOptions,
110
    },
111
    /// Print the version and exit
112
    Version,
113
}
114

            
115
// TODO: Consider testing the env option of clap
116
/// Gobal cli options used by all commands (except `version`).
117
#[derive(Debug, Args)]
118
pub struct GlobalOpts {
119
    /// Log level to use, overrides the level given in the config file
120
    #[clap(long, global = true)]
121
    pub log: Option<String>,
122

            
123
    /// Path to the config file
124
    #[clap(long, short, global = true, default_value = "/config/config.yaml")]
125
    pub config: String,
126
}
127

            
128
/// Addtional cli options used by the local client commands like `create`, `refresh`, and `status`.
129
#[derive(Debug, Args)]
130
pub struct CLIOptions {
131
    /// Github App installation ID
132
    #[clap(index = 1)]
133
    pub app_installation_id: u64,
134
    /// Repository in the format "owner/repo"
135
    #[clap(index = 2)]
136
    pub repo: String,
137
    /// Commit SHA to check
138
    #[clap(index = 3)]
139
    pub commit: String,
140
}
141

            
142
3
fn set_log_level(level: &str) {
143
3
    let level = match level.to_lowercase().as_str() {
144
3
        "error" => Level::ERROR,
145
3
        "warn" => Level::WARN,
146
3
        "info" => Level::INFO,
147
3
        "debug" => Level::DEBUG,
148
        _ => {
149
            eprintln!("Invalid log level: {level}. Defaulting to 'info'.");
150
            Level::INFO
151
        }
152
    };
153
3
    let logger = tracing_subscriber::fmt()
154
3
        .with_max_level(level)
155
3
        .with_ansi(false);
156
    #[cfg(not(test))]
157
    logger.init();
158

            
159
    // We can only init the logger once, but testing might call the parent function multiple times.
160
    #[cfg(test)]
161
3
    logger.try_init().unwrap_or_default();
162
3
}
163

            
164
async fn get_and_print_status(
165
    cli_opts: &CLIOptions,
166
    client: &client::Client,
167
) -> Result<(u32, Option<types::CheckRun>), error::Error> {
168
    let (count, own_run) = client
169
        .get_check_run_status(
170
            cli_opts.app_installation_id,
171
            &cli_opts.repo,
172
            &cli_opts.commit,
173
        )
174
        .await?;
175
    println!("Waiting on '{count}' check runs to complete");
176
    if let Some(own_run) = own_run.clone() {
177
        println!(
178
            "Found {} check-run, status: '{}', conclusion: '{}'",
179
            types::CHECK_RUN_NAME,
180
            own_run.status,
181
            own_run.conclusion.unwrap_or("null".to_string())
182
        );
183
    } else {
184
        println!(
185
            "No {} check-run found for this commit",
186
            types::CHECK_RUN_NAME
187
        );
188
    };
189
    Ok((count, own_run))
190
}