@@ -427,20 +427,20 @@ func readTurnsFile(ctx workflow.Context, url string) (*PRTurns, error) {
427427 f , err := os .Open (path ) //gosec:disable G304 // URL received from signature-verified 3rd-party, suffix is hardcoded.
428428 if err != nil {
429429 if errors .Is (err , os .ErrNotExist ) {
430- return resetTurns (ctx , url )
430+ return resetTurns (ctx , url , nil )
431431 }
432432 return nil , err
433433 }
434434 defer f .Close ()
435435
436436 t := new (PRTurns )
437437 if err := json .NewDecoder (f ).Decode (& t ); err != nil {
438- return resetTurns (ctx , url )
438+ return resetTurns (ctx , url , nil )
439439 }
440440
441441 // Data sanity checks.
442442 if t .Author == "" {
443- return resetTurns (ctx , url )
443+ return resetTurns (ctx , url , t )
444444 }
445445 normalizeEmailAddresses (t )
446446 if t .Reviewers == nil {
@@ -513,51 +513,67 @@ func writeTurnsFileActivity(_ context.Context, url string, t *PRTurns) error {
513513
514514// resetTurns recreates the attention state file for a specific PR, based on the current
515515// PR snapshot. This is a fallback for when the turn file is missing or corrupted.
516- func resetTurns (ctx workflow.Context , url string ) (* PRTurns , error ) {
516+ func resetTurns (ctx workflow.Context , url string , t * PRTurns ) (* PRTurns , error ) {
517517 logger .From (ctx ).Warn ("resetting PR attention state file" , slog .String ("pr_url" , url ))
518518
519519 pr , err := LoadPRSnapshot (ctx , url )
520520 if err != nil {
521521 return nil , err
522522 }
523523
524- author := userEmail (ctx , pr ["author" ])
524+ author , accountType := userEmailAndType (ctx , pr ["author" ])
525+ if author == "" && accountType == "app_user" {
526+ author = "bot"
527+ }
528+
529+ // If the only thing that needs fixing is the author's email, do just that.
530+ if t != nil && len (t .Reviewers )+ len (t .Activity )+ len (t .Approvers ) > 0 {
531+ t .Author = author
532+ return t , writeTurnsFile (ctx , url , t )
533+ }
525534
535+ // Otherwise, recreate the entire struct and file.
526536 reviewers := map [string ]bool {}
527537 jsonList , ok := pr ["reviewers" ].([]any )
528538 if ! ok {
529539 jsonList = []any {}
530540 }
531541 for _ , r := range jsonList {
532- if email := userEmail (ctx , r ); email != "" {
542+ if email , _ := userEmailAndType (ctx , r ); email != "" {
533543 reviewers [email ] = true
534544 }
535545 }
536546
537- t := & PRTurns {Author : author , Reviewers : reviewers , Activity : map [string ]time.Time {}, Approvers : map [string ]time.Time {}}
547+ approvers := map [string ]time.Time {}
548+
549+ t = & PRTurns {Author : author , Reviewers : reviewers , Activity : map [string ]time.Time {}, Approvers : approvers }
538550 return t , writeTurnsFile (ctx , url , t )
539551}
540552
541- // userEmail extracts the Bitbucket account ID from user details map, and converts
542- // it into the user's email address, based on RevChat's own user database.
543- func userEmail (ctx workflow.Context , detailsMap any ) string {
553+ // userEmailAndType extracts the Bitbucket account ID from user details map, and converts
554+ // it into the user's email address and account type , based on RevChat's own user database.
555+ func userEmailAndType (ctx workflow.Context , detailsMap any ) ( email , accountType string ) {
544556 user , ok := detailsMap .(map [string ]any )
545557 if ! ok {
546- return ""
558+ return "" , ""
547559 }
548560
549561 accountID , ok := user ["account_id" ].(string )
550562 if ! ok {
551- return ""
563+ return "" , ""
564+ }
565+ accountType , ok = user ["type" ].(string )
566+ if ! ok {
567+ accountType = ""
552568 }
553569
554- return BitbucketIDToEmail (ctx , accountID )
570+ return BitbucketIDToEmail (ctx , accountID , accountType ), accountType
555571}
556572
557573// BitbucketIDToEmail converts a Bitbucket account ID into an email address. This function returns an empty
558574// string if the account ID is not found. It uses persistent data storage, or API calls as a fallback.
559575// This function is also wrapped in the "users" package, and reused by other packages from there.
560- func BitbucketIDToEmail (ctx workflow.Context , accountID string ) string {
576+ func BitbucketIDToEmail (ctx workflow.Context , accountID , accountType string ) string {
561577 if accountID == "" {
562578 return ""
563579 }
@@ -566,6 +582,10 @@ func BitbucketIDToEmail(ctx workflow.Context, accountID string) string {
566582 return user .Email
567583 }
568584
585+ if accountType == "app_user" {
586+ return "" // Not a real user, so no point to check in Jira.
587+ }
588+
569589 // We use the Jira API as a fallback because the Bitbucket API doesn't expose email addresses.
570590 jiraUser , err := jira .UsersGet (ctx , accountID )
571591 if err != nil {
0 commit comments