Skip to content

Commit 6fdb0e2

Browse files
committed
chore: add race-trace logs for PR comments
sometimes two controller workers can post the same PR comment at nearly the same time. This adds breadcrumbs so we can see who did what and when, and quickly debug duplicate-comment races. Adds a per-call dedup_trace ID and debug-phase logs across initial list, jitter wait, post-jitter list, create start/done, post-create list, primary selection, delete attempts/completions, and dedup completion. Improves marker observability by logging fetched vs matched counts, compact matched comment id/timestamp data, marker hash/length (no full marker body), plus controller_label with explicit unknown fallback. Adds validation-path correlation debug logging before CreateComment in reportValidationErrors with validation count and event/repo/branch context. Enhances TestGithubSecondFlakyPullRequestBadYaml diagnostics: per-comment compact metadata, bounded recheck loop with attempt logs, and actionable final assertion details. Adds unit coverage in TestCreateCommentDedupLogging to verify phase logs and correlation fields for no/one/multiple existing marker scenarios. Signed-off-by: Chmouel Boudjnah <chmouel@redhat.com>
1 parent 31f0397 commit 6fdb0e2

4 files changed

Lines changed: 417 additions & 11 deletions

File tree

pkg/pipelineascode/errors.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,28 @@ func (p *PacRun) reportValidationErrors(ctx context.Context, repo *v1alpha1.Repo
6363
}
6464
markdownErrMessage := fmt.Sprintf(`%s
6565
%s`, provider.ValidationErrorTemplate, strings.Join(errorRows, "\n"))
66+
67+
eventID := "unknown"
68+
org := "unknown"
69+
repository := "unknown"
70+
sourceBranch := "unknown"
71+
targetBranch := "unknown"
72+
pr := 0
73+
if p.event != nil {
74+
org = p.event.Organization
75+
repository = p.event.Repository
76+
sourceBranch = p.event.HeadBranch
77+
targetBranch = p.event.BaseBranch
78+
pr = p.event.PullRequestNumber
79+
if p.event.Request != nil {
80+
if id := p.event.Request.Header.Get("X-GitHub-Delivery"); id != "" {
81+
eventID = id
82+
}
83+
}
84+
}
85+
p.debugf("reportValidationErrors: create_comment validation_error_count=%d event_id=%s pr=%d repo=%s/%s namespace=%s source_branch=%s target_branch=%s",
86+
len(errorRows), eventID, pr, org, repository, repo.GetNamespace(), sourceBranch, targetBranch)
87+
6688
if err := p.vcx.CreateComment(ctx, p.event, markdownErrMessage, provider.ValidationErrorTemplate); err != nil {
6789
p.eventEmitter.EmitMessage(repo, zap.ErrorLevel, "PipelineRunCommentCreationError",
6890
fmt.Sprintf("failed to create comment: %s", err.Error()))

pkg/provider/github/github.go

Lines changed: 172 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package github
22

33
import (
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

Comments
 (0)