Skip to content

Commit 76c1d7a

Browse files
committed
Start auto builds in merge queue
1 parent b083539 commit 76c1d7a

File tree

9 files changed

+352
-9
lines changed

9 files changed

+352
-9
lines changed

.sqlx/query-3ae0ba7ce98f0a68b47df52a9181134df3ac7818af0c085ac432eb52874a62a2.json

Lines changed: 15 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/bors/comment.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,3 +185,10 @@ fn list_workflows_status(workflows: &[WorkflowModel]) -> String {
185185
.collect::<Vec<_>>()
186186
.join("\n")
187187
}
188+
189+
pub fn auto_build_started_comment(head_sha: &CommitSha, merge_sha: &CommitSha) -> Comment {
190+
Comment::new(format!(
191+
":hourglass: Testing commit {} with merge {}...",
192+
head_sha, merge_sha
193+
))
194+
}

src/bors/handlers/workflow.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,9 +237,13 @@ async fn try_complete_build(
237237
#[cfg(test)]
238238
mod tests {
239239
use crate::bors::handlers::trybuild::TRY_BRANCH_NAME;
240+
use crate::bors::merge_queue::{AUTO_BUILD_CHECK_RUN_NAME, AUTO_BRANCH_NAME};
240241
use crate::database::WorkflowStatus;
241242
use crate::database::operations::get_all_workflows;
242-
use crate::tests::mocks::{Branch, CheckSuite, Workflow, WorkflowEvent, run_test};
243+
use crate::tests::mocks::{
244+
BorsBuilder, Branch, CheckSuite, GitHubState, Workflow, WorkflowEvent, default_pr_number,
245+
run_test,
246+
};
243247

244248
#[sqlx::test]
245249
async fn workflow_started_unknown_build(pool: sqlx::PgPool) {

src/bors/merge_queue.rs

Lines changed: 253 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1+
use anyhow::anyhow;
2+
use octocrab::params::checks::{CheckRunOutput, CheckRunStatus};
13
use std::future::Future;
24
use std::sync::Arc;
35
use tokio::sync::mpsc;
46
use tracing::Instrument;
57

68
use crate::BorsContext;
79
use crate::bors::RepositoryState;
10+
use crate::bors::comment::auto_build_started_comment;
11+
use crate::database::{BuildStatus, PullRequestModel};
12+
use crate::github::api::client::GithubRepositoryClient;
13+
use crate::github::api::operations::ForcePush;
14+
use crate::github::{CommitSha, MergeError};
815
use crate::utils::sort_queue::sort_queue_prs;
916

17+
enum MergeResult {
18+
Success(CommitSha),
19+
Conflict,
20+
}
21+
1022
#[derive(Debug)]
1123
enum MergeQueueEvent {
1224
Trigger,
@@ -31,10 +43,17 @@ impl MergeQueueSender {
3143
}
3244
}
3345

46+
/// Branch used for performing merge operations.
47+
/// This branch should not run CI checks.
48+
pub(super) const AUTO_MERGE_BRANCH_NAME: &str = "automation/bors/auto-merge";
49+
3450
/// Branch where CI checks run for auto builds.
3551
/// This branch should run CI checks.
3652
pub(super) const AUTO_BRANCH_NAME: &str = "automation/bors/auto";
3753

54+
// The name of the check run seen in the GitHub UI.
55+
pub(super) const AUTO_BUILD_CHECK_RUN_NAME: &str = "Bors auto build";
56+
3857
pub async fn merge_queue_tick(ctx: Arc<BorsContext>) -> anyhow::Result<()> {
3958
let repos: Vec<Arc<RepositoryState>> =
4059
ctx.repositories.read().unwrap().values().cloned().collect();
@@ -61,14 +80,173 @@ pub async fn merge_queue_tick(ctx: Arc<BorsContext>) -> anyhow::Result<()> {
6180
// then pending builds (which block the queue to prevent starting simultaneous auto-builds).
6281
let prs = sort_queue_prs(prs);
6382

64-
for _ in prs {
65-
// Process PRs...
83+
for pr in prs {
84+
let pr_num = pr.number;
85+
86+
if let Some(auto_build) = &pr.auto_build {
87+
match auto_build.status {
88+
// Build successful - point the base branch to the merged commit.
89+
BuildStatus::Success => {
90+
// Break to give GitHub time to update the base branch.
91+
break;
92+
}
93+
// Build in progress - stop queue. We can only have one PR being built
94+
// at a time.
95+
BuildStatus::Pending => {
96+
tracing::info!("PR {pr_num} has a pending build - blocking queue");
97+
break;
98+
}
99+
BuildStatus::Failure | BuildStatus::Cancelled | BuildStatus::Timeouted => {
100+
unreachable!("Failed auto builds should be filtered out by SQL query");
101+
}
102+
}
103+
}
104+
105+
// No build exists for this PR - start a new auto build.
106+
match start_auto_build(&repo, &ctx, pr).await {
107+
Ok(true) => {
108+
tracing::info!("Starting auto build for PR {pr_num}");
109+
break;
110+
}
111+
Ok(false) => {
112+
tracing::debug!("Failed to start auto build for PR {pr_num}");
113+
continue;
114+
}
115+
Err(error) => {
116+
// Unexpected error - the PR will remain in the "queue" for a retry.
117+
tracing::error!("Error starting auto build for PR {pr_num}: {:?}", error);
118+
continue;
119+
}
120+
}
66121
}
67122
}
68123

124+
#[cfg(test)]
125+
crate::bors::WAIT_FOR_MERGE_QUEUE.mark();
126+
69127
Ok(())
70128
}
71129

130+
/// Starts a new auto build for a pull request.
131+
async fn start_auto_build(
132+
repo: &Arc<RepositoryState>,
133+
ctx: &Arc<BorsContext>,
134+
pr: PullRequestModel,
135+
) -> anyhow::Result<bool> {
136+
let client = &repo.client;
137+
138+
let gh_pr = client.get_pull_request(pr.number).await?;
139+
let base_sha = client.get_branch_sha(&pr.base_branch).await?;
140+
141+
let auto_merge_commit_message = format!(
142+
"Auto merge of #{} - {}, r={}\n\n{}\n\n{}",
143+
pr.number,
144+
gh_pr.head_label,
145+
pr.approver().unwrap_or("<unknown>"),
146+
pr.title,
147+
gh_pr.message
148+
);
149+
150+
// 1. Merge PR head with base branch on `AUTO_MERGE_BRANCH_NAME`
151+
match attempt_merge(
152+
&repo.client,
153+
&gh_pr.head.sha,
154+
&base_sha,
155+
&auto_merge_commit_message,
156+
)
157+
.await?
158+
{
159+
MergeResult::Success(merge_sha) => {
160+
// 2. Push merge commit to `AUTO_BRANCH_NAME` where CI runs
161+
client
162+
.set_branch_to_sha(AUTO_BRANCH_NAME, &merge_sha, ForcePush::Yes)
163+
.await?;
164+
165+
// 3. Record the build in the database
166+
let build_id = ctx
167+
.db
168+
.attach_auto_build(
169+
&pr,
170+
AUTO_BRANCH_NAME.to_string(),
171+
merge_sha.clone(),
172+
base_sha,
173+
)
174+
.await?;
175+
176+
// 4. Set GitHub check run to pending on PR head
177+
match client
178+
.create_check_run(
179+
AUTO_BUILD_CHECK_RUN_NAME,
180+
&gh_pr.head.sha,
181+
CheckRunStatus::InProgress,
182+
CheckRunOutput {
183+
title: AUTO_BUILD_CHECK_RUN_NAME.to_string(),
184+
summary: "".to_string(),
185+
text: None,
186+
annotations: vec![],
187+
images: vec![],
188+
},
189+
&build_id.to_string(),
190+
)
191+
.await
192+
{
193+
Ok(check_run) => {
194+
tracing::info!(
195+
"Created check run {} for build {build_id}",
196+
check_run.id.into_inner()
197+
);
198+
ctx.db
199+
.update_build_check_run_id(build_id, check_run.id.into_inner() as i64)
200+
.await?;
201+
}
202+
Err(error) => {
203+
// Check runs aren't critical, don't block progress if they fail
204+
tracing::error!("Cannot create check run: {error:?}");
205+
}
206+
}
207+
208+
// 5. Post status comment
209+
let comment = auto_build_started_comment(&gh_pr.head.sha, &merge_sha);
210+
client.post_comment(pr.number, comment).await?;
211+
212+
Ok(true)
213+
}
214+
MergeResult::Conflict => Ok(false),
215+
}
216+
}
217+
218+
/// Attempts to merge the given head SHA with base SHA via `AUTO_MERGE_BRANCH_NAME`.
219+
async fn attempt_merge(
220+
client: &GithubRepositoryClient,
221+
head_sha: &CommitSha,
222+
base_sha: &CommitSha,
223+
merge_message: &str,
224+
) -> anyhow::Result<MergeResult> {
225+
tracing::debug!("Attempting to merge with base SHA {base_sha}");
226+
227+
// Reset auto merge branch to point to base branch
228+
client
229+
.set_branch_to_sha(AUTO_MERGE_BRANCH_NAME, base_sha, ForcePush::Yes)
230+
.await
231+
.map_err(|error| anyhow!("Cannot set auto merge branch to {}: {error:?}", base_sha.0))?;
232+
233+
// then merge PR head commit into auto merge branch.
234+
match client
235+
.merge_branches(AUTO_MERGE_BRANCH_NAME, head_sha, merge_message)
236+
.await
237+
{
238+
Ok(merge_sha) => {
239+
tracing::debug!("Merge successful, SHA: {merge_sha}");
240+
Ok(MergeResult::Success(merge_sha))
241+
}
242+
Err(MergeError::Conflict) => {
243+
tracing::warn!("Merge conflict");
244+
Ok(MergeResult::Conflict)
245+
}
246+
Err(error) => Err(error.into()),
247+
}
248+
}
249+
72250
pub fn start_merge_queue(ctx: Arc<BorsContext>) -> (MergeQueueSender, impl Future<Output = ()>) {
73251
let (tx, mut rx) = mpsc::channel::<MergeQueueEvent>(10);
74252
let sender = MergeQueueSender { inner: tx };
@@ -103,3 +281,76 @@ pub fn start_merge_queue(ctx: Arc<BorsContext>) -> (MergeQueueSender, impl Futur
103281

104282
(sender, fut)
105283
}
284+
285+
#[cfg(test)]
286+
mod tests {
287+
288+
use octocrab::params::checks::CheckRunStatus;
289+
290+
use crate::{
291+
bors::merge_queue::{AUTO_BRANCH_NAME, AUTO_BUILD_CHECK_RUN_NAME},
292+
database::{WorkflowStatus, operations::get_all_workflows},
293+
tests::mocks::{WorkflowEvent, run_test},
294+
};
295+
296+
#[sqlx::test]
297+
async fn auto_workflow_started(pool: sqlx::PgPool) {
298+
run_test(pool.clone(), |mut tester| async {
299+
tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1);
300+
301+
tester.post_comment("@bors r+").await?;
302+
tester.expect_comments(1).await;
303+
304+
tester.process_merge_queue().await;
305+
tester.expect_comments(1).await;
306+
307+
tester
308+
.workflow_event(WorkflowEvent::started(tester.auto_branch()))
309+
.await?;
310+
Ok(tester)
311+
})
312+
.await;
313+
let suite = get_all_workflows(&pool).await.unwrap().pop().unwrap();
314+
assert_eq!(suite.status, WorkflowStatus::Pending);
315+
}
316+
317+
#[sqlx::test]
318+
async fn auto_workflow_check_run_created(pool: sqlx::PgPool) {
319+
run_test(pool, |mut tester| async {
320+
tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1);
321+
322+
tester.post_comment("@bors r+").await?;
323+
tester.expect_comments(1).await;
324+
325+
tester.process_merge_queue().await;
326+
tester.expect_comments(1).await;
327+
tester.expect_check_run(
328+
&tester.default_pr().await.get_gh_pr().head_sha,
329+
AUTO_BUILD_CHECK_RUN_NAME,
330+
AUTO_BUILD_CHECK_RUN_NAME,
331+
CheckRunStatus::InProgress,
332+
None,
333+
);
334+
Ok(tester)
335+
})
336+
.await;
337+
}
338+
339+
#[sqlx::test]
340+
async fn auto_workflow_started_comment(pool: sqlx::PgPool) {
341+
run_test(pool, |mut tester| async {
342+
tester.create_branch(AUTO_BRANCH_NAME).expect_suites(1);
343+
344+
tester.post_comment("@bors r+").await?;
345+
tester.expect_comments(1).await;
346+
347+
tester.process_merge_queue().await;
348+
insta::assert_snapshot!(
349+
tester.get_comment().await?,
350+
@":hourglass: Testing commit pr-1-sha with merge merge-main-sha1-pr-1-sha-0..."
351+
);
352+
Ok(tester)
353+
})
354+
.await;
355+
}
356+
}

src/bors/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ pub static WAIT_FOR_PR_STATUS_REFRESH: TestSyncMarker = TestSyncMarker::new();
3737
#[cfg(test)]
3838
pub static WAIT_FOR_WORKFLOW_STARTED: TestSyncMarker = TestSyncMarker::new();
3939

40+
#[cfg(test)]
41+
pub static WAIT_FOR_MERGE_QUEUE: TestSyncMarker = TestSyncMarker::new();
42+
4043
#[derive(Clone, Debug, PartialEq, Eq)]
4144
pub enum CheckSuiteStatus {
4245
Pending,

src/database/client.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@ use super::operations::{
1717
get_running_builds, get_workflow_urls_for_build, get_workflows_for_build,
1818
insert_repo_if_not_exists, set_pr_assignees, set_pr_priority, set_pr_rollup, set_pr_status,
1919
unapprove_pull_request, undelegate_pull_request, update_build_check_run_id,
20-
update_build_status, update_mergeable_states_by_base_branch, update_pr_mergeable_state,
21-
update_pr_try_build_id, update_workflow_status, upsert_pull_request, upsert_repository,
20+
update_build_status, update_mergeable_states_by_base_branch, update_pr_auto_build_id,
21+
update_pr_mergeable_state, update_pr_try_build_id, update_workflow_status, upsert_pull_request,
22+
upsert_repository,
2223
};
2324
use super::{ApprovalInfo, DelegatedPermission, MergeableState, RunId, UpsertPullRequestParams};
2425

@@ -191,6 +192,21 @@ impl PgDbClient {
191192
Ok(build_id)
192193
}
193194

195+
pub async fn attach_auto_build(
196+
&self,
197+
pr: &PullRequestModel,
198+
branch: String,
199+
commit_sha: CommitSha,
200+
parent: CommitSha,
201+
) -> anyhow::Result<i32> {
202+
let mut tx = self.pool.begin().await?;
203+
let build_id =
204+
create_build(&mut *tx, &pr.repository, &branch, &commit_sha, &parent).await?;
205+
update_pr_auto_build_id(&mut *tx, pr.id, build_id).await?;
206+
tx.commit().await?;
207+
Ok(build_id)
208+
}
209+
194210
pub async fn find_build(
195211
&self,
196212
repo: &GithubRepoName,

0 commit comments

Comments
 (0)