@@ -90,13 +90,18 @@ Please choose another assignee.";
90
90
// Special account that we use to prevent assignment.
91
91
const GHOST_ACCOUNT : & str = "ghost" ;
92
92
93
+ /// Assignment data stored in the issue/PR body.
93
94
#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
94
95
struct AssignData {
95
96
user : Option < String > ,
96
97
}
97
98
98
- /// Input for auto-assignment when a PR is created.
99
- pub ( super ) struct AssignInput { }
99
+ /// Input for auto-assignment when a PR is created or converted from draft.
100
+ #[ derive( Debug ) ]
101
+ pub ( super ) enum AssignInput {
102
+ Opened { draft : bool } ,
103
+ ReadyForReview ,
104
+ }
100
105
101
106
/// Prepares the input when a new PR is opened.
102
107
pub ( super ) async fn parse_input (
@@ -108,13 +113,17 @@ pub(super) async fn parse_input(
108
113
Some ( config) => config,
109
114
None => return Ok ( None ) ,
110
115
} ;
111
- if config. owners . is_empty ( )
112
- || !matches ! ( event. action, IssuesAction :: Opened )
113
- || !event. issue . is_pr ( )
114
- {
116
+ if config. owners . is_empty ( ) || !event. issue . is_pr ( ) {
115
117
return Ok ( None ) ;
116
118
}
117
- Ok ( Some ( AssignInput { } ) )
119
+
120
+ match event. action {
121
+ IssuesAction :: Opened => Ok ( Some ( AssignInput :: Opened {
122
+ draft : event. issue . draft ,
123
+ } ) ) ,
124
+ IssuesAction :: ReadyForReview => Ok ( Some ( AssignInput :: ReadyForReview ) ) ,
125
+ _ => Ok ( None ) ,
126
+ }
118
127
}
119
128
120
129
/// Handles the work of setting an assignment for a new PR and posting a
@@ -123,8 +132,31 @@ pub(super) async fn handle_input(
123
132
ctx : & Context ,
124
133
config : & AssignConfig ,
125
134
event : & IssuesEvent ,
126
- _input : AssignInput ,
135
+ input : AssignInput ,
127
136
) -> anyhow:: Result < ( ) > {
137
+ let assign_command = find_assign_command ( ctx, event) ;
138
+
139
+ // Perform assignment when:
140
+ // - PR was opened normally
141
+ // - PR was opened as a draft with an explicit r? (but not r? ghost)
142
+ // - PR was converted from a draft and there are no current assignees
143
+ let should_assign = match input {
144
+ AssignInput :: Opened { draft : false } => true ,
145
+ AssignInput :: Opened { draft : true } => {
146
+ // Even if the PR is opened as a draft, we still want to perform assignment if r?
147
+ // was used. However, historically, `r? ghost` was supposed to mean "do not
148
+ // perform assignment". So in that case, we skip the assignment and only perform it once
149
+ // the PR has been marked as being ready for review.
150
+ assign_command. as_ref ( ) . is_some_and ( |a| a != GHOST_ACCOUNT )
151
+ }
152
+ AssignInput :: ReadyForReview => event. issue . assignees . is_empty ( ) ,
153
+ } ;
154
+
155
+ if !should_assign {
156
+ log:: info!( "Skipping PR assignment, input: {input:?}, assign_command: {assign_command:?}" ) ;
157
+ return Ok ( ( ) ) ;
158
+ }
159
+
128
160
let Some ( diff) = event. issue . diff ( & ctx. github ) . await ? else {
129
161
bail ! (
130
162
"expected issue {} to be a PR, but the diff could not be determined" ,
@@ -134,7 +166,8 @@ pub(super) async fn handle_input(
134
166
135
167
// Don't auto-assign or welcome if the user manually set the assignee when opening.
136
168
if event. issue . assignees . is_empty ( ) {
137
- let ( assignee, from_comment) = determine_assignee ( ctx, event, config, & diff) . await ?;
169
+ let ( assignee, from_comment) =
170
+ determine_assignee ( ctx, assign_command, event, config, & diff) . await ?;
138
171
if assignee. as_deref ( ) == Some ( GHOST_ACCOUNT ) {
139
172
// "ghost" is GitHub's placeholder account for deleted accounts.
140
173
// It is used here as a convenient way to prevent assignment. This
@@ -257,13 +290,14 @@ async fn set_assignee(issue: &Issue, github: &GithubClient, username: &str) {
257
290
/// determined from the diff).
258
291
async fn determine_assignee (
259
292
ctx : & Context ,
293
+ assign_command : Option < String > ,
260
294
event : & IssuesEvent ,
261
295
config : & AssignConfig ,
262
296
diff : & [ FileDiff ] ,
263
297
) -> anyhow:: Result < ( Option < String > , bool ) > {
264
298
let db_client = ctx. db . get ( ) . await ;
265
299
let teams = crate :: team_data:: teams ( & ctx. github ) . await ?;
266
- if let Some ( name) = find_assign_command ( ctx , event ) {
300
+ if let Some ( name) = assign_command {
267
301
// User included `r?` in the opening PR body.
268
302
match find_reviewer_from_names ( & db_client, & teams, config, & event. issue , & [ name] ) . await {
269
303
Ok ( assignee) => return Ok ( ( Some ( assignee) , true ) ) ,
0 commit comments