diff --git a/src/bors/event.rs b/src/bors/event.rs index 91a73b3a..f98bca1c 100644 --- a/src/bors/event.rs +++ b/src/bors/event.rs @@ -1,5 +1,6 @@ use crate::database::{WorkflowStatus, WorkflowType}; use crate::github::{CommitSha, GithubRepoName, GithubUser, PullRequest, PullRequestNumber}; +use chrono::Duration; use octocrab::models::RunId; #[derive(Debug)] @@ -88,6 +89,7 @@ pub struct WorkflowCompleted { pub commit_sha: CommitSha, pub run_id: RunId, pub status: WorkflowStatus, + pub running_time: Option, } #[derive(Debug)] diff --git a/src/bors/handlers/workflow.rs b/src/bors/handlers/workflow.rs index 00b5871a..7f79a98c 100644 --- a/src/bors/handlers/workflow.rs +++ b/src/bors/handlers/workflow.rs @@ -1,4 +1,5 @@ use std::sync::Arc; +use std::time::Duration; use crate::bors::comment::try_build_succeeded_comment; use crate::bors::event::{CheckSuiteCompleted, WorkflowCompleted, WorkflowStarted}; @@ -61,12 +62,29 @@ pub(super) async fn handle_workflow_started( pub(super) async fn handle_workflow_completed( repo: Arc, db: Arc, - payload: WorkflowCompleted, + mut payload: WorkflowCompleted, ) -> anyhow::Result<()> { if !is_bors_observed_branch(&payload.branch) { return Ok(()); } + if let Some(running_time) = payload.running_time { + let running_time_as_duration = + chrono::Duration::to_std(&running_time).unwrap_or(Duration::from_secs(0)); + if let Some(min_ci_time) = repo.config.load().min_ci_time { + if running_time_as_duration < min_ci_time { + payload.status = WorkflowStatus::Failure; + tracing::warn!( + "Workflow running time is less than the minimum CI duration: {:?} < {:?}", + running_time_as_duration, + min_ci_time + ); + } + } + } else { + tracing::warn!("Running time is not available."); + } + tracing::info!("Updating status of workflow to {:?}", payload.status); db.update_workflow_status(*payload.run_id, payload.status) .await?; diff --git a/src/config.rs b/src/config.rs index 63b95cbe..9d62a9b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,12 +19,23 @@ pub struct RepositoryConfig { pub timeout: Duration, #[serde(default, deserialize_with = "deserialize_labels")] pub labels: HashMap>, + #[serde(default, deserialize_with = "deserialize_duration_from_secs_opt")] + pub min_ci_time: Option, } fn default_timeout() -> Duration { Duration::from_secs(3600) } +fn deserialize_duration_from_secs_opt<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + // Allow null values for the option + let maybe_seconds = Option::::deserialize(deserializer)?; + Ok(maybe_seconds.map(Duration::from_secs)) +} + fn deserialize_duration_from_secs<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, @@ -124,7 +135,7 @@ where #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::{collections::BTreeMap, time::Duration}; use crate::config::{default_timeout, RepositoryConfig}; @@ -142,6 +153,20 @@ mod tests { assert_eq!(config.timeout.as_secs(), 3600); } + #[test] + fn deserialize_min_ci_time_empty() { + let content = ""; + let config = load_config(content); + assert_eq!(config.min_ci_time, None); + } + + #[test] + fn deserialize_min_ci_time() { + let content = "min_ci_time = 3600"; + let config = load_config(content); + assert_eq!(config.min_ci_time, Some(Duration::from_secs(3600))); + } + #[test] fn deserialize_labels() { let content = r#"[labels] diff --git a/src/github/webhook.rs b/src/github/webhook.rs index a2750ab0..cc6bea57 100644 --- a/src/github/webhook.rs +++ b/src/github/webhook.rs @@ -273,18 +273,29 @@ fn parse_workflow_run_events(body: &[u8]) -> anyhow::Result> { url: payload.workflow_run.html_url.into(), }, ))), - "completed" => Some(BorsEvent::Repository( - BorsRepositoryEvent::WorkflowCompleted(WorkflowCompleted { - repository: repository_name, - branch: payload.workflow_run.head_branch, - commit_sha: CommitSha(payload.workflow_run.head_sha), - run_id: RunId(payload.workflow_run.id.0), - status: match payload.workflow_run.conclusion.unwrap_or_default().as_str() { - "success" => WorkflowStatus::Success, - _ => WorkflowStatus::Failure, - }, - }), - )), + "completed" => { + let running_time = if let (Some(started_at), Some(completed_at)) = ( + Some(payload.workflow_run.created_at), + Some(payload.workflow_run.updated_at), + ) { + Some(completed_at - started_at) + } else { + None + }; + Some(BorsEvent::Repository( + BorsRepositoryEvent::WorkflowCompleted(WorkflowCompleted { + repository: repository_name, + branch: payload.workflow_run.head_branch, + commit_sha: CommitSha(payload.workflow_run.head_sha), + run_id: RunId(payload.workflow_run.id.0), + running_time, + status: match payload.workflow_run.conclusion.unwrap_or_default().as_str() { + "success" => WorkflowStatus::Success, + _ => WorkflowStatus::Failure, + }, + }), + )) + } _ => None, }; Ok(result) @@ -774,6 +785,12 @@ mod tests { 4900979072, ), status: Failure, + running_time: Some( + TimeDelta { + secs: 13, + nanos: 0, + }, + ), }, ), ),