1
+ use anyhow:: anyhow;
2
+ use octocrab:: params:: checks:: { CheckRunOutput , CheckRunStatus } ;
1
3
use std:: future:: Future ;
2
4
use std:: sync:: Arc ;
3
5
use tokio:: sync:: mpsc;
4
6
use tracing:: Instrument ;
5
7
6
8
use crate :: BorsContext ;
7
9
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 } ;
8
15
use crate :: utils:: sort_queue:: sort_queue_prs;
9
16
17
+ enum MergeResult {
18
+ Success ( CommitSha ) ,
19
+ Conflict ,
20
+ }
21
+
10
22
#[ derive( Debug ) ]
11
23
enum MergeQueueEvent {
12
24
Trigger ,
@@ -31,10 +43,17 @@ impl MergeQueueSender {
31
43
}
32
44
}
33
45
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
+
34
50
/// Branch where CI checks run for auto builds.
35
51
/// This branch should run CI checks.
36
52
pub ( super ) const AUTO_BRANCH_NAME : & str = "automation/bors/auto" ;
37
53
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
+
38
57
pub async fn merge_queue_tick ( ctx : Arc < BorsContext > ) -> anyhow:: Result < ( ) > {
39
58
let repos: Vec < Arc < RepositoryState > > =
40
59
ctx. repositories . read ( ) . unwrap ( ) . values ( ) . cloned ( ) . collect ( ) ;
@@ -61,14 +80,173 @@ pub async fn merge_queue_tick(ctx: Arc<BorsContext>) -> anyhow::Result<()> {
61
80
// then pending builds (which block the queue to prevent starting simultaneous auto-builds).
62
81
let prs = sort_queue_prs ( prs) ;
63
82
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
+ }
66
121
}
67
122
}
68
123
124
+ #[ cfg( test) ]
125
+ crate :: bors:: WAIT_FOR_MERGE_QUEUE . mark ( ) ;
126
+
69
127
Ok ( ( ) )
70
128
}
71
129
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
+
72
250
pub fn start_merge_queue ( ctx : Arc < BorsContext > ) -> ( MergeQueueSender , impl Future < Output = ( ) > ) {
73
251
let ( tx, mut rx) = mpsc:: channel :: < MergeQueueEvent > ( 10 ) ;
74
252
let sender = MergeQueueSender { inner : tx } ;
@@ -103,3 +281,76 @@ pub fn start_merge_queue(ctx: Arc<BorsContext>) -> (MergeQueueSender, impl Futur
103
281
104
282
( sender, fut)
105
283
}
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
+ }
0 commit comments