@@ -90,13 +90,18 @@ Please choose another assignee.";
9090// Special account that we use to prevent assignment.
9191const GHOST_ACCOUNT : & str = "ghost" ;
9292
93+ /// Assignment data stored in the issue/PR body.
9394#[ derive( Debug , PartialEq , Eq , serde:: Serialize , serde:: Deserialize ) ]
9495struct AssignData {
9596 user : Option < String > ,
9697}
9798
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+ }
100105
101106/// Prepares the input when a new PR is opened.
102107pub ( super ) async fn parse_input (
@@ -108,13 +113,17 @@ pub(super) async fn parse_input(
108113 Some ( config) => config,
109114 None => return Ok ( None ) ,
110115 } ;
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 ( ) {
115117 return Ok ( None ) ;
116118 }
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+ }
118127}
119128
120129/// Handles the work of setting an assignment for a new PR and posting a
@@ -123,8 +132,31 @@ pub(super) async fn handle_input(
123132 ctx : & Context ,
124133 config : & AssignConfig ,
125134 event : & IssuesEvent ,
126- _input : AssignInput ,
135+ input : AssignInput ,
127136) -> 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+
128160 let Some ( diff) = event. issue . diff ( & ctx. github ) . await ? else {
129161 bail ! (
130162 "expected issue {} to be a PR, but the diff could not be determined" ,
@@ -134,7 +166,8 @@ pub(super) async fn handle_input(
134166
135167 // Don't auto-assign or welcome if the user manually set the assignee when opening.
136168 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 ?;
138171 if assignee. as_deref ( ) == Some ( GHOST_ACCOUNT ) {
139172 // "ghost" is GitHub's placeholder account for deleted accounts.
140173 // 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) {
257290/// determined from the diff).
258291async fn determine_assignee (
259292 ctx : & Context ,
293+ assign_command : Option < String > ,
260294 event : & IssuesEvent ,
261295 config : & AssignConfig ,
262296 diff : & [ FileDiff ] ,
263297) -> anyhow:: Result < ( Option < String > , bool ) > {
264298 let db_client = ctx. db . get ( ) . await ;
265299 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 {
267301 // User included `r?` in the opening PR body.
268302 match find_reviewer_from_names ( & db_client, & teams, config, & event. issue , & [ name] ) . await {
269303 Ok ( assignee) => return Ok ( ( Some ( assignee) , true ) ) ,
0 commit comments