@@ -2,7 +2,9 @@ package github
22
33import (
44 "context"
5+ "crypto/sha256"
56 "encoding/base64"
7+ "encoding/hex"
68 "fmt"
79 "math/rand"
810 "net/http"
@@ -750,7 +752,117 @@ func (v *Provider) GetTemplate(commentType provider.CommentType) string {
750752 return provider .GetHTMLTemplate (commentType )
751753}
752754
753- func (v * Provider ) listCommentsByMarker (ctx context.Context , event * info.Event , marker string ) ([]* github.IssueComment , error ) {
755+ type commentTraceLogContext struct {
756+ dedupTrace string
757+ eventID string
758+ markerHash string
759+ markerLen int
760+ controllerLabel string
761+ }
762+
763+ func newDedupTraceID () string {
764+ //nolint:gosec // best-effort correlation ID for debug logs only
765+ return fmt .Sprintf ("%x-%04x" , time .Now ().UnixNano (), rand .Intn (1 << 16 ))
766+ }
767+
768+ func markerHash (marker string ) string {
769+ if marker == "" {
770+ return "none"
771+ }
772+ sum := sha256 .Sum256 ([]byte (marker ))
773+ digest := hex .EncodeToString (sum [:])
774+ if len (digest ) > 12 {
775+ return digest [:12 ]
776+ }
777+ return digest
778+ }
779+
780+ func formatCommentTime (ts github.Timestamp ) string {
781+ if ts .IsZero () {
782+ return "unknown"
783+ }
784+ return ts .UTC ().Format (time .RFC3339 )
785+ }
786+
787+ func compactCommentIDs (comments []* github.IssueComment ) []string {
788+ out := make ([]string , 0 , len (comments ))
789+ for _ , comment := range comments {
790+ out = append (out , fmt .Sprintf ("%d@%s" , comment .GetID (), formatCommentTime (comment .GetCreatedAt ())))
791+ }
792+ return out
793+ }
794+
795+ func responseStatusCode (resp * github.Response ) int {
796+ if resp == nil {
797+ return 0
798+ }
799+ return resp .StatusCode
800+ }
801+
802+ func eventID (event * info.Event ) string {
803+ if event == nil || event .Request == nil {
804+ return "unknown"
805+ }
806+ if id := event .Request .Header .Get ("X-GitHub-Delivery" ); id != "" {
807+ return id
808+ }
809+ return "unknown"
810+ }
811+
812+ func (v * Provider ) controllerLabel (ctx context.Context ) string {
813+ if name := info .GetCurrentControllerName (ctx ); name != "" {
814+ return name
815+ }
816+ if v .Run != nil && v .Run .Info .Controller != nil && v .Run .Info .Controller .Name != "" {
817+ return v .Run .Info .Controller .Name
818+ }
819+ return "unknown"
820+ }
821+
822+ func (v * Provider ) newCommentTraceLogContext (ctx context.Context , event * info.Event , marker string ) commentTraceLogContext {
823+ return commentTraceLogContext {
824+ dedupTrace : newDedupTraceID (),
825+ eventID : eventID (event ),
826+ markerHash : markerHash (marker ),
827+ markerLen : len (marker ),
828+ controllerLabel : v .controllerLabel (ctx ),
829+ }
830+ }
831+
832+ func (v * Provider ) debugCommentPhase (event * info.Event , trace commentTraceLogContext , phase string , kv ... any ) {
833+ if v .Logger == nil {
834+ return
835+ }
836+
837+ org := "unknown"
838+ repo := "unknown"
839+ pr := 0
840+ if event != nil {
841+ org = event .Organization
842+ repo = event .Repository
843+ pr = event .PullRequestNumber
844+ }
845+
846+ baseFields := []any {
847+ "phase" , phase ,
848+ "organization" , org ,
849+ "repository" , repo ,
850+ "pr" , pr ,
851+ "event_id" , trace .eventID ,
852+ "dedup_trace" , trace .dedupTrace ,
853+ "marker_hash" , trace .markerHash ,
854+ "marker_len" , trace .markerLen ,
855+ "controller_label" , trace .controllerLabel ,
856+ }
857+ v .Logger .Debugw ("github comment dedup flow" , append (baseFields , kv ... )... )
858+ }
859+
860+ func (v * Provider ) listCommentsByMarker (
861+ ctx context.Context ,
862+ event * info.Event ,
863+ marker , phase string ,
864+ trace commentTraceLogContext ,
865+ ) ([]* github.IssueComment , error ) {
754866 comments , _ , err := wrapAPI (v , "list_comments" , func () ([]* github.IssueComment , * github.Response , error ) {
755867 return v .Client ().Issues .ListComments (ctx , event .Organization , event .Repository , event .PullRequestNumber , & github.IssueListCommentsOptions {
756868 ListOptions : github.ListOptions {
@@ -770,6 +882,13 @@ func (v *Provider) listCommentsByMarker(ctx context.Context, event *info.Event,
770882 matchedComments = append (matchedComments , comment )
771883 }
772884 }
885+
886+ v .debugCommentPhase (event , trace , phase ,
887+ "fetched_count" , len (comments ),
888+ "matched_count" , len (matchedComments ),
889+ "matched_comments" , compactCommentIDs (matchedComments ),
890+ )
891+
773892 return matchedComments , nil
774893}
775894
@@ -778,11 +897,16 @@ func (v *Provider) ensureSingleMarkerComment(
778897 event * info.Event ,
779898 comments []* github.IssueComment ,
780899 commit string ,
900+ trace commentTraceLogContext ,
781901) error {
782902 if len (comments ) == 0 {
783903 return nil
784904 }
785905
906+ if len (comments ) > 1 {
907+ v .debugCommentPhase (event , trace , "duplicate_detected" , "matched_count" , len (comments ))
908+ }
909+
786910 primaryComment := comments [0 ]
787911 for _ , comment := range comments {
788912 if comment .GetBody () == commit {
@@ -791,6 +915,11 @@ func (v *Provider) ensureSingleMarkerComment(
791915 }
792916 }
793917
918+ v .debugCommentPhase (event , trace , "dedup_select_primary" ,
919+ "matched_count" , len (comments ),
920+ "primary_comment_id" , primaryComment .GetID (),
921+ )
922+
794923 if primaryComment .GetBody () != commit {
795924 if _ , _ , err := wrapAPI (v , "edit_comment" , func () (* github.IssueComment , * github.Response , error ) {
796925 return v .Client ().Issues .EditComment (ctx , event .Organization , event .Repository , primaryComment .GetID (), & github.IssueComment {
@@ -806,14 +935,23 @@ func (v *Provider) ensureSingleMarkerComment(
806935 if comment .GetID () == primaryComment .GetID () {
807936 continue
808937 }
809- if _ , _ , err := wrapAPI (v , "delete_comment" , func () (struct {}, * github.Response , error ) {
938+
939+ v .debugCommentPhase (event , trace , "dedup_delete_attempt" , "delete_comment_id" , comment .GetID ())
940+ _ , resp , err := wrapAPI (v , "delete_comment" , func () (struct {}, * github.Response , error ) {
810941 resp , err := v .Client ().Issues .DeleteComment (ctx , event .Organization , event .Repository , comment .GetID ())
811942 return struct {}{}, resp , err
812- }); err != nil && v .Logger != nil {
943+ })
944+ v .debugCommentPhase (event , trace , "dedup_delete_done" ,
945+ "delete_comment_id" , comment .GetID (),
946+ "status_code" , responseStatusCode (resp ),
947+ "delete_error" , err != nil ,
948+ )
949+ if err != nil && v .Logger != nil {
813950 v .Logger .Warnf ("failed to delete duplicate comment %d on %s/%s#%d: %v" ,
814951 comment .GetID (), event .Organization , event .Repository , event .PullRequestNumber , err )
815952 }
816953 }
954+ v .debugCommentPhase (event , trace , "dedup_complete" , "final_expected_count" , 1 )
817955 return nil
818956}
819957
@@ -827,18 +965,25 @@ func (v *Provider) CreateComment(ctx context.Context, event *info.Event, commit,
827965 return fmt .Errorf ("create comment only works on pull requests" )
828966 }
829967
968+ trace := v .newCommentTraceLogContext (ctx , event , updateMarker )
969+
830970 if updateMarker != "" {
831- existingComments , err := v .listCommentsByMarker (ctx , event , updateMarker )
971+ existingComments , err := v .listCommentsByMarker (ctx , event , updateMarker , "initial_list" , trace )
832972 if err != nil {
833973 return err
834974 }
835975
976+ if len (existingComments ) > 1 {
977+ v .debugCommentPhase (event , trace , "duplicate_detected" , "matched_count" , len (existingComments ))
978+ }
979+
836980 if len (existingComments ) > 0 {
837- return v .ensureSingleMarkerComment (ctx , event , existingComments , commit )
981+ return v .ensureSingleMarkerComment (ctx , event , existingComments , commit , trace )
838982 }
839983
840984 //nolint:gosec // No need for crypto/rand here, just reducing timing window
841985 jitter := time .Duration (rand .Intn (500 )) * time .Millisecond
986+ v .debugCommentPhase (event , trace , "jitter_wait" , "jitter_ms" , jitter .Milliseconds ())
842987 timer := time .NewTimer (jitter )
843988 defer timer .Stop ()
844989
@@ -849,33 +994,50 @@ func (v *Provider) CreateComment(ctx context.Context, event *info.Event, commit,
849994 }
850995
851996 // Re-check after jitter in case another processor already created the marker comment.
852- existingComments , err = v .listCommentsByMarker (ctx , event , updateMarker )
997+ existingComments , err = v .listCommentsByMarker (ctx , event , updateMarker , "post_jitter_list" , trace )
853998 if err != nil {
854999 return err
8551000 }
1001+ if len (existingComments ) > 1 {
1002+ v .debugCommentPhase (event , trace , "duplicate_detected" , "matched_count" , len (existingComments ))
1003+ }
8561004 if len (existingComments ) > 0 {
857- return v .ensureSingleMarkerComment (ctx , event , existingComments , commit )
1005+ return v .ensureSingleMarkerComment (ctx , event , existingComments , commit , trace )
8581006 }
1007+
1008+ v .debugCommentPhase (event , trace , "pre_create_race_window" , "matched_count" , len (existingComments ))
8591009 }
8601010
861- _ , _ , err := wrapAPI (v , "create_comment" , func () (* github.IssueComment , * github.Response , error ) {
1011+ v .debugCommentPhase (event , trace , "create_comment_start" )
1012+ createdComment , createResp , err := wrapAPI (v , "create_comment" , func () (* github.IssueComment , * github.Response , error ) {
8621013 return v .Client ().Issues .CreateComment (ctx , event .Organization , event .Repository , event .PullRequestNumber , & github.IssueComment {
8631014 Body : github .Ptr (commit ),
8641015 })
8651016 })
8661017 if err != nil {
1018+ v .debugCommentPhase (event , trace , "create_comment_done" ,
1019+ "status_code" , responseStatusCode (createResp ),
1020+ "create_error" , err .Error (),
1021+ )
8671022 return err
8681023 }
1024+ v .debugCommentPhase (event , trace , "create_comment_done" ,
1025+ "status_code" , responseStatusCode (createResp ),
1026+ "created_comment_id" , createdComment .GetID (),
1027+ )
8691028
8701029 if updateMarker == "" {
8711030 return nil
8721031 }
8731032
8741033 // Best-effort post-create reconciliation to collapse duplicates created by
8751034 // concurrent processors handling the same event.
876- matchedComments , listErr := v .listCommentsByMarker (ctx , event , updateMarker )
1035+ matchedComments , listErr := v .listCommentsByMarker (ctx , event , updateMarker , "post_create_list" , trace )
8771036 if listErr != nil {
8781037 return nil
8791038 }
880- return v .ensureSingleMarkerComment (ctx , event , matchedComments , commit )
1039+ if len (matchedComments ) > 1 {
1040+ v .debugCommentPhase (event , trace , "duplicate_detected" , "matched_count" , len (matchedComments ))
1041+ }
1042+ return v .ensureSingleMarkerComment (ctx , event , matchedComments , commit , trace )
8811043}
0 commit comments