Repository: reviewdog/reviewdog
Pull request #1792 (2024-06-17 17:01:56)

Introduce comment metadata and fingerprint for identifying existing posted comments
Introduce metadata comment and fingerprint to github-pr-review reporter

Now reviwedog include metadata as HTML comment for github-pr-review
reporter.

We can now use calculated fingerprint to identify existing posted comments.

It resolves the problem w.r.t. related location feature.
Since reviewdog include the latest SHA in body of comment for relate
location feature, comparing comment body cannot be used for identifying
existing comments and it leads to post duplicate comments.
We can avoid this problem with fingerprint check.

- [x] Updated Unreleased section in [CHANGELOG](https://github.com/reviewdog/reviewdog/blob/master/CHANGELOG.md) or it's not notable changes.




Patch:
diff --git a/.github/workflows/rdformat.yml b/.github/workflows/rdformat.yml
index 2e174b2a..7214fd91 100644
--- a/.github/workflows/rdformat.yml
+++ b/.github/workflows/rdformat.yml
@@ -1,4 +1,4 @@
-name: RDFormat
+name: Proto
 on: [push,pull_request]
 jobs:
   build:
@@ -7,7 +7,7 @@ jobs:
     steps:
     - uses: actions/checkout@v4
     - name: Build
-      working-directory: proto/rdf/
+      working-directory: proto/
       run: ./update.sh
     - name: Check diff
       run: git diff --exit-code
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1431cfe..83d32912 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - ...
 
 ### :bug: Fixes
-- ...
+- [#1792](https://github.com/reviewdog/reviewdog/pull/1792) Introduce `__reviewdog__` HTML comment metadata and fingerprint for identifying existing posted comments for `github-pr-review` reporter. This resolve duplicated comments issue with related location feature.
 
 ### :rotating_light: Breaking changes
 - ...
diff --git a/proto/rdf/Dockerfile b/proto/Dockerfile
similarity index 100%
rename from proto/rdf/Dockerfile
rename to proto/Dockerfile
diff --git a/proto/entrypoint.sh b/proto/entrypoint.sh
new file mode 100755
index 00000000..a48ec457
--- /dev/null
+++ b/proto/entrypoint.sh
@@ -0,0 +1,3 @@
+#!/bin/bash
+protoc --proto_path=./rdf --go_out=./rdf --go_opt=paths=source_relative --jsonschema_out=./rdf/jsonschema ./rdf/reviewdog.proto
+protoc --proto_path=./metacomment --go_out=./metacomment --go_opt=paths=source_relative ./metacomment/metacomment.proto
diff --git a/proto/metacomment/metacomment.pb.go b/proto/metacomment/metacomment.pb.go
new file mode 100644
index 00000000..2fb9b8f6
--- /dev/null
+++ b/proto/metacomment/metacomment.pb.go
@@ -0,0 +1,166 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.34.2
+// 	protoc        v5.27.1
+// source: metacomment.proto
+
+package metacomment
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// Represents a metadata of a diagnostic result.
+// It's expected to be base64 encoded and included into reporter comments such
+// as GitHub Pull Request Review comment.
+//
+// This metadata allow reviewdog to identify the same existing comment and
+// avoid posting duplicated comments. It can also be used for resolving or
+// deleting existing comments.
+type MetaComment struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// An unique identity, or "fingerprint", of the diagnostic result.
+	Fingerprint string `protobuf:"bytes,1,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"`
+	// Source (tool) name of the diagnostic result.
+	// It's important to have source name so that reviewdog can handle existing
+	// comments with the same source properly.
+	SourceName string `protobuf:"bytes,2,opt,name=source_name,json=sourceName,proto3" json:"source_name,omitempty"`
+}
+
+func (x *MetaComment) Reset() {
+	*x = MetaComment{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_metacomment_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MetaComment) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MetaComment) ProtoMessage() {}
+
+func (x *MetaComment) ProtoReflect() protoreflect.Message {
+	mi := &file_metacomment_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MetaComment.ProtoReflect.Descriptor instead.
+func (*MetaComment) Descriptor() ([]byte, []int) {
+	return file_metacomment_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *MetaComment) GetFingerprint() string {
+	if x != nil {
+		return x.Fingerprint
+	}
+	return ""
+}
+
+func (x *MetaComment) GetSourceName() string {
+	if x != nil {
+		return x.SourceName
+	}
+	return ""
+}
+
+var File_metacomment_proto protoreflect.FileDescriptor
+
+var file_metacomment_proto_rawDesc = []byte{
+	0x0a, 0x11, 0x6d, 0x65, 0x74, 0x61, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x64, 0x6f, 0x67, 0x2e, 0x72,
+	0x64, 0x66, 0x22, 0x50, 0x0a, 0x0b, 0x4d, 0x65, 0x74, 0x61, 0x43, 0x6f, 0x6d, 0x6d, 0x65, 0x6e,
+	0x74, 0x12, 0x20, 0x0a, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72,
+	0x69, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e, 0x61,
+	0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+	0x4e, 0x61, 0x6d, 0x65, 0x42, 0x32, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
+	0x6f, 0x6d, 0x2f, 0x72, 0x65, 0x76, 0x69, 0x65, 0x77, 0x64, 0x6f, 0x67, 0x2f, 0x72, 0x65, 0x76,
+	0x69, 0x65, 0x77, 0x64, 0x6f, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6d, 0x65, 0x74,
+	0x61, 0x63, 0x6f, 0x6d, 0x6d, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_metacomment_proto_rawDescOnce sync.Once
+	file_metacomment_proto_rawDescData = file_metacomment_proto_rawDesc
+)
+
+func file_metacomment_proto_rawDescGZIP() []byte {
+	file_metacomment_proto_rawDescOnce.Do(func() {
+		file_metacomment_proto_rawDescData = protoimpl.X.CompressGZIP(file_metacomment_proto_rawDescData)
+	})
+	return file_metacomment_proto_rawDescData
+}
+
+var file_metacomment_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_metacomment_proto_goTypes = []any{
+	(*MetaComment)(nil), // 0: reviewdog.rdf.MetaComment
+}
+var file_metacomment_proto_depIdxs = []int32{
+	0, // [0:0] is the sub-list for method output_type
+	0, // [0:0] is the sub-list for method input_type
+	0, // [0:0] is the sub-list for extension type_name
+	0, // [0:0] is the sub-list for extension extendee
+	0, // [0:0] is the sub-list for field type_name
+}
+
+func init() { file_metacomment_proto_init() }
+func file_metacomment_proto_init() {
+	if File_metacomment_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_metacomment_proto_msgTypes[0].Exporter = func(v any, i int) any {
+			switch v := v.(*MetaComment); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_metacomment_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_metacomment_proto_goTypes,
+		DependencyIndexes: file_metacomment_proto_depIdxs,
+		MessageInfos:      file_metacomment_proto_msgTypes,
+	}.Build()
+	File_metacomment_proto = out.File
+	file_metacomment_proto_rawDesc = nil
+	file_metacomment_proto_goTypes = nil
+	file_metacomment_proto_depIdxs = nil
+}
diff --git a/proto/metacomment/metacomment.proto b/proto/metacomment/metacomment.proto
new file mode 100644
index 00000000..26ac2201
--- /dev/null
+++ b/proto/metacomment/metacomment.proto
@@ -0,0 +1,21 @@
+syntax = "proto3";
+package reviewdog.rdf;
+
+option go_package = "github.com/reviewdog/reviewdog/proto/metacomment";
+
+// Represents a metadata of a diagnostic result.
+// It's expected to be base64 encoded and included into reporter comments such
+// as GitHub Pull Request Review comment.
+//
+// This metadata allow reviewdog to identify the same existing comment and
+// avoid posting duplicated comments. It can also be used for resolving or
+// deleting existing comments.
+message MetaComment {
+  // An unique identity, or "fingerprint", of the diagnostic result.
+  string fingerprint = 1;
+
+  // Source (tool) name of the diagnostic result.
+  // It's important to have source name so that reviewdog can handle existing
+  // comments with the same source properly.
+  string source_name = 2;
+}
diff --git a/proto/rdf/entrypoint.sh b/proto/rdf/entrypoint.sh
deleted file mode 100755
index 9c5718ec..00000000
--- a/proto/rdf/entrypoint.sh
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/bash
-protoc --proto_path=. --go_out=. --go_opt=paths=source_relative --jsonschema_out=./jsonschema ./reviewdog.proto
diff --git a/proto/rdf/update.sh b/proto/update.sh
similarity index 100%
rename from proto/rdf/update.sh
rename to proto/update.sh
diff --git a/scripts/decode-metacomment/main.go b/scripts/decode-metacomment/main.go
new file mode 100644
index 00000000..f75edb69
--- /dev/null
+++ b/scripts/decode-metacomment/main.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+
+	"github.com/reviewdog/reviewdog/service/github"
+)
+
+func main() {
+	if len(os.Args) == 1 {
+		log.Fatal("require one argument")
+	}
+	meta, err := github.DecodeMetaComment(os.Args[1])
+	if err != nil {
+		log.Fatalf("failed to decode meta comment: %v", err)
+	}
+	fmt.Printf("%v\n", meta)
+}
diff --git a/service/commentutil/commentutil.go b/service/commentutil/commentutil.go
index 0393c4a2..d2db28bb 100644
--- a/service/commentutil/commentutil.go
+++ b/service/commentutil/commentutil.go
@@ -9,13 +9,13 @@ import (
 	"github.com/reviewdog/reviewdog/proto/rdf"
 )
 
-// `path` to `position`(Lnum for new file) to comment `body`s
+// `path` to `position`(Lnum for new file) to comment `body` or `finterprint`
 type PostedComments map[string]map[int][]string
 
 // IsPosted returns true if a given comment has been posted in code review service already,
 // otherwise returns false. It sees comments with same path, same position,
 // and same body as same comments.
-func (p PostedComments) IsPosted(c *reviewdog.Comment, lineNum int, body string) bool {
+func (p PostedComments) IsPosted(c *reviewdog.Comment, lineNum int, bodyOrFingerprint string) bool {
 	path := c.Result.Diagnostic.GetLocation().GetPath()
 	if _, ok := p[path]; !ok {
 		return false
@@ -25,7 +25,7 @@ func (p PostedComments) IsPosted(c *reviewdog.Comment, lineNum int, body string)
 		return false
 	}
 	for _, b := range bodies {
-		if b == body {
+		if b == bodyOrFingerprint {
 			return true
 		}
 	}
@@ -33,14 +33,14 @@ func (p PostedComments) IsPosted(c *reviewdog.Comment, lineNum int, body string)
 }
 
 // AddPostedComment adds a posted comment.
-func (p PostedComments) AddPostedComment(path string, lineNum int, body string) {
+func (p PostedComments) AddPostedComment(path string, lineNum int, bodyOrFingerprint string) {
 	if _, ok := p[path]; !ok {
 		p[path] = make(map[int][]string)
 	}
 	if _, ok := p[path][lineNum]; !ok {
 		p[path][lineNum] = make([]string, 0)
 	}
-	p[path][lineNum] = append(p[path][lineNum], body)
+	p[path][lineNum] = append(p[path][lineNum], bodyOrFingerprint)
 }
 
 // DebugLog outputs posted comments as log for debugging.
diff --git a/service/github/github.go b/service/github/github.go
index fe40c76c..a7ddaaa6 100644
--- a/service/github/github.go
+++ b/service/github/github.go
@@ -2,8 +2,10 @@ package github
 
 import (
 	"context"
+	"encoding/base64"
 	"errors"
 	"fmt"
+	"hash/fnv"
 	"log"
 	"net/http"
 	"os"
@@ -13,10 +15,12 @@ import (
 	"sync"
 
 	"github.com/google/go-github/v60/github"
+	"google.golang.org/protobuf/proto"
 
 	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/cienv"
 	"github.com/reviewdog/reviewdog/pathutil"
+	"github.com/reviewdog/reviewdog/proto/metacomment"
 	"github.com/reviewdog/reviewdog/proto/rdf"
 	"github.com/reviewdog/reviewdog/service/commentutil"
 	"github.com/reviewdog/reviewdog/service/github/githubutils"
@@ -154,8 +158,12 @@ func (g *PullRequest) postAsReviewComment(ctx context.Context) error {
 			}
 			repoBaseHTMLURLForRelatedLoc = repo.GetHTMLURL() + "/blob/" + g.sha
 		}
-		body := buildBody(c, repoBaseHTMLURLForRelatedLoc, rootPath)
-		if g.postedcs.IsPosted(c, githubCommentLine(c), body) {
+		fprint, err := fingerprint(c.Result.Diagnostic)
+		if err != nil {
+			return err
+		}
+		body := buildBody(c, repoBaseHTMLURLForRelatedLoc, rootPath, fprint)
+		if g.postedcs.IsPosted(c, githubCommentLine(c), fprint) {
 			// it's already posted. skip it.
 			continue
 		}
@@ -307,11 +315,42 @@ func (g *PullRequest) setPostedComment(ctx context.Context) error {
 		if c.GetSubjectType() == "line" {
 			line = c.GetLine()
 		}
-		g.postedcs.AddPostedComment(c.GetPath(), line, c.GetBody())
+		if meta := extractMetaComment(c.GetBody()); meta != nil {
+			g.postedcs.AddPostedComment(c.GetPath(), line, meta.GetFingerprint())
+		}
+	}
+	return nil
+}
+
+func extractMetaComment(body string) *metacomment.MetaComment {
+	prefix := "<!-- __reviewdog__:"
+	for _, line := range strings.Split(body, "\n") {
+		if after, found := strings.CutPrefix(line, prefix); found {
+			if metastring, foundSuffix := strings.CutSuffix(after, " -->"); foundSuffix {
+				meta, err := DecodeMetaComment(metastring)
+				if err != nil {
+					log.Printf("failed to decode MetaComment: %v", err)
+					continue
+				}
+				return meta
+			}
+		}
 	}
 	return nil
 }
 
+func DecodeMetaComment(metaBase64 string) (*metacomment.MetaComment, error) {
+	b, err := base64.StdEncoding.DecodeString(metaBase64)
+	if err != nil {
+		return nil, err
+	}
+	meta := &metacomment.MetaComment{}
+	if err := proto.Unmarshal(b, meta); err != nil {
+		return nil, err
+	}
+	return meta, nil
+}
+
 // Diff returns a diff of PullRequest.
 func (g *PullRequest) Diff(ctx context.Context) ([]byte, error) {
 	opt := github.RawOptions{Type: github.Diff}
@@ -403,7 +442,7 @@ func listAllPullRequestsComments(ctx context.Context, cli *github.Client,
 	return append(comments, restComments...), nil
 }
 
-func buildBody(c *reviewdog.Comment, baseRelatedLocURL string, gitRootPath string) string {
+func buildBody(c *reviewdog.Comment, baseRelatedLocURL string, gitRootPath string, fprint string) string {
 	cbody := commentutil.MarkdownComment(c)
 	if suggestion := buildSuggestions(c); suggestion != "" {
 		cbody += "\n" + suggestion
@@ -420,9 +459,20 @@ func buildBody(c *reviewdog.Comment, baseRelatedLocURL string, gitRootPath strin
 		}
 		cbody += "\n<hr>\n\n" + relatedLoc.GetMessage() + "\n" + relatedURL
 	}
+	cbody += fmt.Sprintf("\n<!-- __reviewdog__:%s -->\n", buildMetaComment(fprint, c.ToolName))
 	return cbody
 }
 
+func buildMetaComment(fprint string, toolName string) string {
+	b, _ := proto.Marshal(
+		&metacomment.MetaComment{
+			Fingerprint: fprint,
+			SourceName:  toolName,
+		},
+	)
+	return base64.StdEncoding.EncodeToString(b)
+}
+
 func buildSuggestions(c *reviewdog.Comment) string {
 	var sb strings.Builder
 	for _, s := range c.Result.Diagnostic.GetSuggestions() {
@@ -505,3 +555,21 @@ func getSourceLine(sourceLines map[int]string, line int) (string, error) {
 	}
 	return lineContent, nil
 }
+
+func fingerprint(d *rdf.Diagnostic) (string, error) {
+	h := fnv.New64a()
+	// Ideally, we should not use proto.Marshal since Proto Serialization Is Not
+	// Canonical.
+	// https://protobuf.dev/programming-guides/serialization-not-canonical/
+	//
+	// However, I left it as-is for now considering the same reviewdog binary
+	// should re-calculate and compare fingerprint for almost all cases.
+	data, err := proto.Marshal(d)
+	if err != nil {
+		return "", err
+	}
+	if _, err := h.Write(data); err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%x", h.Sum64()), nil
+}


Test patch:
diff --git a/service/github/github_test.go b/service/github/github_test.go
index 3be394cb..51626fe4 100644
--- a/service/github/github_test.go
+++ b/service/github/github_test.go
@@ -8,6 +8,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"regexp"
 	"strings"
 	"testing"
 
@@ -183,7 +184,7 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					{
 						Path:        github.String("reviewdog.go"),
 						Line:        github.Int(2),
-						Body:        github.String(commentutil.BodyPrefix + "already commented"),
+						Body:        github.String(commentutil.BodyPrefix + "already commented" + "\n<!-- __reviewdog__:ChBmMzg0YTRlZDRkYTViOTZl -->\n"),
 						SubjectType: github.String("line"),
 					},
 				}
@@ -196,21 +197,21 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					{
 						Path:        github.String("reviewdog.go"),
 						Line:        github.Int(15),
-						Body:        github.String(commentutil.BodyPrefix + "already commented 2"),
+						Body:        github.String(commentutil.BodyPrefix + "already commented 2" + "\n<!-- __reviewdog__:ChAxNDgzY2EyNTY0MjU2NmYx -->\n"),
 						SubjectType: github.String("line"),
 					},
 					{
 						Path:        github.String("reviewdog.go"),
 						StartLine:   github.Int(15),
 						Line:        github.Int(16),
-						Body:        github.String(commentutil.BodyPrefix + "multiline existing comment"),
+						Body:        github.String(commentutil.BodyPrefix + "multiline existing comment" + "\n<!-- __reviewdog__:ChBjNGNiNTRjMDc2YjNhMjcx -->\n"),
 						SubjectType: github.String("line"),
 					},
 					{
 						Path:        github.String("reviewdog.go"),
 						StartLine:   github.Int(15),
 						Line:        github.Int(17),
-						Body:        github.String(commentutil.BodyPrefix + "multiline existing comment (line-break)"),
+						Body:        github.String(commentutil.BodyPrefix + "multiline existing comment (line-break)" + "\n<!-- __reviewdog__:ChA2NjI1ZDI2MGJmNTdhNjUw -->\n"),
 						SubjectType: github.String("line"),
 					},
 					{
@@ -257,7 +258,7 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 				Path: github.String("reviewdog.go"),
 				Side: github.String("RIGHT"),
 				Line: github.Int(15),
-				Body: github.String(commentutil.BodyPrefix + "new comment"),
+				Body: github.String(commentutil.BodyPrefix + "new comment" + "\n<!-- __reviewdog__:xxxxxxxxxx -->\n"),
 			},
 			{
 				Path:      github.String("reviewdog.go"),
@@ -265,7 +266,7 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 				StartSide: github.String("RIGHT"),
 				StartLine: github.Int(15),
 				Line:      github.Int(16),
-				Body:      github.String(commentutil.BodyPrefix + "multiline new comment"),
+				Body:      github.String(commentutil.BodyPrefix + "multiline new comment" + "\n<!-- __reviewdog__:xxxxxxxxxx -->\n"),
 			},
 			{
 				Path:      github.String("reviewdog.go"),
@@ -280,6 +281,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"line2",
 					"line3",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -292,6 +295,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"line1",
 					"line2",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -303,6 +308,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
 					"invalid lines suggestion comment",
 					invalidSuggestionPre + "GitHub comment range and suggestion line range must be same. L15-L16 v.s. L16-L17" + invalidSuggestionPost,
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -318,6 +325,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"line2",
 					"line3",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -334,6 +343,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"line3",
 					"```",
 					invalidSuggestionPre + "GitHub comment range and suggestion line range must be same. L14-L16 v.s. L14-L14" + invalidSuggestionPost,
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -345,6 +356,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 				Body: github.String(commentutil.BodyPrefix + strings.Join([]string{
 					"non-line based suggestion comment (no source lines)",
 					invalidSuggestionPre + "source lines are not available" + invalidSuggestionPost,
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -356,6 +369,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```suggestion",
 					"haya14busa",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -369,6 +384,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```suggestion",
 					"haya14busa (multi-line)",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -382,6 +399,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```suggestion",
 					"line 15 (content at line 15)",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -393,6 +412,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```suggestion",
 					"haya14busa",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -410,6 +431,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```suggestion",
 					"haya14busa",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -421,6 +444,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```suggestion",
 					"haya14busa",
 					"```",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -436,6 +461,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"some code",
 					"```",
 					"````",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -449,6 +476,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"some code",
 					"```",
 					"````",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -463,6 +492,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"```",
 					"`````",
 					"``````",
+					"",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
 				}, "\n") + "\n"),
 			},
 			{
@@ -481,9 +512,18 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					"",
 					"related loc test (2)",
 					"https://test/repo/path/blob/sha/service/github/reviewdog2.go#L14",
+					"<!-- __reviewdog__:xxxxxxxxxx -->",
+					"",
 				}, "\n")),
 			},
 		}
+		// Replace __reviewdog__ comment so that the test pass regardless of environments.
+		// Proto serialization is not canonical, and test could break unless
+		// replacing the metacomment string.
+		for i := 0; i < len(req.Comments); i++ {
+			metaCommentRe := regexp.MustCompile(`__reviewdog__:\S+`)
+			req.Comments[i].Body = github.String(metaCommentRe.ReplaceAllString(*req.Comments[i].Body, `__reviewdog__:xxxxxxxxxx`))
+		}
 		if diff := pretty.Compare(want, req.Comments); diff != "" {
 			t.Errorf("req.Comments diff: (-got +want)\n%s", diff)
 		}