diff --git a/migrations/2024-12-26-044555_auto_try/down.sql b/migrations/2024-12-26-044555_auto_try/down.sql new file mode 100644 index 0000000..1035b2a --- /dev/null +++ b/migrations/2024-12-26-044555_auto_try/down.sql @@ -0,0 +1 @@ +ALTER TABLE github_pr DROP COLUMN auto_try; diff --git a/migrations/2024-12-26-044555_auto_try/up.sql b/migrations/2024-12-26-044555_auto_try/up.sql new file mode 100644 index 0000000..b146d9c --- /dev/null +++ b/migrations/2024-12-26-044555_auto_try/up.sql @@ -0,0 +1 @@ +ALTER TABLE github_pr ADD COLUMN auto_try BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/migrations/schema.patch b/migrations/schema.patch index e0c042c..ef95df5 100644 --- a/migrations/schema.patch +++ b/migrations/schema.patch @@ -1,5 +1,5 @@ ---- migrations/schema.unpatched.rs 2024-12-25 03:01:31.500330121 +0000 -+++ server/src/database/schema.rs 2024-12-25 03:01:20.600359108 +0000 +--- migrations/schema.unpatched.rs 2024-12-26 04:47:27.914819445 +0000 ++++ server/src/database/schema.rs 2024-12-26 04:47:19.166873340 +0000 @@ -1,38 +1,44 @@ +#![cfg_attr(coverage_nightly, coverage(off))] // @generated automatically by Diesel CLI. @@ -99,13 +99,13 @@ - added_labels -> Array>, + /// (Manually changed from `Array>` to `Array`) + added_labels -> Array, - } - } - - diesel::table! { - /// Representation of the `health_check` table. - /// -@@ -326,12 +332,13 @@ + /// The `auto_try` column of the `github_pr` table. + /// + /// Its SQL type is `Bool`. + /// + /// (Automatically generated by Diesel.) + auto_try -> Bool, +@@ -332,12 +338,13 @@ updated_at -> Timestamptz, } } diff --git a/migrations/schema.unpatched.rs b/migrations/schema.unpatched.rs index ae597d5..3b07c4c 100644 --- a/migrations/schema.unpatched.rs +++ b/migrations/schema.unpatched.rs @@ -304,6 +304,12 @@ diesel::table! { /// /// (Automatically generated by Diesel.) added_labels -> Array>, + /// The `auto_try` column of the `github_pr` table. + /// + /// Its SQL type is `Bool`. + /// + /// (Automatically generated by Diesel.) + auto_try -> Bool, } } diff --git a/server/src/command/auto_try.rs b/server/src/command/auto_try.rs new file mode 100644 index 0000000..6df2047 --- /dev/null +++ b/server/src/command/auto_try.rs @@ -0,0 +1,81 @@ +use anyhow::Context; +use diesel::OptionalExtension; +use diesel_async::{AsyncPgConnection, RunQueryDsl}; + +use super::BrawlCommandContext; +use crate::database::ci_run::CiRun; +use crate::database::pr::Pr; +use crate::github::messages; +use crate::github::models::PullRequest; +use crate::github::repo::GitHubRepoClient; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct AutoTryCommand { + pub force: bool, + pub disable: bool, +} + +pub async fn handle( + conn: &mut AsyncPgConnection, + context: BrawlCommandContext<'_, R>, + command: AutoTryCommand, +) -> anyhow::Result<()> { + let pr = context.repo.get_pull_request(context.pr_number).await?; + handle_with_pr(conn, pr, context, command).await +} + +async fn handle_with_pr( + conn: &mut AsyncPgConnection, + pr: PullRequest, + context: BrawlCommandContext<'_, R>, + command: AutoTryCommand, +) -> anyhow::Result<()> { + if !context.repo.config().enabled { + return Ok(()); + } + + if !context.repo.can_try(context.user.id).await? { + tracing::debug!("user does not have permission to do this"); + return Ok(()); + } + + let active_run = CiRun::active(context.repo.id(), context.pr_number) + .get_result(conn) + .await + .optional() + .context("fetch active run")?; + + if active_run.is_some_and(|r| !r.is_dry_run) { + context.repo.send_message(context.pr_number, &messages::error_no_body("Cannot enable auto-try while a merge is in progress, use `?brawl cancel` to cancel it first & then try again.")).await?; + return Ok(()); + } + + let db_pr = Pr::find(context.repo.id(), context.pr_number).get_result(conn).await?; + + match (command.disable, db_pr.auto_try) { + (true, true) => { + db_pr.update().auto_try(false).build().query().execute(conn).await?; + + context + .repo + .send_message(context.pr_number, &messages::auto_try_disabled()) + .await?; + } + (false, false) => { + if !command.force && pr.head.repo.is_none_or(|r| r.id != context.repo.id()) { + context.repo.send_message(context.pr_number, &messages::error_no_body("This PR is not from this repository, so auto-try cannot be enabled. To bypass this check, use force `?brawl auto-try force`")).await?; + return Ok(()); + } + + db_pr.update().auto_try(true).build().query().execute(conn).await?; + + context + .repo + .send_message(context.pr_number, &messages::auto_try_enabled()) + .await?; + } + (_, _) => {} + } + + Ok(()) +} diff --git a/server/src/command/merge.rs b/server/src/command/merge.rs index 9a547ee..4662a95 100644 --- a/server/src/command/merge.rs +++ b/server/src/command/merge.rs @@ -191,11 +191,15 @@ mod tests { sha: "head_sha".to_string(), label: Some("head".to_string()), ref_field: "head".to_string(), + repo: None, + user: None, }, base: PrBranch { sha: "base_sha".to_string(), label: Some("base".to_string()), ref_field: "base".to_string(), + repo: None, + user: None, }, requested_reviewers: vec![ User { diff --git a/server/src/command/mod.rs b/server/src/command/mod.rs index 8c55261..c9898cd 100644 --- a/server/src/command/mod.rs +++ b/server/src/command/mod.rs @@ -1,5 +1,6 @@ use std::str::FromStr; +use auto_try::AutoTryCommand; use diesel_async::AsyncPgConnection; use dry_run::DryRunCommand; use merge::MergeCommand; @@ -7,6 +8,7 @@ use merge::MergeCommand; use crate::github::models::User; use crate::github::repo::GitHubRepoClient; +mod auto_try; mod cancel; mod dry_run; mod merge; @@ -20,6 +22,7 @@ pub enum BrawlCommand { Retry, Cancel, Ping, + AutoTry(AutoTryCommand), } pub struct BrawlCommandContext<'a, R> { @@ -40,6 +43,7 @@ impl BrawlCommand { BrawlCommand::Retry => retry::handle(conn, context).await, BrawlCommand::Cancel => cancel::handle(conn, context).await, BrawlCommand::Ping => ping::handle(conn, context).await, + BrawlCommand::AutoTry(command) => auto_try::handle(conn, context, command).await, } } } @@ -110,6 +114,18 @@ impl FromStr for BrawlCommand { } "retry" => Ok(BrawlCommand::Retry), "ping" => Ok(BrawlCommand::Ping), + "auto-try" => { + let mut force = false; + let mut disable = false; + + match splits.next() { + Some("force") => force = true, + Some("disable") => disable = true, + _ => (), + } + + Ok(BrawlCommand::AutoTry(AutoTryCommand { force, disable })) + } command => { tracing::debug!("invalid command: {}", command); Err(BrawlCommandError::InvalidCommand(command.into())) @@ -205,6 +221,55 @@ mod tests { ), ("", Err(BrawlCommandError::NoCommand)), ("@brawl @brawl merge", Err(BrawlCommandError::InvalidCommand("@brawl".into()))), + ( + "@brawl auto-try", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: false, + disable: false, + })), + ), + ( + "@brawl auto-try disable", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: false, + disable: true, + })), + ), + ( + "@brawl auto-try force", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: true, + disable: false, + })), + ), + ( + "@brawl auto-try something else", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: false, + disable: false, + })), + ), + ( + "@brawl auto-try force something else", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: true, + disable: false, + })), + ), + ( + "@brawl auto-try disable something else", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: false, + disable: true, + })), + ), + ( + "@brawl auto-try disable force", + Ok(BrawlCommand::AutoTry(AutoTryCommand { + force: false, + disable: true, + })), + ), ]; for (input, expected) in cases { diff --git a/server/src/database/pr.rs b/server/src/database/pr.rs index c8351ce..8206793 100644 --- a/server/src/database/pr.rs +++ b/server/src/database/pr.rs @@ -33,6 +33,7 @@ pub struct Pr<'a> { pub added_labels: Vec>, pub created_at: chrono::DateTime, pub updated_at: chrono::DateTime, + pub auto_try: bool, } #[derive(AsChangeset, Identifiable, Clone, bon::Builder)] @@ -53,6 +54,7 @@ pub struct UpdatePr<'a> { pub merge_commit_sha: Option>>, pub target_branch: Option>, pub latest_commit_sha: Option>, + pub auto_try: Option, #[builder(default = chrono::Utc::now())] pub updated_at: chrono::DateTime, } @@ -67,6 +69,34 @@ impl UpdatePr<'_> { } } +impl<'a> UpdatePr<'a> { + pub fn update_pr(self, pr: &mut Pr<'a>) { + if self.needs_update() { + pr.updated_at = self.updated_at; + } + + macro_rules! update_if_some { + ($field:expr, $value:expr) => { + if let Some(value) = $value { + $field = value; + } + }; + } + + update_if_some!(pr.added_labels, self.added_labels); + update_if_some!(pr.title, self.title); + update_if_some!(pr.body, self.body); + update_if_some!(pr.merge_status, self.merge_status); + update_if_some!(pr.assigned_ids, self.assigned_ids); + update_if_some!(pr.status, self.status); + update_if_some!(pr.default_priority, self.default_priority); + update_if_some!(pr.merge_commit_sha, self.merge_commit_sha); + update_if_some!(pr.target_branch, self.target_branch); + update_if_some!(pr.latest_commit_sha, self.latest_commit_sha); + update_if_some!(pr.auto_try, self.auto_try); + } +} + fn pr_status(pr: &PullRequest) -> GithubPrStatus { match pr.state { Some(IssueState::Open) if matches!(pr.draft, Some(true)) => GithubPrStatus::Draft, @@ -107,14 +137,15 @@ impl<'a> Pr<'a> { author_id: pr.user.as_ref().map(|u| u.id.0 as i64).unwrap_or(user_id.0 as i64), assigned_ids: pr_assigned_ids(pr), status: pr_status(pr), - default_priority: None, merge_commit_sha: pr.merged_at.and_then(|_| pr.merge_commit_sha.as_deref().map(Cow::Borrowed)), target_branch: Cow::Borrowed(&pr.base.ref_field), source_branch: Cow::Borrowed(&pr.head.ref_field), latest_commit_sha: Cow::Borrowed(&pr.head.sha), - added_labels: Vec::new(), created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), + default_priority: None, + added_labels: Vec::new(), + auto_try: false, } } @@ -141,13 +172,14 @@ impl<'a> Pr<'a> { title: Some(Cow::Borrowed(self.title.as_ref())), body: Some(Cow::Borrowed(self.body.as_ref())), merge_status: Some(self.merge_status), - added_labels: Some(self.added_labels.clone()), assigned_ids: Some(self.assigned_ids.clone()), status: Some(self.status), - default_priority: Some(self.default_priority), merge_commit_sha: Some(self.merge_commit_sha.as_deref().map(Cow::Borrowed)), target_branch: Some(Cow::Borrowed(self.target_branch.as_ref())), latest_commit_sha: Some(Cow::Borrowed(self.latest_commit_sha.as_ref())), + default_priority: None, + added_labels: None, + auto_try: None, updated_at: self.updated_at, }) .returning(Pr::as_select()) @@ -162,7 +194,7 @@ impl<'a> Pr<'a> { UpdatePr::builder(self.github_repo_id, self.github_pr_number) } - pub fn update_from(&'a self, new: &'a PullRequest) -> UpdatePr<'a> { + pub fn update_from(&self, new: &'a PullRequest) -> UpdatePr<'a> { let title = Cow::Borrowed(new.title.as_str()); let body = Cow::Borrowed(new.body.as_str()); let merge_status = pr_merge_status(new); @@ -272,6 +304,7 @@ mod tests { merge_status: GithubPrMergeStatus::NotReady, status: GithubPrStatus::Open, merge_commit_sha: None, + auto_try: false, }), expected: @r#" INSERT INTO @@ -376,6 +409,7 @@ mod tests { merge_status: GithubPrMergeStatus::NotReady, status: GithubPrStatus::Open, merge_commit_sha: None, + auto_try: false, }), expected: @r#" INSERT INTO @@ -536,6 +570,7 @@ mod tests { merge_commit_sha: None, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), + auto_try: false, } .insert() .execute(&mut conn) @@ -567,6 +602,7 @@ mod tests { merge_commit_sha: None, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), + auto_try: false, } .insert() .execute(&mut conn) @@ -606,6 +642,7 @@ mod tests { merge_commit_sha: None, created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), + auto_try: false, } .insert() .execute(&mut conn) diff --git a/server/src/database/schema.rs b/server/src/database/schema.rs index d2c6e6f..c4938cf 100644 --- a/server/src/database/schema.rs +++ b/server/src/database/schema.rs @@ -310,6 +310,12 @@ diesel::table! { /// /// (Manually changed from `Array>` to `Array`) added_labels -> Array, + /// The `auto_try` column of the `github_pr` table. + /// + /// Its SQL type is `Bool`. + /// + /// (Automatically generated by Diesel.) + auto_try -> Bool, } } diff --git a/server/src/github/config.rs b/server/src/github/config.rs index ae034d9..6585a72 100644 --- a/server/src/github/config.rs +++ b/server/src/github/config.rs @@ -108,6 +108,9 @@ pub struct GitHubBrawlLabelsConfig { /// The label to attach to PRs when they fail to try #[serde(skip_serializing_if = "Vec::is_empty", deserialize_with = "string_or_vec")] pub on_try_failure: Vec, + /// The label attached to PRs when auto-try is enabled + #[serde(skip_serializing_if = "Vec::is_empty", deserialize_with = "string_or_vec")] + pub auto_try_enabled: Vec, } fn string_or_vec<'de, D: Deserializer<'de>>(s: D) -> Result, D::Error> { diff --git a/server/src/github/label_state.rs b/server/src/github/label_state.rs index eaf82af..d414232 100644 --- a/server/src/github/label_state.rs +++ b/server/src/github/label_state.rs @@ -8,30 +8,44 @@ use super::repo::GitHubRepoClient; use crate::database::enums::GithubCiRunStatus; use crate::database::pr::Pr; -fn desired_labels(status: GithubCiRunStatus, is_dry_run: bool, config: &GitHubBrawlLabelsConfig) -> &[String] { +fn desired_labels<'a>( + status: GithubCiRunStatus, + is_dry_run: bool, + pr: &Pr<'_>, + config: &'a GitHubBrawlLabelsConfig, +) -> Vec> { + let mut labels = Vec::new(); + match (status, is_dry_run) { (GithubCiRunStatus::Queued, false) => { - return &config.on_merge_queued; + labels.extend(config.on_merge_queued.iter().map(|s| Cow::Borrowed(s.as_ref()))); } (GithubCiRunStatus::InProgress, true) => { - return &config.on_try_in_progress; + labels.extend(config.on_try_in_progress.iter().map(|s| Cow::Borrowed(s.as_ref()))); } (GithubCiRunStatus::InProgress, false) => { - return &config.on_merge_in_progress; + labels.extend(config.on_merge_in_progress.iter().map(|s| Cow::Borrowed(s.as_ref()))); } (GithubCiRunStatus::Failure, true) => { - return &config.on_try_failure; + labels.extend(config.on_try_failure.iter().map(|s| Cow::Borrowed(s.as_ref()))); } (GithubCiRunStatus::Failure, false) => { - return &config.on_merge_failure; + labels.extend(config.on_merge_failure.iter().map(|s| Cow::Borrowed(s.as_ref()))); } (GithubCiRunStatus::Success, false) => { - return &config.on_merge_success; + labels.extend(config.on_merge_success.iter().map(|s| Cow::Borrowed(s.as_ref()))); } _ => {} } - &[] + if pr.auto_try { + labels.extend(config.auto_try_enabled.iter().map(|s| Cow::Borrowed(s.as_ref()))); + } + + labels.sort(); + labels.dedup(); + + labels } struct LabelsAdjustments<'a> { @@ -46,13 +60,7 @@ fn get_adjustments<'a>( is_dry_run: bool, config: &'a GitHubBrawlLabelsConfig, ) -> LabelsAdjustments<'a> { - let mut desired_labels = desired_labels(status, is_dry_run, config) - .iter() - .map(|s| Cow::Borrowed(s.as_ref())) - .collect::>>(); - - desired_labels.sort(); - desired_labels.dedup(); + let desired_labels = desired_labels(status, is_dry_run, pr, config); let desired_labels_set = desired_labels.iter().cloned().collect::>>(); @@ -151,6 +159,7 @@ mod tests { on_try_failure: vec!["try".to_string(), "failure".to_string()], on_merge_failure: vec!["merge".to_string(), "failure".to_string()], on_merge_success: vec!["merge".to_string(), "success".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; let cases: &[(GithubCiRunStatus, bool, &[&str])] = &[ @@ -164,9 +173,18 @@ mod tests { (GithubCiRunStatus::Cancelled, true, &[]), ]; + let pr = PullRequest::default(); + let mut pr = Pr::new(&pr, UserId(1), RepositoryId(1)); + for (status, is_dry_run, expected) in cases { - assert_eq!(desired_labels(*status, *is_dry_run, &config), *expected); + assert_eq!(desired_labels(*status, *is_dry_run, &pr, &config), *expected); } + + pr.auto_try = true; + assert_eq!( + desired_labels(GithubCiRunStatus::Queued, false, &pr, &config), + &["auto-try", "queued"] + ); } #[test] @@ -181,6 +199,7 @@ mod tests { on_try_failure: vec!["try".to_string(), "failure".to_string()], on_merge_failure: vec!["merge".to_string(), "failure".to_string()], on_merge_success: vec!["merge".to_string(), "success".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; let LabelsAdjustments { @@ -221,6 +240,16 @@ mod tests { assert_eq!(desired_labels, &["in_progress", "merge"]); assert_eq!(labels_to_add, &["in_progress"]); assert_eq!(labels_to_remove, &["queued"]); + + pr.auto_try = true; + let LabelsAdjustments { + desired_labels, + labels_to_add, + labels_to_remove, + } = get_adjustments(&pr, GithubCiRunStatus::InProgress, false, &config); + assert_eq!(desired_labels, &["in_progress", "merge", "auto-try"]); + assert_eq!(labels_to_add, &["in_progress", "auto-try"]); + assert_eq!(labels_to_remove, &["queued"]); } #[tokio::test] @@ -245,6 +274,7 @@ mod tests { on_try_failure: vec!["try".to_string(), "failure".to_string()], on_merge_failure: vec!["merge".to_string(), "failure".to_string()], on_merge_success: vec!["merge".to_string(), "success".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; let pr_number = pr.github_pr_number as u64; @@ -384,6 +414,7 @@ mod tests { on_try_failure: vec!["try".to_string(), "failure".to_string()], on_merge_failure: vec!["merge".to_string(), "failure".to_string()], on_merge_success: vec!["merge".to_string(), "success".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; let pr_number = pr.github_pr_number as u64; diff --git a/server/src/github/merge_workflow.rs b/server/src/github/merge_workflow.rs index e1ec5f4..81d5fcd 100644 --- a/server/src/github/merge_workflow.rs +++ b/server/src/github/merge_workflow.rs @@ -816,6 +816,7 @@ mod tests { created_at: chrono::Utc::now(), updated_at: chrono::Utc::now(), added_labels: vec![], + auto_try: false, }; diesel::insert_into(crate::database::schema::github_pr::table) @@ -897,6 +898,7 @@ mod tests { on_merge_failure: vec!["merge_failure".to_string()], on_try_in_progress: vec!["try_in_progress".to_string()], on_try_failure: vec!["try_failure".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; UpdateCiRun::builder(run.id) @@ -1337,6 +1339,7 @@ mod tests { on_merge_failure: vec!["merge_failure".to_string()], on_try_in_progress: vec!["try_in_progress".to_string()], on_try_failure: vec!["try_failure".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; UpdateCiRun::builder(run.id) @@ -1551,6 +1554,7 @@ mod tests { on_merge_failure: vec!["merge_failure".to_string()], on_try_in_progress: vec!["try_in_progress".to_string()], on_try_failure: vec!["try_failure".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; let task = tokio::spawn(async move { @@ -2291,6 +2295,7 @@ mod tests { on_merge_failure: vec!["merge_failure".to_string()], on_try_in_progress: vec!["try_in_progress".to_string()], on_try_failure: vec!["try_failure".to_string()], + auto_try_enabled: vec!["auto-try".to_string()], }; let task = tokio::spawn(async move { diff --git a/server/src/github/messages/auto_try_disabled.md b/server/src/github/messages/auto_try_disabled.md new file mode 100644 index 0000000..2957970 --- /dev/null +++ b/server/src/github/messages/auto_try_disabled.md @@ -0,0 +1 @@ +🔴 Auto-try disabled diff --git a/server/src/github/messages/auto_try_enabled.md b/server/src/github/messages/auto_try_enabled.md new file mode 100644 index 0000000..8fd5ca4 --- /dev/null +++ b/server/src/github/messages/auto_try_enabled.md @@ -0,0 +1,2 @@ +🟢 Auto-try enabled +Every push to this PR will automatically trigger a try run. diff --git a/server/src/github/messages/mod.rs b/server/src/github/messages/mod.rs index 8ec84a5..fc02257 100644 --- a/server/src/github/messages/mod.rs +++ b/server/src/github/messages/mod.rs @@ -9,6 +9,8 @@ pub enum IssueMessage { TestsTimeout(String), TestsStart(String), Pong(String), + AutoTryEnabled(String), + AutoTryDisabled(String), } impl AsRef for IssueMessage { @@ -23,6 +25,8 @@ impl AsRef for IssueMessage { IssueMessage::TestsTimeout(s) => s.as_ref(), IssueMessage::TestsStart(s) => s.as_ref(), IssueMessage::Pong(s) => s.as_ref(), + IssueMessage::AutoTryEnabled(s) => s.as_ref(), + IssueMessage::AutoTryDisabled(s) => s.as_ref(), } } } @@ -164,3 +168,11 @@ pub fn tests_start(head_sha_link: impl std::fmt::Display, base_sha_link: impl st pub fn pong(username: impl std::fmt::Display, status: impl std::fmt::Display) -> IssueMessage { IssueMessage::Pong(format!(include_str!("pong.md"), username = username, status = status,)) } + +pub fn auto_try_enabled() -> IssueMessage { + IssueMessage::AutoTryEnabled(include_str!("auto_try_enabled.md").to_string()) +} + +pub fn auto_try_disabled() -> IssueMessage { + IssueMessage::AutoTryDisabled(include_str!("auto_try_disabled.md").to_string()) +} diff --git a/server/src/github/models.rs b/server/src/github/models.rs index a02f436..95b5c41 100644 --- a/server/src/github/models.rs +++ b/server/src/github/models.rs @@ -51,6 +51,8 @@ pub struct PrBranch { pub label: Option, pub ref_field: String, pub sha: String, + pub user: Option, + pub repo: Option, } impl From for PrBranch { @@ -59,6 +61,8 @@ impl From for PrBranch { label: value.label, ref_field: value.ref_field, sha: value.sha, + user: value.user.map(|u| u.into()), + repo: value.repo.map(|r| r.into()), } } } @@ -69,6 +73,8 @@ impl From for PrBranch { label: value.label, ref_field: value.ref_field, sha: value.sha, + user: value.user.map(|u| u.into()), + repo: value.repo.map(|r| r.into()), } } } diff --git a/server/src/webhook/pull_request.rs b/server/src/webhook/pull_request.rs index 1269a86..cccd1a9 100644 --- a/server/src/webhook/pull_request.rs +++ b/server/src/webhook/pull_request.rs @@ -2,7 +2,7 @@ use anyhow::Context; use diesel::OptionalExtension; use diesel_async::{AsyncPgConnection, RunQueryDsl}; -use crate::database::ci_run::CiRun; +use crate::database::ci_run::{Base, CiRun}; use crate::database::enums::GithubCiRunStatus; use crate::database::pr::Pr; use crate::github::merge_workflow::GitHubMergeWorkflow; @@ -26,7 +26,7 @@ pub async fn handle_with_pr( pr: PullRequest, user: User, ) -> anyhow::Result<()> { - if let Some(current) = Pr::find(repo.id(), pr.number) + if let Some(mut current) = Pr::find(repo.id(), pr.number) .get_result(conn) .await .optional() @@ -35,6 +35,9 @@ pub async fn handle_with_pr( let update = current.update_from(&pr); if update.needs_update() { update.query().execute(conn).await?; + let current_head_sha = current.latest_commit_sha.clone(); + let commit_head_changed = current_head_sha != pr.head.sha; + update.update_pr(&mut current); // Fetch the active run (if there is one) let run = CiRun::active(repo.id(), pr.number) @@ -43,7 +46,7 @@ pub async fn handle_with_pr( .optional() .context("fetch ci run")?; - match run { + match &run { Some(run) if !run.is_dry_run => { repo.merge_workflow().cancel(&run, repo, conn, ¤t).await?; repo.send_message( @@ -58,8 +61,32 @@ pub async fn handle_with_pr( ) .await?; } + Some(run) if current.auto_try && commit_head_changed && run.is_dry_run => { + repo.merge_workflow().cancel(&run, repo, conn, ¤t).await?; + } _ => {} } + + if current.auto_try && commit_head_changed && run.is_none_or(|r| r.is_dry_run) { + tracing::info!( + "starting auto-try because head changed from {} to {}", + current_head_sha, + pr.head.sha + ); + let run = CiRun::insert(repo.id(), pr.number) + .base_ref(Base::from_pr(&pr)) + .head_commit_sha(pr.head.sha.as_str().into()) + .ci_branch(repo.config().try_branch(pr.number).into()) + .requested_by_id(pr.head.user.as_ref().map(|u| u.id.0 as i64).unwrap_or(user.id.0 as i64)) + .approved_by_ids(vec![]) + .is_dry_run(true) + .build() + .query() + .get_result(conn) + .await?; + + repo.merge_workflow().start(&run, repo, conn, ¤t).await?; + } } } else { Pr::new(&pr, user.id, repo.id())