From 76c1d7ab14b5651845145e13d8b6cc462c1ea154 Mon Sep 17 00:00:00 2001 From: Sakibul Islam Date: Fri, 11 Jul 2025 15:14:22 +0100 Subject: [PATCH 1/4] Start auto builds in merge queue --- ...1134df3ac7818af0c085ac432eb52874a62a2.json | 15 ++ src/bors/comment.rs | 7 + src/bors/handlers/workflow.rs | 6 +- src/bors/merge_queue.rs | 255 +++++++++++++++++- src/bors/mod.rs | 3 + src/database/client.rs | 20 +- src/database/operations.rs | 18 ++ src/tests/mocks/bors.rs | 36 ++- src/tests/mocks/repository.rs | 1 + 9 files changed, 352 insertions(+), 9 deletions(-) create mode 100644 .sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json diff --git a/.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json b/.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json new file mode 100644 index 00000000..d4166e22 --- /dev/null +++ b/.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "UPDATE pull_request SET auto_build_id = $1 WHERE id = $2", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int4", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2" +} diff --git a/src/bors/comment.rs b/src/bors/comment.rs index ca606cb1..f9d126e6 100644 --- a/src/bors/comment.rs +++ b/src/bors/comment.rs @@ -185,3 +185,10 @@ fn list_workflows_status(workflows: &[WorkflowModel]) -> String { .collect::>() .join("\n") } + +pub fn auto_build_started_comment(head_sha: &CommitSha, merge_sha: &CommitSha) -> Comment { + Comment::new(format!( + ":hourglass: Testing commit {} with merge {}...", + head_sha, merge_sha + )) +} diff --git a/src/bors/handlers/workflow.rs b/src/bors/handlers/workflow.rs index f3679894..a4bf7c0f 100644 --- a/src/bors/handlers/workflow.rs +++ b/src/bors/handlers/workflow.rs @@ -237,9 +237,13 @@ async fn try_complete_build( #[cfg(test)] mod tests { use crate::bors::handlers::trybuild::TRY_BRANCH_NAME; + use crate::bors::merge_queue::{AUTO_BUILD_CHECK_RUN_NAME, AUTO_BRANCH_NAME}; use crate::database::WorkflowStatus; use crate::database::operations::get_all_workflows; - use crate::tests::mocks::{Branch, CheckSuite, Workflow, WorkflowEvent, run_test}; + use crate::tests::mocks::{ + BorsBuilder, Branch, CheckSuite, GitHubState, Workflow, WorkflowEvent, default_pr_number, + run_test, + }; #[sqlx::test] async fn workflow_started_unknown_build(pool: sqlx::PgPool) { diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs index 88ccf341..2ef2e8f4 100644 --- a/src/bors/merge_queue.rs +++ b/src/bors/merge_queue.rs @@ -1,3 +1,5 @@ +use anyhow::anyhow; +use octocrab::params::checks::{CheckRunOutput, CheckRunStatus}; use std::future::Future; use std::sync::Arc; use tokio::sync::mpsc; @@ -5,8 +7,18 @@ use tracing::Instrument; use crate::BorsContext; use crate::bors::RepositoryState; +use crate::bors::comment::auto_build_started_comment; +use crate::database::{BuildStatus, PullRequestModel}; +use crate::github::api::client::GithubRepositoryClient; +use crate::github::api::operations::ForcePush; +use crate::github::{CommitSha, MergeError}; use crate::utils::sort_queue::sort_queue_prs; +enum MergeResult { + Success(CommitSha), + Conflict, +} + #[derive(Debug)] enum MergeQueueEvent { Trigger, @@ -31,10 +43,17 @@ impl MergeQueueSender { } } +/// Branch used for performing merge operations. +/// This branch should not run CI checks. +pub(super) const AUTO_MERGE_BRANCH_NAME: &str = "automation/bors/auto-merge"; + /// Branch where CI checks run for auto builds. /// This branch should run CI checks. pub(super) const AUTO_BRANCH_NAME: &str = "automation/bors/auto"; +// The name of the check run seen in the GitHub UI. +pub(super) const AUTO_BUILD_CHECK_RUN_NAME: &str = "Bors auto build"; + pub async fn merge_queue_tick(ctx: Arc) -> anyhow::Result<()> { let repos: Vec> = ctx.repositories.read().unwrap().values().cloned().collect(); @@ -61,14 +80,173 @@ pub async fn merge_queue_tick(ctx: Arc) -> anyhow::Result<()> { // then pending builds (which block the queue to prevent starting simultaneous auto-builds). let prs = sort_queue_prs(prs); - for _ in prs { - // Process PRs... + for pr in prs { + let pr_num = pr.number; + + if let Some(auto_build) = &pr.auto_build { + match auto_build.status { + // Build successful - point the base branch to the merged commit. + BuildStatus::Success => { + // Break to give GitHub time to update the base branch. + break; + } + // Build in progress - stop queue. We can only have one PR being built + // at a time. + BuildStatus::Pending => { + tracing::info!("PR {pr_num} has a pending build - blocking queue"); + break; + } + BuildStatus::Failure | BuildStatus::Cancelled | BuildStatus::Timeouted => { + unreachable!("Failed auto builds should be filtered out by SQL query"); + } + } + } + + // No build exists for this PR - start a new auto build. + match start_auto_build(&repo, &ctx, pr).await { + Ok(true) => { + tracing::info!("Starting auto build for PR {pr_num}"); + break; + } + Ok(false) => { + tracing::debug!("Failed to start auto build for PR {pr_num}"); + continue; + } + Err(error) => { + // Unexpected error - the PR will remain in the "queue" for a retry. + tracing::error!("Error starting auto build for PR {pr_num}: {:?}", error); + continue; + } + } } } + #[cfg(test)] + crate::bors::WAIT_FOR_MERGE_QUEUE.mark(); + Ok(()) } +/// Starts a new auto build for a pull request. +async fn start_auto_build( + repo: &Arc, + ctx: &Arc, + pr: PullRequestModel, +) -> anyhow::Result { + let client = &repo.client; + + let gh_pr = client.get_pull_request(pr.number).await?; + let base_sha = client.get_branch_sha(&pr.base_branch).await?; + + let auto_merge_commit_message = format!( + "Auto merge of #{} - {}, r={}\n\n{}\n\n{}", + pr.number, + gh_pr.head_label, + pr.approver().unwrap_or(""), + pr.title, + gh_pr.message + ); + + // 1. Merge PR head with base branch on `AUTO_MERGE_BRANCH_NAME` + match attempt_merge( + &repo.client, + &gh_pr.head.sha, + &base_sha, + &auto_merge_commit_message, + ) + .await? + { + MergeResult::Success(merge_sha) => { + // 2. Push merge commit to `AUTO_BRANCH_NAME` where CI runs + client + .set_branch_to_sha(AUTO_BRANCH_NAME, &merge_sha, ForcePush::Yes) + .await?; + + // 3. Record the build in the database + let build_id = ctx + .db + .attach_auto_build( + &pr, + AUTO_BRANCH_NAME.to_string(), + merge_sha.clone(), + base_sha, + ) + .await?; + + // 4. Set GitHub check run to pending on PR head + match client + .create_check_run( + AUTO_BUILD_CHECK_RUN_NAME, + &gh_pr.head.sha, + CheckRunStatus::InProgress, + CheckRunOutput { + title: AUTO_BUILD_CHECK_RUN_NAME.to_string(), + summary: "".to_string(), + text: None, + annotations: vec![], + images: vec![], + }, + &build_id.to_string(), + ) + .await + { + Ok(check_run) => { + tracing::info!( + "Created check run {} for build {build_id}", + check_run.id.into_inner() + ); + ctx.db + .update_build_check_run_id(build_id, check_run.id.into_inner() as i64) + .await?; + } + Err(error) => { + // Check runs aren't critical, don't block progress if they fail + tracing::error!("Cannot create check run: {error:?}"); + } + } + + // 5. Post status comment + let comment = auto_build_started_comment(&gh_pr.head.sha, &merge_sha); + client.post_comment(pr.number, comment).await?; + + Ok(true) + } + MergeResult::Conflict => Ok(false), + } +} + +/// Attempts to merge the given head SHA with base SHA via `AUTO_MERGE_BRANCH_NAME`. +async fn attempt_merge( + client: &GithubRepositoryClient, + head_sha: &CommitSha, + base_sha: &CommitSha, + merge_message: &str, +) -> anyhow::Result { + tracing::debug!("Attempting to merge with base SHA {base_sha}"); + + // Reset auto merge branch to point to base branch + client + .set_branch_to_sha(AUTO_MERGE_BRANCH_NAME, base_sha, ForcePush::Yes) + .await + .map_err(|error| anyhow!("Cannot set auto merge branch to {}: {error:?}", base_sha.0))?; + + // then merge PR head commit into auto merge branch. + match client + .merge_branches(AUTO_MERGE_BRANCH_NAME, head_sha, merge_message) + .await + { + Ok(merge_sha) => { + tracing::debug!("Merge successful, SHA: {merge_sha}"); + Ok(MergeResult::Success(merge_sha)) + } + Err(MergeError::Conflict) => { + tracing::warn!("Merge conflict"); + Ok(MergeResult::Conflict) + } + Err(error) => Err(error.into()), + } +} + pub fn start_merge_queue(ctx: Arc) -> (MergeQueueSender, impl Future) { let (tx, mut rx) = mpsc::channel::(10); let sender = MergeQueueSender { inner: tx }; @@ -103,3 +281,76 @@ pub fn start_merge_queue(ctx: Arc) -> (MergeQueueSender, impl Futur (sender, fut) } + +#[cfg(test)] +mod tests { + + use octocrab::params::checks::CheckRunStatus; + + use crate::{ + bors::merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME}, + database::{WorkflowStatus, operations::get_all_workflows}, + tests::mocks::{WorkflowEvent, run_test}, + }; + + #[sqlx::test] + async fn auto_workflow_started(pool: sqlx::PgPool) { + run_test(pool.clone(), |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester + .workflow_event(WorkflowEvent::started(tester.auto_branch())) + .await?; + Ok(tester) + }) + .await; + let suite = get_all_workflows(&pool).await.unwrap().pop().unwrap(); + assert_eq!(suite.status, WorkflowStatus::Pending); + } + + #[sqlx::test] + async fn auto_workflow_check_run_created(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::InProgress, + None, + ); + Ok(tester) + }) + .await; + } + + #[sqlx::test] + async fn auto_workflow_started_comment(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + insta::assert_snapshot!( + tester.get_comment().await?, + @":hourglass: Testing commit pr-1-sha with merge merge-main-sha1-pr-1-sha-0..." + ); + Ok(tester) + }) + .await; + } +} diff --git a/src/bors/mod.rs b/src/bors/mod.rs index 39c3413c..66ed8808 100644 --- a/src/bors/mod.rs +++ b/src/bors/mod.rs @@ -37,6 +37,9 @@ pub static WAIT_FOR_PR_STATUS_REFRESH: TestSyncMarker = TestSyncMarker::new(); #[cfg(test)] pub static WAIT_FOR_WORKFLOW_STARTED: TestSyncMarker = TestSyncMarker::new(); +#[cfg(test)] +pub static WAIT_FOR_MERGE_QUEUE: TestSyncMarker = TestSyncMarker::new(); + #[derive(Clone, Debug, PartialEq, Eq)] pub enum CheckSuiteStatus { Pending, diff --git a/src/database/client.rs b/src/database/client.rs index 3c3d7e98..8845e80a 100644 --- a/src/database/client.rs +++ b/src/database/client.rs @@ -17,8 +17,9 @@ use super::operations::{ get_running_builds, get_workflow_urls_for_build, get_workflows_for_build, insert_repo_if_not_exists, set_pr_assignees, set_pr_priority, set_pr_rollup, set_pr_status, unapprove_pull_request, undelegate_pull_request, update_build_check_run_id, - update_build_status, update_mergeable_states_by_base_branch, update_pr_mergeable_state, - update_pr_try_build_id, update_workflow_status, upsert_pull_request, upsert_repository, + update_build_status, update_mergeable_states_by_base_branch, update_pr_auto_build_id, + update_pr_mergeable_state, update_pr_try_build_id, update_workflow_status, upsert_pull_request, + upsert_repository, }; use super::{ApprovalInfo, DelegatedPermission, MergeableState, RunId, UpsertPullRequestParams}; @@ -191,6 +192,21 @@ impl PgDbClient { Ok(build_id) } + pub async fn attach_auto_build( + &self, + pr: &PullRequestModel, + branch: String, + commit_sha: CommitSha, + parent: CommitSha, + ) -> anyhow::Result { + let mut tx = self.pool.begin().await?; + let build_id = + create_build(&mut *tx, &pr.repository, &branch, &commit_sha, &parent).await?; + update_pr_auto_build_id(&mut *tx, pr.id, build_id).await?; + tx.commit().await?; + Ok(build_id) + } + pub async fn find_build( &self, repo: &GithubRepoName, diff --git a/src/database/operations.rs b/src/database/operations.rs index ce10d361..1095c415 100644 --- a/src/database/operations.rs +++ b/src/database/operations.rs @@ -515,6 +515,24 @@ pub(crate) async fn update_pr_try_build_id( .await } +pub(crate) async fn update_pr_auto_build_id( + executor: impl PgExecutor<'_>, + pr_id: i32, + auto_build_id: i32, +) -> anyhow::Result<()> { + measure_db_query("update_pr_auto_build_id", || async { + sqlx::query!( + "UPDATE pull_request SET auto_build_id = $1 WHERE id = $2", + auto_build_id, + pr_id + ) + .execute(executor) + .await?; + Ok(()) + }) + .await +} + pub(crate) async fn create_build( executor: impl PgExecutor<'_>, repo: &GithubRepoName, diff --git a/src/tests/mocks/bors.rs b/src/tests/mocks/bors.rs index 4b9a54d7..b2987797 100644 --- a/src/tests/mocks/bors.rs +++ b/src/tests/mocks/bors.rs @@ -19,10 +19,12 @@ use super::repository::PullRequest; use crate::bors::merge_queue::MergeQueueSender; use crate::bors::mergeable_queue::MergeableQueueSender; use crate::bors::{ - RollupMode, WAIT_FOR_CANCEL_TIMED_OUT_BUILDS_REFRESH, WAIT_FOR_MERGEABILITY_STATUS_REFRESH, - WAIT_FOR_PR_STATUS_REFRESH, WAIT_FOR_WORKFLOW_STARTED, + RollupMode, WAIT_FOR_CANCEL_TIMED_OUT_BUILDS_REFRESH, WAIT_FOR_MERGE_QUEUE, + WAIT_FOR_MERGEABILITY_STATUS_REFRESH, WAIT_FOR_PR_STATUS_REFRESH, WAIT_FOR_WORKFLOW_STARTED, +}; +use crate::database::{ + BuildStatus, DelegatedPermission, OctocrabMergeableState, PullRequestModel, TreeState, }; -use crate::database::{BuildStatus, DelegatedPermission, OctocrabMergeableState, PullRequestModel}; use crate::github::api::load_repositories; use crate::github::server::BorsProcess; use crate::github::{GithubRepoName, PullRequestNumber}; @@ -127,7 +129,13 @@ impl BorsTester { let mut repos = HashMap::default(); for (name, repo) in loaded_repos { let repo = repo.unwrap(); - repos.insert(name, Arc::new(repo)); + repos.insert(name.clone(), Arc::new(repo)); + } + + for (name, _repo) in &repos { + if let Err(error) = db.insert_repo_if_not_exists(name, TreeState::Open).await { + tracing::warn!("Failed to insert repository {name} in test: {error:?}"); + } } let ctx = BorsContext::new( @@ -277,6 +285,10 @@ impl BorsTester { self.get_branch("automation/bors/try") } + pub fn auto_branch(&self) -> Branch { + self.get_branch("automation/bors/auto") + } + /// Wait until the next bot comment is received on the default repo and the default PR. pub async fn get_comment(&mut self) -> anyhow::Result { Ok(self @@ -340,6 +352,22 @@ impl BorsTester { .unwrap(); } + pub async fn process_merge_queue(&self) { + // Wait until the merge queue processing is fully handled + wait_for_marker( + async || { + self.global_tx + .send(BorsGlobalEvent::ProcessMergeQueue) + .await + .unwrap(); + Ok(()) + }, + &WAIT_FOR_MERGE_QUEUE, + ) + .await + .unwrap(); + } + /// Performs a single started/success/failure workflow event. pub async fn workflow_event(&mut self, event: WorkflowEvent) -> anyhow::Result<()> { if let Some(branch) = self diff --git a/src/tests/mocks/repository.rs b/src/tests/mocks/repository.rs index 03bd0de7..36ecdd3a 100644 --- a/src/tests/mocks/repository.rs +++ b/src/tests/mocks/repository.rs @@ -232,6 +232,7 @@ impl Default for Repo { fn default() -> Self { let config = r#" timeout = 3600 +merge_queue_enabled = true # Set labels on PR approvals [labels] From 4071b91474cedb1639241d111f413c98e66ef2cf Mon Sep 17 00:00:00 2001 From: Sakibul Islam Date: Sat, 12 Jul 2025 12:25:11 +0100 Subject: [PATCH 2/4] Deal with auto build success - Post comment - Update check run - Modify labels --- rust-bors.example.toml | 2 + src/bors/comment.rs | 17 ++++ src/bors/handlers/workflow.rs | 171 ++++++++++++++++++++++++++++++---- src/bors/merge_queue.rs | 36 ++++++- src/config.rs | 26 +++++- src/github/labels.rs | 2 + 6 files changed, 230 insertions(+), 24 deletions(-) diff --git a/rust-bors.example.toml b/rust-bors.example.toml index f739e63e..46188ae0 100644 --- a/rust-bors.example.toml +++ b/rust-bors.example.toml @@ -19,3 +19,5 @@ approve = ["+approved"] try = ["+foo", "-bar"] try_succeed = ["+foobar", "+foo", "+baz"] try_failed = [] +succeeded = ["+foo", "+bar"] +failed = ["+foo", "+bar"] diff --git a/src/bors/comment.rs b/src/bors/comment.rs index f9d126e6..c75d31fb 100644 --- a/src/bors/comment.rs +++ b/src/bors/comment.rs @@ -192,3 +192,20 @@ pub fn auto_build_started_comment(head_sha: &CommitSha, merge_sha: &CommitSha) - head_sha, merge_sha )) } + +pub fn auto_build_succeeded_comment( + workflows: &[WorkflowModel], + approved_by: &str, + merge_sha: &CommitSha, + base_ref: &str, +) -> Comment { + let urls = workflows + .iter() + .map(|w| format!("[{}]({})", w.name, w.url)) + .collect::>() + .join(", "); + + Comment::new(format!( + ":sunny: Test successful - {urls}\nApproved by: `{approved_by}`\nPushing {merge_sha} to `{base_ref}`...", + )) +} diff --git a/src/bors/handlers/workflow.rs b/src/bors/handlers/workflow.rs index a4bf7c0f..87549091 100644 --- a/src/bors/handlers/workflow.rs +++ b/src/bors/handlers/workflow.rs @@ -9,6 +9,7 @@ use crate::bors::CheckSuiteStatus; use crate::bors::RepositoryState; use crate::bors::comment::{try_build_succeeded_comment, workflow_failed_comment}; use crate::bors::event::{CheckSuiteCompleted, WorkflowCompleted, WorkflowStarted}; +use crate::bors::handlers::TRY_BRANCH_NAME; use crate::bors::handlers::is_bors_observed_branch; use crate::bors::handlers::labels::handle_label_trigger; use crate::bors::merge_queue::AUTO_BRANCH_NAME; @@ -176,6 +177,7 @@ async fn try_complete_build( let has_failure = checks .iter() .any(|check| matches!(check.status, CheckSuiteStatus::Failure)); + let build_succeeded = !has_failure; let mut workflows = db.get_workflows_for_build(&build).await?; workflows.sort_by(|a, b| a.name.cmp(&b.name)); @@ -192,42 +194,78 @@ async fn try_complete_build( return Ok(()); } - let (status, trigger) = if has_failure { - (BuildStatus::Failure, LabelTrigger::TryBuildFailed) + let pr_num = pr.number; + + if build_succeeded { + tracing::info!("Build succeeded for PR {pr_num}"); } else { - (BuildStatus::Success, LabelTrigger::TryBuildSucceeded) + tracing::info!("Build failed for PR {pr_num}"); + } + + let branch = payload.branch.as_str(); + let (status, trigger, comment_opt) = match (branch, build_succeeded) { + (TRY_BRANCH_NAME, true) => ( + BuildStatus::Success, + LabelTrigger::TryBuildSucceeded, + Some(try_build_succeeded_comment( + &workflows, + payload.commit_sha.clone(), + &build, + )), + ), + (TRY_BRANCH_NAME, false) => ( + BuildStatus::Failure, + LabelTrigger::TryBuildFailed, + Some(workflow_failed_comment(&workflows)), + ), + (AUTO_BRANCH_NAME, true) => (BuildStatus::Success, LabelTrigger::Succeeded, None), + (AUTO_BRANCH_NAME, false) => ( + BuildStatus::Failure, + LabelTrigger::Failed, + Some(workflow_failed_comment(&workflows)), + ), + _ => unreachable!("Branch should be bors observed branch"), }; + db.update_build_status(&build, status).await?; + handle_label_trigger(repo, pr_num, trigger).await?; - handle_label_trigger(repo, pr.number, trigger).await?; + if let Some(comment) = comment_opt { + repo.client.post_comment(pr_num, comment).await?; + } if let Some(check_run_id) = build.check_run_id { - let (status, conclusion) = if has_failure { - (CheckRunStatus::Completed, Some(CheckRunConclusion::Failure)) + tracing::info!( + "Updating check run {check_run_id} for build {} on {branch}", + build.commit_sha, + ); + let conclusion = if build_succeeded { + Some(CheckRunConclusion::Success) } else { - (CheckRunStatus::Completed, Some(CheckRunConclusion::Success)) + Some(CheckRunConclusion::Failure) }; if let Err(error) = repo .client - .update_check_run(check_run_id as u64, status, conclusion, None) + .update_check_run( + check_run_id as u64, + CheckRunStatus::Completed, + conclusion, + None, + ) .await { tracing::error!("Could not update check run {check_run_id}: {error:?}"); } - } - - let message = if !has_failure { - tracing::info!("Workflow succeeded"); - try_build_succeeded_comment(&workflows, payload.commit_sha, &build) } else { - tracing::info!("Workflow failed"); - workflow_failed_comment(&workflows) - }; - repo.client.post_comment(pr.number, message).await?; + tracing::warn!( + "No check_run_id found for build {} on {branch}", + build.commit_sha, + ); + } // Trigger merge queue when an auto build completes - if payload.branch == AUTO_BRANCH_NAME { + if branch == AUTO_BRANCH_NAME { merge_queue_tx.trigger().await?; } @@ -236,8 +274,10 @@ async fn try_complete_build( #[cfg(test)] mod tests { + use octocrab::params::checks::{CheckRunConclusion, CheckRunStatus}; + use crate::bors::handlers::trybuild::TRY_BRANCH_NAME; - use crate::bors::merge_queue::{AUTO_BUILD_CHECK_RUN_NAME, AUTO_BRANCH_NAME}; + use crate::bors::merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME}; use crate::database::WorkflowStatus; use crate::database::operations::get_all_workflows; use crate::tests::mocks::{ @@ -424,4 +464,97 @@ mod tests { }) .await; } + + #[sqlx::test] + async fn auto_build_success_comment(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_success(tester.auto_branch()).await?; + tester.process_merge_queue().await; + insta::assert_snapshot!( + tester.get_comment().await?, + @r" + :sunny: Test successful - [Workflow1](https://github.com/workflows/Workflow1/1) + Approved by: `default-user` + Pushing merge-main-sha1-pr-1-sha-0 to `main`... + " + ); + + Ok(tester) + }) + .await; + } + + #[sqlx::test] + async fn auto_build_check_run_success(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_success(tester.auto_branch()).await?; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::Completed, + Some(CheckRunConclusion::Success), + ); + + Ok(tester) + }) + .await; + } + + #[sqlx::test] + async fn auto_build_success_labels(pool: sqlx::PgPool) { + let github = GitHubState::default().with_default_config( + r#" +merge_queue_enabled = true + +[labels] +succeeded = ["+foo", "+bar", "-baz"] +"#, + ); + + BorsBuilder::new(pool) + .github(github) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + let repo = tester.default_repo(); + repo.lock() + .get_pr(default_pr_number()) + .check_added_labels(&[]); + tester.workflow_success(tester.auto_branch()).await?; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + let pr = repo.lock().get_pr(default_pr_number()).clone(); + pr.check_added_labels(&["foo", "bar"]); + pr.check_removed_labels(&["baz"]); + + Ok(tester) + }) + .await; + } } diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs index 2ef2e8f4..e78ed82d 100644 --- a/src/bors/merge_queue.rs +++ b/src/bors/merge_queue.rs @@ -6,8 +6,8 @@ use tokio::sync::mpsc; use tracing::Instrument; use crate::BorsContext; -use crate::bors::RepositoryState; -use crate::bors::comment::auto_build_started_comment; +use crate::bors::comment::{auto_build_started_comment, auto_build_succeeded_comment}; +use crate::bors::{PullRequestStatus, RepositoryState}; use crate::database::{BuildStatus, PullRequestModel}; use crate::github::api::client::GithubRepositoryClient; use crate::github::api::operations::ForcePush; @@ -84,9 +84,39 @@ pub async fn merge_queue_tick(ctx: Arc) -> anyhow::Result<()> { let pr_num = pr.number; if let Some(auto_build) = &pr.auto_build { + let commit_sha = CommitSha(auto_build.commit_sha.clone()); + match auto_build.status { // Build successful - point the base branch to the merged commit. BuildStatus::Success => { + let workflows = ctx.db.get_workflows_for_build(auto_build).await?; + let comment = auto_build_succeeded_comment( + &workflows, + pr.approver().unwrap_or(""), + &commit_sha, + &pr.base_branch, + ); + repo.client.post_comment(pr.number, comment).await?; + + match repo + .client + .set_branch_to_sha(&pr.base_branch, &commit_sha, ForcePush::No) + .await + { + Ok(()) => { + tracing::info!("Auto build succeeded and merged for PR {pr_num}"); + + ctx.db + .set_pr_status( + &pr.repository, + pr.number, + PullRequestStatus::Merged, + ) + .await?; + } + Err(_) => {} + }; + // Break to give GitHub time to update the base branch. break; } @@ -337,7 +367,7 @@ mod tests { } #[sqlx::test] - async fn auto_workflow_started_comment(pool: sqlx::PgPool) { + async fn auto_build_started_comment(pool: sqlx::PgPool) { run_test(pool, |mut tester| async { tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); diff --git a/src/config.rs b/src/config.rs index 28e8634f..d6da7a10 100644 --- a/src/config.rs +++ b/src/config.rs @@ -69,6 +69,8 @@ where Try, TrySucceed, TryFailed, + Succeeded, + Failed, } impl From for LabelTrigger { @@ -79,6 +81,8 @@ where Trigger::Try => LabelTrigger::TryBuildStarted, Trigger::TrySucceed => LabelTrigger::TryBuildSucceeded, Trigger::TryFailed => LabelTrigger::TryBuildFailed, + Trigger::Succeeded => LabelTrigger::Succeeded, + Trigger::Failed => LabelTrigger::Failed, } } } @@ -206,9 +210,11 @@ approve = ["+approved"] try = ["+foo", "-bar"] try_succeed = ["+foobar", "+foo", "+baz"] try_failed = [] +auto_succeed = ["+foobar", "-foo"] +auto_failed = ["+bar", "+baz"] "#; let config = load_config(content); - insta::assert_debug_snapshot!(config.labels.into_iter().collect::>(), @r###" + insta::assert_debug_snapshot!(config.labels.into_iter().collect::>(), @r#" { Approved: [ Add( @@ -240,8 +246,24 @@ try_failed = [] ), ], TryBuildFailed: [], + AutoBuildSucceeded: [ + Add( + "foobar", + ), + Remove( + "foo", + ), + ], + AutoBuildFailed: [ + Add( + "bar", + ), + Add( + "baz", + ), + ], } - "###); + "#); } #[test] diff --git a/src/github/labels.rs b/src/github/labels.rs index 337bb33b..e12fe16c 100644 --- a/src/github/labels.rs +++ b/src/github/labels.rs @@ -6,6 +6,8 @@ pub enum LabelTrigger { TryBuildStarted, TryBuildSucceeded, TryBuildFailed, + Succeeded, + Failed, } #[derive(Debug, Eq, PartialEq)] From cbe518d35d67a715eb60b43535afc1f129107b3a Mon Sep 17 00:00:00 2001 From: Sakibul Islam Date: Sun, 13 Jul 2025 10:21:19 +0100 Subject: [PATCH 3/4] Deal with auto build failure - Post comment - Update check run --- src/bors/comment.rs | 6 ++ src/bors/merge_queue.rs | 122 ++++++++++++++++++++++++++++++++-- src/tests/mocks/bors.rs | 8 +++ src/tests/mocks/repository.rs | 7 ++ 4 files changed, 139 insertions(+), 4 deletions(-) diff --git a/src/bors/comment.rs b/src/bors/comment.rs index c75d31fb..e5c82104 100644 --- a/src/bors/comment.rs +++ b/src/bors/comment.rs @@ -209,3 +209,9 @@ pub fn auto_build_succeeded_comment( ":sunny: Test successful - {urls}\nApproved by: `{approved_by}`\nPushing {merge_sha} to `{base_ref}`...", )) } + +pub fn auto_build_push_failed_comment(error: &str) -> Comment { + Comment::new(format!( + ":eyes: Test was successful, but fast-forwarding failed: {error}" + )) +} diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs index e78ed82d..f4c765ac 100644 --- a/src/bors/merge_queue.rs +++ b/src/bors/merge_queue.rs @@ -1,12 +1,14 @@ use anyhow::anyhow; -use octocrab::params::checks::{CheckRunOutput, CheckRunStatus}; +use octocrab::params::checks::{CheckRunConclusion, CheckRunOutput, CheckRunStatus}; use std::future::Future; use std::sync::Arc; use tokio::sync::mpsc; use tracing::Instrument; use crate::BorsContext; -use crate::bors::comment::{auto_build_started_comment, auto_build_succeeded_comment}; +use crate::bors::comment::{ + auto_build_push_failed_comment, auto_build_started_comment, auto_build_succeeded_comment, +}; use crate::bors::{PullRequestStatus, RepositoryState}; use crate::database::{BuildStatus, PullRequestModel}; use crate::github::api::client::GithubRepositoryClient; @@ -114,7 +116,36 @@ pub async fn merge_queue_tick(ctx: Arc) -> anyhow::Result<()> { ) .await?; } - Err(_) => {} + Err(error) => { + tracing::error!( + "Failed to push PR {pr_num} to base branch: {:?}", + error + ); + + if let Some(check_run_id) = auto_build.check_run_id { + if let Err(error) = repo + .client + .update_check_run( + check_run_id as u64, + CheckRunStatus::Completed, + Some(CheckRunConclusion::Failure), + None, + ) + .await + { + tracing::error!( + "Could not update check run {check_run_id}: {error:?}" + ); + } + } + + ctx.db + .update_build_status(auto_build, BuildStatus::Failure) + .await?; + + let comment = auto_build_push_failed_comment(&error.to_string()); + repo.client.post_comment(pr.number, comment).await?; + } }; // Break to give GitHub time to update the base branch. @@ -315,7 +346,7 @@ pub fn start_merge_queue(ctx: Arc) -> (MergeQueueSender, impl Futur #[cfg(test)] mod tests { - use octocrab::params::checks::CheckRunStatus; + use octocrab::params::checks::{CheckRunConclusion, CheckRunStatus}; use crate::{ bors::merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME}, @@ -383,4 +414,87 @@ mod tests { }) .await; } + + #[sqlx::test] + async fn auto_build_push_fail_comment(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + + tester.default_repo().lock().push_error = true; + + tester.process_merge_queue().await; + insta::assert_snapshot!( + tester.get_comment().await?, + @":eyes: Test was successful, but fast-forwarding failed: IO error" + ); + + Ok(tester) + }) + .await; + } + + #[sqlx::test] + async fn auto_build_push_fail_updates_check_run(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + + tester.default_repo().lock().push_error = true; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::Completed, + Some(CheckRunConclusion::Failure), + ); + Ok(tester) + }) + .await; + } + + #[sqlx::test] + async fn auto_build_push_fail_in_db(pool: sqlx::PgPool) { + run_test(pool, |mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + + tester.default_repo().lock().push_error = true; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.default_pr().await.expect_auto_build_failed(); + Ok(tester) + }) + .await; + } } diff --git a/src/tests/mocks/bors.rs b/src/tests/mocks/bors.rs index b2987797..6d79770b 100644 --- a/src/tests/mocks/bors.rs +++ b/src/tests/mocks/bors.rs @@ -951,6 +951,14 @@ impl PullRequestProxy { ); } + #[track_caller] + pub fn expect_auto_build_failed(&self) { + assert_eq!( + self.require_db_pr().auto_build.as_ref().unwrap().status, + BuildStatus::Failure + ); + } + #[track_caller] fn require_db_pr(&self) -> &PullRequestModel { self.db_pr.as_ref().unwrap() diff --git a/src/tests/mocks/repository.rs b/src/tests/mocks/repository.rs index 36ecdd3a..7d9dd813 100644 --- a/src/tests/mocks/repository.rs +++ b/src/tests/mocks/repository.rs @@ -152,6 +152,8 @@ pub struct Repo { pub check_runs: Vec, // Cause pull request fetch to fail. pub pull_request_error: bool, + // Cause branch push to fail. + pub push_error: bool, pub pr_push_counter: u64, } @@ -166,6 +168,7 @@ impl Repo { cancelled_workflows: vec![], workflow_cancel_error: false, pull_request_error: false, + push_error: false, pr_push_counter: 0, check_runs: vec![], } @@ -499,6 +502,10 @@ async fn mock_update_branch(repo: Arc>, mock_server: &MockServer) { } let data: SetRefRequest = req.body_json().unwrap(); + + if repo.push_error { + return ResponseTemplate::new(500).set_body_string("Simulated push error"); + } let sha = data.sha; match repo.get_branch_by_name(branch_name) { From d47d6e0393d970e9b2c3bde9b89b3a31fa26f49e Mon Sep 17 00:00:00 2001 From: Sakibul Islam Date: Tue, 15 Jul 2025 17:12:53 +0100 Subject: [PATCH 4/4] Add tests --- src/bors/handlers/pr_events.rs | 1 + src/bors/handlers/workflow.rs | 169 ++++++++++---- src/bors/merge_queue.rs | 398 +++++++++++++++++++++++---------- src/config.rs | 8 +- src/tests/mocks/mod.rs | 2 +- src/tests/mocks/repository.rs | 3 +- 6 files changed, 420 insertions(+), 161 deletions(-) diff --git a/src/bors/handlers/pr_events.rs b/src/bors/handlers/pr_events.rs index c63247f3..d5a6eb56 100644 --- a/src/bors/handlers/pr_events.rs +++ b/src/bors/handlers/pr_events.rs @@ -334,6 +334,7 @@ async fn notify_of_unclean_auto_build_cancelled_comment( #[cfg(test)] mod tests { + use crate::bors::PullRequestStatus; use crate::tests::mocks::default_pr_number; use crate::{ diff --git a/src/bors/handlers/workflow.rs b/src/bors/handlers/workflow.rs index 87549091..8f8dc19c 100644 --- a/src/bors/handlers/workflow.rs +++ b/src/bors/handlers/workflow.rs @@ -278,13 +278,21 @@ mod tests { use crate::bors::handlers::trybuild::TRY_BRANCH_NAME; use crate::bors::merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME}; - use crate::database::WorkflowStatus; use crate::database::operations::get_all_workflows; + use crate::database::{BuildStatus, WorkflowStatus}; use crate::tests::mocks::{ BorsBuilder, Branch, CheckSuite, GitHubState, Workflow, WorkflowEvent, default_pr_number, run_test, }; + fn gh_state_with_merge_queue() -> GitHubState { + GitHubState::default().with_default_config( + r#" + merge_queue_enabled = true + "#, + ) + } + #[sqlx::test] async fn workflow_started_unknown_build(pool: sqlx::PgPool) { run_test(pool.clone(), |mut tester| async { @@ -467,57 +475,69 @@ mod tests { #[sqlx::test] async fn auto_build_success_comment(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + let github = GitHubState::default().with_default_config( + r#" +merge_queue_enabled = true +"#, + ); - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; + BorsBuilder::new(pool) + .github(github) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - tester.process_merge_queue().await; - tester.expect_comments(1).await; + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; - tester.workflow_success(tester.auto_branch()).await?; - tester.process_merge_queue().await; - insta::assert_snapshot!( - tester.get_comment().await?, - @r" + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_success(tester.auto_branch()).await?; + tester.process_merge_queue().await; + insta::assert_snapshot!( + tester.get_comment().await?, + @r" :sunny: Test successful - [Workflow1](https://github.com/workflows/Workflow1/1) Approved by: `default-user` Pushing merge-main-sha1-pr-1-sha-0 to `main`... " - ); + ); - Ok(tester) - }) - .await; + Ok(tester) + }) + .await; } #[sqlx::test] async fn auto_build_check_run_success(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; - - tester.process_merge_queue().await; - tester.expect_comments(1).await; - - tester.workflow_success(tester.auto_branch()).await?; - tester.process_merge_queue().await; - tester.expect_comments(1).await; + let github = GitHubState::default().with_default_config( + r#" +merge_queue_enabled = true +"#, + ); - tester.expect_check_run( - &tester.default_pr().await.get_gh_pr().head_sha, - AUTO_BUILD_CHECK_RUN_NAME, - AUTO_BUILD_CHECK_RUN_NAME, - CheckRunStatus::Completed, - Some(CheckRunConclusion::Success), - ); + BorsBuilder::new(pool) + .github(github) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + tester.process_merge_queue().await; + tester.expect_comments(1).await; - Ok(tester) - }) - .await; + tester.workflow_success(tester.auto_branch()).await?; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::Completed, + Some(CheckRunConclusion::Success), + ); + Ok(tester) + }) + .await; } #[sqlx::test] @@ -537,7 +557,6 @@ succeeded = ["+foo", "+bar", "-baz"] tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); tester.post_comment("@bors r+").await?; tester.expect_comments(1).await; - tester.process_merge_queue().await; tester.expect_comments(1).await; @@ -557,4 +576,76 @@ succeeded = ["+foo", "+bar", "-baz"] }) .await; } + + #[sqlx::test] + async fn auto_build_failed_labels(pool: sqlx::PgPool) { + let github = GitHubState::default().with_default_config( + r#" + merge_queue_enabled = true + + [labels] + failed = ["+foo", "+bar", "-baz"] + "#, + ); + + BorsBuilder::new(pool) + .github(github) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + let repo = tester.default_repo(); + repo.lock() + .get_pr(default_pr_number()) + .check_added_labels(&[]); + + tester.workflow_failure(tester.auto_branch()).await?; + tester + .wait_for_default_pr(|pr| { + pr.auto_build.as_ref().unwrap().status == BuildStatus::Failure + }) + .await?; + tester.expect_comments(1).await; + + let pr = repo.lock().get_pr(default_pr_number()).clone(); + pr.check_added_labels(&["foo", "bar"]); + pr.check_removed_labels(&["baz"]); + + Ok(tester) + }) + .await; + } + + #[sqlx::test] + async fn auto_build_failure_updates_check_run_on(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + + tester.workflow_failure(tester.auto_branch()).await?; + tester.expect_comments(1).await; + tester + .wait_for_default_pr(|pr| { + pr.auto_build.as_ref().unwrap().status == BuildStatus::Failure + }) + .await?; + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::Completed, + Some(CheckRunConclusion::Failure), + ); + Ok(tester) + }) + .await; + } } diff --git a/src/bors/merge_queue.rs b/src/bors/merge_queue.rs index f4c765ac..07b86083 100644 --- a/src/bors/merge_queue.rs +++ b/src/bors/merge_queue.rs @@ -349,152 +349,320 @@ mod tests { use octocrab::params::checks::{CheckRunConclusion, CheckRunStatus}; use crate::{ - bors::merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME}, - database::{WorkflowStatus, operations::get_all_workflows}, - tests::mocks::{WorkflowEvent, run_test}, + bors::{ + PullRequestStatus, + merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME, AUTO_MERGE_BRANCH_NAME}, + }, + database::{BuildStatus, WorkflowStatus, operations::get_all_workflows}, + github::CommitSha, + tests::mocks::{ + BorsBuilder, GitHubState, Workflow, WorkflowEvent, bors::BorsTester, default_repo_name, + }, }; - #[sqlx::test] - async fn auto_workflow_started(pool: sqlx::PgPool) { - run_test(pool.clone(), |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; + fn gh_state_with_merge_queue() -> GitHubState { + GitHubState::default().with_default_config( + r#" + merge_queue_enabled = true + "#, + ) + } - tester.process_merge_queue().await; - tester.expect_comments(1).await; + async fn start_auto_build(tester: &mut BorsTester) -> anyhow::Result<()> { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + Ok(()) + } - tester - .workflow_event(WorkflowEvent::started(tester.auto_branch())) - .await?; - Ok(tester) - }) - .await; + #[sqlx::test] + async fn auto_workflow_started(pool: sqlx::PgPool) { + BorsBuilder::new(pool.clone()) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester + .workflow_event(WorkflowEvent::started(tester.auto_branch())) + .await?; + Ok(tester) + }) + .await; let suite = get_all_workflows(&pool).await.unwrap().pop().unwrap(); assert_eq!(suite.status, WorkflowStatus::Pending); } #[sqlx::test] async fn auto_workflow_check_run_created(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; - - tester.process_merge_queue().await; - tester.expect_comments(1).await; - tester.expect_check_run( - &tester.default_pr().await.get_gh_pr().head_sha, - AUTO_BUILD_CHECK_RUN_NAME, - AUTO_BUILD_CHECK_RUN_NAME, - CheckRunStatus::InProgress, - None, - ); - Ok(tester) - }) - .await; + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::InProgress, + None, + ); + Ok(tester) + }) + .await; } #[sqlx::test] async fn auto_build_started_comment(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; - - tester.process_merge_queue().await; - insta::assert_snapshot!( - tester.get_comment().await?, - @":hourglass: Testing commit pr-1-sha with merge merge-main-sha1-pr-1-sha-0..." - ); - Ok(tester) - }) - .await; + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + tester.process_merge_queue().await; + insta::assert_snapshot!( + tester.get_comment().await?, + @":hourglass: Testing commit pr-1-sha with merge merge-main-sha1-pr-1-sha-0..." + ); + Ok(tester) + }) + .await; } #[sqlx::test] - async fn auto_build_push_fail_comment(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); + async fn auto_build_succeeds_and_merges_in_db(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + tester + .wait_for_default_pr(|pr| { + pr.auto_build.as_ref().unwrap().status == BuildStatus::Success + }) + .await?; + tester + .wait_for_default_pr(|pr| pr.pr_status == PullRequestStatus::Merged) + .await?; + Ok(tester) + }) + .await; + } - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; + #[sqlx::test] + async fn auto_build_insert_into_db(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + assert!( + tester + .db() + .find_build( + &default_repo_name(), + AUTO_BRANCH_NAME.to_string(), + CommitSha(tester.auto_branch().get_sha().to_string()), + ) + .await? + .is_some() + ); + Ok(tester) + }) + .await; + } - tester.process_merge_queue().await; - tester.expect_comments(1).await; + #[sqlx::test] + async fn auto_build_succeeded_comment(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + insta::assert_snapshot!( + tester.get_comment().await?, + @r" + :sunny: Test successful - [Workflow1](https://github.com/workflows/Workflow1/1) + Approved by: `default-user` + Pushing merge-main-sha1-pr-1-sha-0 to `main`... + " + ); + Ok(tester) + }) + .await; + } - tester.workflow_success(tester.auto_branch()).await?; - tester.expect_comments(1).await; + #[sqlx::test] + async fn auto_build_fails_in_db(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_failure(tester.auto_branch()).await?; + tester + .wait_for_default_pr(|pr| { + pr.auto_build.as_ref().unwrap().status == BuildStatus::Failure + }) + .await?; + tester + .wait_for_default_pr(|pr| pr.pr_status == PullRequestStatus::Open) + .await?; + tester.expect_comments(1).await; + Ok(tester) + }) + .await; + } - tester.default_repo().lock().push_error = true; + #[sqlx::test] + async fn auto_build_failure_comment(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_failure(tester.auto_branch()).await?; + tester + .wait_for_default_pr(|pr| { + pr.auto_build.as_ref().unwrap().status == BuildStatus::Failure + }) + .await?; + insta::assert_snapshot!( + tester.get_comment().await?, + @r" + :broken_heart: Test failed + - [Workflow1](https://github.com/workflows/Workflow1/1) :x: + " + ); + Ok(tester) + }) + .await; + } - tester.process_merge_queue().await; - insta::assert_snapshot!( - tester.get_comment().await?, - @":eyes: Test was successful, but fast-forwarding failed: IO error" - ); + #[sqlx::test] + async fn auto_build_partial_failure_multiple_suites(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + tester.create_branch(AUTO_BRANCH_NAME).expect_suites(2); + tester.post_comment("@bors r+").await?; + tester.expect_comments(1).await; + tester.process_merge_queue().await; + tester.expect_comments(1).await; + tester + .workflow_success(Workflow::from(tester.auto_branch()).with_run_id(1)) + .await?; + tester + .workflow_failure(Workflow::from(tester.auto_branch()).with_run_id(2)) + .await?; + tester + .wait_for_default_pr(|pr| { + pr.auto_build.as_ref().unwrap().status == BuildStatus::Failure + }) + .await?; + insta::assert_snapshot!( + tester.get_comment().await?, + @r" + :broken_heart: Test failed + - [Workflow1](https://github.com/workflows/Workflow1/1) :white_check_mark: + - [Workflow1](https://github.com/workflows/Workflow1/2) :x: + " + ); + Ok(tester) + }) + .await; + } - Ok(tester) - }) - .await; + #[sqlx::test] + async fn auto_build_push_fail_comment(pool: sqlx::PgPool) { + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + + tester.default_repo().lock().push_error = true; + + tester.process_merge_queue().await; + insta::assert_snapshot!( + tester.get_comment().await?, + @":eyes: Test was successful, but fast-forwarding failed: IO error" + ); + Ok(tester) + }) + .await; } #[sqlx::test] async fn auto_build_push_fail_updates_check_run(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; - - tester.process_merge_queue().await; - tester.expect_comments(1).await; - - tester.workflow_success(tester.auto_branch()).await?; - tester.expect_comments(1).await; - - tester.default_repo().lock().push_error = true; - - tester.process_merge_queue().await; - tester.expect_comments(1).await; - - tester.expect_check_run( - &tester.default_pr().await.get_gh_pr().head_sha, - AUTO_BUILD_CHECK_RUN_NAME, - AUTO_BUILD_CHECK_RUN_NAME, - CheckRunStatus::Completed, - Some(CheckRunConclusion::Failure), - ); - Ok(tester) - }) - .await; + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + + tester.default_repo().lock().push_error = true; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + tester.expect_check_run( + &tester.default_pr().await.get_gh_pr().head_sha, + AUTO_BUILD_CHECK_RUN_NAME, + AUTO_BUILD_CHECK_RUN_NAME, + CheckRunStatus::Completed, + Some(CheckRunConclusion::Failure), + ); + Ok(tester) + }) + .await; } #[sqlx::test] async fn auto_build_push_fail_in_db(pool: sqlx::PgPool) { - run_test(pool, |mut tester| async { - tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1); - - tester.post_comment("@bors r+").await?; - tester.expect_comments(1).await; - - tester.process_merge_queue().await; - tester.expect_comments(1).await; - - tester.workflow_success(tester.auto_branch()).await?; - tester.expect_comments(1).await; - - tester.default_repo().lock().push_error = true; - - tester.process_merge_queue().await; - tester.expect_comments(1).await; + BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + + tester.default_repo().lock().push_error = true; + + tester.process_merge_queue().await; + tester.expect_comments(1).await; + tester.default_pr().await.expect_auto_build_failed(); + Ok(tester) + }) + .await; + } - tester.default_pr().await.expect_auto_build_failed(); - Ok(tester) - }) - .await; + #[sqlx::test] + async fn auto_build_complete_branch_history(pool: sqlx::PgPool) { + let gh = BorsBuilder::new(pool) + .github(gh_state_with_merge_queue()) + .run_test(|mut tester| async { + start_auto_build(&mut tester).await?; + tester.workflow_success(tester.auto_branch()).await?; + tester.expect_comments(1).await; + Ok(tester) + }) + .await; + gh.check_sha_history( + default_repo_name(), + "main", + &["main-sha1", "merge-main-sha1-pr-1-sha-0"], + ); + gh.check_sha_history( + default_repo_name(), + AUTO_MERGE_BRANCH_NAME, + &["main-sha1", "merge-main-sha1-pr-1-sha-0"], + ); + gh.check_sha_history( + default_repo_name(), + AUTO_BRANCH_NAME, + &["automation/bors/auto-initial", "merge-main-sha1-pr-1-sha-0"], + ); } } diff --git a/src/config.rs b/src/config.rs index d6da7a10..7b0fc858 100644 --- a/src/config.rs +++ b/src/config.rs @@ -210,8 +210,8 @@ approve = ["+approved"] try = ["+foo", "-bar"] try_succeed = ["+foobar", "+foo", "+baz"] try_failed = [] -auto_succeed = ["+foobar", "-foo"] -auto_failed = ["+bar", "+baz"] +succeeded = ["+foobar", "-foo"] +failed = ["+bar", "+baz"] "#; let config = load_config(content); insta::assert_debug_snapshot!(config.labels.into_iter().collect::>(), @r#" @@ -246,7 +246,7 @@ auto_failed = ["+bar", "+baz"] ), ], TryBuildFailed: [], - AutoBuildSucceeded: [ + Succeeded: [ Add( "foobar", ), @@ -254,7 +254,7 @@ auto_failed = ["+bar", "+baz"] "foo", ), ], - AutoBuildFailed: [ + Failed: [ Add( "bar", ), diff --git a/src/tests/mocks/mod.rs b/src/tests/mocks/mod.rs index 2e292d5e..d4c51c3a 100644 --- a/src/tests/mocks/mod.rs +++ b/src/tests/mocks/mod.rs @@ -29,7 +29,7 @@ pub use workflow::Workflow; pub use workflow::WorkflowEvent; mod app; -mod bors; +pub mod bors; mod comment; mod github; mod permissions; diff --git a/src/tests/mocks/repository.rs b/src/tests/mocks/repository.rs index 7d9dd813..c3bba719 100644 --- a/src/tests/mocks/repository.rs +++ b/src/tests/mocks/repository.rs @@ -235,7 +235,6 @@ impl Default for Repo { fn default() -> Self { let config = r#" timeout = 3600 -merge_queue_enabled = true # Set labels on PR approvals [labels] @@ -502,7 +501,7 @@ async fn mock_update_branch(repo: Arc>, mock_server: &MockServer) { } let data: SetRefRequest = req.body_json().unwrap(); - + if repo.push_error { return ResponseTemplate::new(500).set_body_string("Simulated push error"); }