Skip to main content

cerberus_mergeguard/
lib.rs

1#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
2use clap::{Args, Parser, Subcommand};
3use tracing::Level;
4
5mod api;
6mod client;
7mod config;
8mod error;
9mod server;
10#[cfg(test)]
11mod test;
12#[cfg(any(test, feature = "e2e"))]
13pub mod testutils;
14mod types;
15mod version;
16
17/// Guard PRs from merging until all triggered checks have passed.
18#[derive(Debug, Parser)]
19#[clap(disable_version_flag = true)]
20pub 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
30impl App {
31    /// Run the application based on the provided command and options.
32    pub async fn run(self) -> Result<(), error::Error> {
33        if let Command::Version = self.command {
34            version::print_version_and_exit();
35        }
36
37        let config = config::Configuration::load(&self.global_opts.config)?;
38
39        let log_level = match self.global_opts.log {
40            Some(level) => level,
41            None => config.log_level,
42        };
43        set_log_level(&log_level);
44
45        let client = client::Client::build(config.github)?;
46
47        match self.command {
48            Command::Server => {
49                let server = server::Server::new(config.server);
50                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)]
93pub 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)]
118pub 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)]
130pub 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
142fn set_log_level(level: &str) {
143    let level = match level.to_lowercase().as_str() {
144        "error" => Level::ERROR,
145        "warn" => Level::WARN,
146        "info" => Level::INFO,
147        "debug" => Level::DEBUG,
148        _ => {
149            eprintln!("Invalid log level: {level}. Defaulting to 'info'.");
150            Level::INFO
151        }
152    };
153    let logger = tracing_subscriber::fmt()
154        .with_max_level(level)
155        .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    logger.try_init().unwrap_or_default();
162}
163
164async 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}