Repository: reviewdog/reviewdog
Pull request #1809 (2024-07-01 14:32:34)

Support posting comments outside diff context in github-pr-review reporter
- [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/reviewdog.yml b/.github/workflows/reviewdog.yml
index 0c549f90..8379d097 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -151,7 +151,7 @@ jobs:
           REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         run: |
           cat ./_testdata/custom_rdjson.json | \
-            reviewdog -name="custom-rdjson" -f=rdjson -reporter=github-pr-review
+            reviewdog -name="custom-rdjson" -f=rdjson -reporter=github-pr-review -filter-mode=nofilter
 
       - name: gofmt -s with reviewdog
         env:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0adc1f8..e598c92b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - [#1807](https://github.com/reviewdog/reviewdog/pull/1807) Add -reporter=sarif
   which outputs SARIF format to stdout. You can upload the output SARIF to
   GitHub and see code scanning alerts.
-
+- [#1809](https://github.com/reviewdog/reviewdog/pull/1809) Support posting
+  comments outside diff context as a file comment in github-pr-review reporter
 
 ### :bug: Fixes
 - ...
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index d5c2a5a8..452fac21 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -20,7 +20,7 @@ import (
 	"golang.org/x/build/gerrit"
 	"golang.org/x/oauth2"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/mattn/go-shellwords"
 	"github.com/reviewdog/errorformat/fmts"
 	"github.com/xanzy/go-gitlab"
diff --git a/doghouse/appengine/github.go b/doghouse/appengine/github.go
index c9601a84..5a9e0765 100644
--- a/doghouse/appengine/github.go
+++ b/doghouse/appengine/github.go
@@ -12,7 +12,7 @@ import (
 	"net/url"
 	"strings"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/justinas/nosurf"
 	"github.com/vvakame/sdlog/aelog"
 	"golang.org/x/oauth2"
diff --git a/doghouse/appengine/github_webhook.go b/doghouse/appengine/github_webhook.go
index 7538c307..8044c051 100644
--- a/doghouse/appengine/github_webhook.go
+++ b/doghouse/appengine/github_webhook.go
@@ -6,7 +6,7 @@ import (
 	"fmt"
 	"net/http"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 
 	"github.com/reviewdog/reviewdog/doghouse/server/storage"
 )
diff --git a/doghouse/server/doghouse.go b/doghouse/server/doghouse.go
index 33230665..5931b871 100644
--- a/doghouse/server/doghouse.go
+++ b/doghouse/server/doghouse.go
@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 
 	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/diff"
diff --git a/doghouse/server/github.go b/doghouse/server/github.go
index e0a1fae0..a20fb193 100644
--- a/doghouse/server/github.go
+++ b/doghouse/server/github.go
@@ -6,7 +6,7 @@ import (
 	"net/http"
 
 	"github.com/bradleyfalzon/ghinstallation/v2"
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 )
 
 type NewGitHubClientOption struct {
diff --git a/go.mod b/go.mod
index 5cbacfeb..2a994069 100644
--- a/go.mod
+++ b/go.mod
@@ -9,7 +9,7 @@ require (
 	contrib.go.opencensus.io/exporter/stackdriver v0.13.12
 	github.com/bradleyfalzon/ghinstallation/v2 v2.11.0
 	github.com/google/go-cmp v0.6.0
-	github.com/google/go-github/v60 v60.0.0
+	github.com/google/go-github/v62 v62.0.0
 	github.com/haya14busa/go-actions-toolkit v0.0.0-20200105081403-ca0307860f01
 	github.com/haya14busa/go-sarif v0.0.0-20240630170108-a3ba8d79599f
 	github.com/haya14busa/secretbox v0.0.0-20180525171038-07c7ecf409f5
@@ -48,7 +48,6 @@ require (
 	github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
-	github.com/google/go-github/v62 v62.0.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/google/s2a-go v0.1.7 // indirect
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
diff --git a/go.sum b/go.sum
index 677e543e..0d577b21 100644
--- a/go.sum
+++ b/go.sum
@@ -170,6 +170,8 @@ github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCy
 github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
 github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
 github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
+github.com/google/go-github/v62 v62.0.1-0.20240626135736-42ae4fe29180 h1:7hwybxRYjKO3kRzxJSW0S+H3b8yBtHF/oUL1ROmCx8Q=
+github.com/google/go-github/v62 v62.0.1-0.20240626135736-42ae4fe29180/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
 github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
 github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
diff --git a/scripts/trigger-depup/main.go b/scripts/trigger-depup/main.go
index d5d09961..b42da679 100644
--- a/scripts/trigger-depup/main.go
+++ b/scripts/trigger-depup/main.go
@@ -10,7 +10,7 @@ import (
 	"os"
 	"strings"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"golang.org/x/oauth2"
 )
 
diff --git a/service/github/check.go b/service/github/check.go
index ad01f225..d39c8ea3 100644
--- a/service/github/check.go
+++ b/service/github/check.go
@@ -8,7 +8,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/cienv"
 	"github.com/reviewdog/reviewdog/filter"
diff --git a/service/github/diff.go b/service/github/diff.go
index d513d490..81174c2d 100644
--- a/service/github/diff.go
+++ b/service/github/diff.go
@@ -8,7 +8,7 @@ import (
 	"os"
 	"os/exec"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/reviewdog/reviewdog"
 )
 
diff --git a/service/github/github.go b/service/github/github.go
index a4a91e43..086dfad7 100644
--- a/service/github/github.go
+++ b/service/github/github.go
@@ -13,7 +13,7 @@ import (
 	"strings"
 	"sync"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"google.golang.org/protobuf/proto"
 
 	"github.com/reviewdog/reviewdog"
@@ -31,6 +31,8 @@ var _ reviewdog.DiffService = (*PullRequest)(nil)
 
 const maxCommentsPerRequest = 30
 
+const maxFileComments = 10
+
 const (
 	invalidSuggestionPre  = "<details><summary>reviewdog suggestion error</summary>"
 	invalidSuggestionPost = "</details>"
@@ -137,15 +139,19 @@ func (g *PullRequest) postAsReviewComment(ctx context.Context) error {
 	g.postComments = nil
 	rawComments := make([]*reviewdog.Comment, 0, len(postComments))
 	reviewComments := make([]*github.DraftReviewComment, 0, len(postComments))
+	fileComments := make([]*github.PullRequestComment, 0)
 	remaining := make([]*reviewdog.Comment, 0)
-	repoBaseHTMLURLForRelatedLoc := ""
 	rootPath, err := serviceutil.GetGitRoot()
 	if err != nil {
 		return err
 	}
+	repoBaseHTMLURL, err := g.repoBaseHTMLURL(ctx)
+	if err != nil {
+		return err
+	}
 	for _, c := range postComments {
-		if !c.Result.InDiffContext {
-			// GitHub Review API cannot report results outside diff. If it's running
+		if !c.Result.InDiffFile {
+			// GitHub Review API cannot report results outside diff file. If it's running
 			// in GitHub Actions, fallback to GitHub Actions log as report.
 			if cienv.IsInGitHubAction() {
 				if err := g.logWriter.Post(ctx, c); err != nil {
@@ -154,49 +160,51 @@ func (g *PullRequest) postAsReviewComment(ctx context.Context) error {
 			}
 			continue
 		}
-		if repoBaseHTMLURLForRelatedLoc == "" && len(c.Result.Diagnostic.GetRelatedLocations()) > 0 {
-			repo, _, err := g.cli.Repositories.Get(ctx, g.owner, g.repo)
-			if err != nil {
-				return err
-			}
-			repoBaseHTMLURLForRelatedLoc = repo.GetHTMLURL() + "/blob/" + g.sha
-		}
 		fprint, err := fingerprint(c.Result.Diagnostic)
 		if err != nil {
 			return err
 		}
-		body := buildBody(c, repoBaseHTMLURLForRelatedLoc, rootPath, fprint, g.toolName)
 		if g.postedcs.IsPosted(c, githubCommentLine(c), fprint) {
 			// it's already posted. Mark the comment as non-outdated and skip it.
 			delete(g.outdatedComments, fprint)
 			continue
 		}
 
-		// Only posts maxCommentsPerRequest comments per 1 request to avoid spammy
-		// review comments. An example GitHub error if we don't limit the # of
-		// review comments.
-		//
-		// > 403 You have triggered an abuse detection mechanism and have been
-		// > temporarily blocked from content creation. Please retry your request
-		// > again later.
-		// https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limiting
-		if len(reviewComments) >= maxCommentsPerRequest {
-			remaining = append(remaining, c)
-			continue
+		if c.Result.InDiffContext {
+			// Only posts maxCommentsPerRequest comments per 1 request to avoid spammy
+			// review comments. An example GitHub error if we don't limit the # of
+			// review comments.
+			//
+			// > 403 You have triggered an abuse detection mechanism and have been
+			// > temporarily blocked from content creation. Please retry your request
+			// > again later.
+			// https://docs.github.com/en/rest/overview/resources-in-the-rest-api?apiVersion=2022-11-28#rate-limiting
+			if len(reviewComments) >= maxCommentsPerRequest {
+				remaining = append(remaining, c)
+				continue
+			}
+			comment := buildDraftReviewComment(c, buildBody(c, repoBaseHTMLURL, rootPath, fprint, g.toolName))
+			reviewComments = append(reviewComments, comment)
+		} else {
+			if len(fileComments) >= maxFileComments {
+				remaining = append(remaining, c)
+				continue
+			}
+			comment := buildPullRequestFileComment(c, buildBody(c, repoBaseHTMLURL, rootPath, fprint, g.toolName), g.sha)
+			fileComments = append(fileComments, comment)
 		}
-		reviewComments = append(reviewComments, buildDraftReviewComment(c, body))
 	}
 	if err := g.logWriter.Flush(ctx); err != nil {
 		return err
 	}
 
-	if len(reviewComments) > 0 {
+	if len(reviewComments) > 0 || len(remaining) > 0 {
 		// send review comments to GitHub.
 		review := &github.PullRequestReviewRequest{
 			CommitID: &g.sha,
 			Event:    github.String("COMMENT"),
 			Comments: reviewComments,
-			Body:     github.String(g.remainingCommentsSummary(remaining)),
+			Body:     github.String(g.remainingCommentsSummary(remaining, repoBaseHTMLURL, rootPath)),
 		}
 		_, _, err := g.cli.PullRequests.CreateReview(ctx, g.owner, g.repo, g.pr, review)
 		if err != nil {
@@ -209,6 +217,17 @@ func (g *PullRequest) postAsReviewComment(ctx context.Context) error {
 			return err
 		}
 	}
+	for _, c := range fileComments {
+		if _, _, err := g.cli.PullRequests.CreateComment(ctx, g.owner, g.repo, g.pr, c); err != nil {
+			log.Printf("reviewdog: failed to post a pull request comment: %v", err)
+			// GitHub returns 403 or 404 if we don't have permission to post a review comment.
+			// fallback to log message in this case.
+			if isPermissionError(err) && cienv.IsInGitHubAction() {
+				goto FALLBACK
+			}
+			return err
+		}
+	}
 
 	for _, c := range g.outdatedComments {
 		if ok := g.prCommentWithReply[c.GetID()]; ok {
@@ -257,12 +276,22 @@ func buildDraftReviewComment(c *reviewdog.Comment, body string) *github.DraftRev
 	return r
 }
 
+func buildPullRequestFileComment(c *reviewdog.Comment, body string, sha string) *github.PullRequestComment {
+	return &github.PullRequestComment{
+		Path:        github.String(c.Result.Diagnostic.GetLocation().GetPath()),
+		Side:        github.String("RIGHT"),
+		Body:        github.String(body),
+		CommitID:    github.String(sha),
+		SubjectType: github.String("file"),
+	}
+}
+
 // line represents end line if it's a multiline comment in GitHub, otherwise
 // it's start line.
 // Document: https://docs.github.com/en/rest/reference/pulls#create-a-review-comment-for-a-pull-request
 func githubCommentLine(c *reviewdog.Comment) int {
 	if !c.Result.InDiffContext {
-		return 0
+		return 1 // GitHub returns line==1 for FILE comment.
 	}
 	_, end := githubCommentLineRange(c)
 	return end
@@ -288,7 +317,7 @@ func githubCommentLineRange(c *reviewdog.Comment) (start int, end int) {
 	return int(startLine), int(endLine)
 }
 
-func (g *PullRequest) remainingCommentsSummary(remaining []*reviewdog.Comment) string {
+func (g *PullRequest) remainingCommentsSummary(remaining []*reviewdog.Comment, baseURL string, gitRootPath string) string {
 	if len(remaining) == 0 {
 		return ""
 	}
@@ -304,7 +333,14 @@ func (g *PullRequest) remainingCommentsSummary(remaining []*reviewdog.Comment) s
 		sb.WriteString(fmt.Sprintf("<summary>%s</summary>\n", tool))
 		sb.WriteString("\n")
 		for _, c := range comments {
-			sb.WriteString(githubutils.LinkedMarkdownDiagnostic(g.owner, g.repo, g.sha, c.Result.Diagnostic))
+			sb.WriteString("<hr>")
+			sb.WriteString("\n")
+			sb.WriteString("\n")
+			sb.WriteString(commentutil.MarkdownComment(c))
+			sb.WriteString("\n")
+			sb.WriteString("\n")
+			sb.WriteString(githubCodeSnippetURL(baseURL, gitRootPath, c.Result.Diagnostic.GetLocation()))
+			sb.WriteString("\n")
 			sb.WriteString("\n")
 		}
 		sb.WriteString("</details>\n")
@@ -325,15 +361,8 @@ func (g *PullRequest) setPostedComment(ctx context.Context) error {
 		if id := c.GetInReplyTo(); id != 0 {
 			g.prCommentWithReply[id] = true
 		}
-		if c.Line == nil || c.Path == nil || c.Body == nil || c.SubjectType == nil {
-			continue
-		}
-		var line int
-		if c.GetSubjectType() == "line" {
-			line = c.GetLine()
-		}
 		if meta := extractMetaComment(c.GetBody()); meta != nil {
-			g.postedcs.AddPostedComment(c.GetPath(), line, meta.GetFingerprint())
+			g.postedcs.AddPostedComment(c.GetPath(), c.GetLine(), meta.GetFingerprint())
 			if meta.SourceName == g.toolName {
 				g.outdatedComments[meta.GetFingerprint()] = c // Remove non-outdated comment later.
 			}
@@ -388,6 +417,14 @@ func (g *PullRequest) Strip() int {
 	return 1
 }
 
+func (g *PullRequest) repoBaseHTMLURL(ctx context.Context) (string, error) {
+	repo, _, err := g.cli.Repositories.Get(ctx, g.owner, g.repo)
+	if err != nil {
+		return "", fmt.Errorf("failed to build repo base HTML URL: %w", err)
+	}
+	return repo.GetHTMLURL() + "/blob/" + g.sha, nil
+}
+
 func (g *PullRequest) comment(ctx context.Context) ([]*github.PullRequestComment, error) {
 	// https://developer.github.com/v3/guides/traversing-with-pagination/
 	opts := &github.PullRequestListCommentsOptions{
@@ -424,27 +461,42 @@ func listAllPullRequestsComments(ctx context.Context, cli *github.Client,
 	return append(comments, restComments...), nil
 }
 
-func buildBody(c *reviewdog.Comment, baseRelatedLocURL string, gitRootPath string, fprint string, toolName string) string {
+func buildBody(c *reviewdog.Comment, baseURL string, gitRootPath string, fprint string, toolName string) string {
 	cbody := commentutil.MarkdownComment(c)
-	if suggestion := buildSuggestions(c); suggestion != "" {
-		cbody += "\n" + suggestion
+	if c.Result.InDiffContext {
+		if suggestion := buildSuggestions(c); suggestion != "" {
+			cbody += "\n" + suggestion
+		}
+	} else {
+		if c.Result.Diagnostic.GetLocation().GetRange().GetStart().GetLine() > 0 {
+			snippetURL := githubCodeSnippetURL(baseURL, gitRootPath, c.Result.Diagnostic.GetLocation())
+			cbody += "\n\n" + snippetURL
+		}
 	}
 	for _, relatedLoc := range c.Result.Diagnostic.GetRelatedLocations() {
 		loc := relatedLoc.GetLocation()
 		if loc.GetPath() == "" || loc.GetRange().GetStart().GetLine() == 0 {
 			continue
 		}
-		relPath := pathutil.NormalizePath(loc.GetPath(), gitRootPath, "")
-		relatedURL := fmt.Sprintf("%s/%s#L%d", baseRelatedLocURL, relPath, loc.GetRange().GetStart().GetLine())
-		if endLine := loc.GetRange().GetEnd().GetLine(); endLine > 0 {
-			relatedURL += fmt.Sprintf("-L%d", endLine)
-		}
-		cbody += "\n<hr>\n\n" + relatedLoc.GetMessage() + "\n" + relatedURL
+		snippetURL := githubCodeSnippetURL(baseURL, gitRootPath, loc)
+		cbody += "\n<hr>\n\n" + relatedLoc.GetMessage() + "\n" + snippetURL
 	}
 	cbody += fmt.Sprintf("\n<!-- __reviewdog__:%s -->\n", BuildMetaComment(fprint, toolName))
 	return cbody
 }
 
+func githubCodeSnippetURL(baseURL, gitRootPath string, loc *rdf.Location) string {
+	relPath := pathutil.NormalizePath(loc.GetPath(), gitRootPath, "")
+	relatedURL := fmt.Sprintf("%s/%s", baseURL, relPath)
+	if startLine := loc.GetRange().GetStart().GetLine(); startLine > 0 {
+		relatedURL += fmt.Sprintf("#L%d", startLine)
+	}
+	if endLine := loc.GetRange().GetEnd().GetLine(); endLine > 0 {
+		relatedURL += fmt.Sprintf("-L%d", endLine)
+	}
+	return relatedURL
+}
+
 func BuildMetaComment(fprint string, toolName string) string {
 	b, _ := proto.Marshal(
 		&metacomment.MetaComment{


Test patch:
diff --git a/_testdata/custom.txt b/_testdata/custom.txt
index 07a3770d..05253f7f 100644
--- a/_testdata/custom.txt
+++ b/_testdata/custom.txt
@@ -17,3 +17,5 @@ function f()
 }
 
 RelatedLoc
+
+new line
diff --git a/_testdata/custom_rdjson.json b/_testdata/custom_rdjson.json
index 8ab744db..0c4a0894 100644
--- a/_testdata/custom_rdjson.json
+++ b/_testdata/custom_rdjson.json
@@ -249,6 +249,12 @@
           }
         }
       ]
+    },
+    {
+      "message": "test file comment",
+      "location": {
+        "path": "_testdata/custom.txt"
+      }
     }
   ]
 }
diff --git a/doghouse/server/doghouse_test.go b/doghouse/server/doghouse_test.go
index 4922e28a..146b5859 100644
--- a/doghouse/server/doghouse_test.go
+++ b/doghouse/server/doghouse_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/reviewdog/reviewdog/doghouse"
 	"github.com/reviewdog/reviewdog/proto/rdf"
 )
diff --git a/service/github/check_test.go b/service/github/check_test.go
index 713392a6..9288a01e 100644
--- a/service/github/check_test.go
+++ b/service/github/check_test.go
@@ -10,7 +10,7 @@ import (
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/filter"
 	"github.com/reviewdog/reviewdog/proto/rdf"
diff --git a/service/github/diff_test.go b/service/github/diff_test.go
index 81f02eee..a7b76f1e 100644
--- a/service/github/diff_test.go
+++ b/service/github/diff_test.go
@@ -9,7 +9,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 )
 
 const sampleDiff = `--- a/sample.old.txt	2016-10-13 05:09:35.820791185 +0900
diff --git a/service/github/github_test.go b/service/github/github_test.go
index df25272a..2cb9569b 100644
--- a/service/github/github_test.go
+++ b/service/github/github_test.go
@@ -12,10 +12,11 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/google/go-github/v60/github"
+	"github.com/google/go-github/v62/github"
 	"github.com/kylelemons/godebug/pretty"
 	"golang.org/x/oauth2"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/filter"
 	"github.com/reviewdog/reviewdog/proto/rdf"
@@ -132,7 +133,8 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 	defer setupEnvs()()
 
 	listCommentsAPICalled := 0
-	postCommentsAPICalled := 0
+	postReviewCommentAPICalled := 0
+	postPullRequestCommentAPICalled := 0
 	repoAPICalled := 0
 	delCommentsAPICalled := 0
 	mux := http.NewServeMux()
@@ -193,13 +195,13 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					{
 						Path:        github.String("reviewdog.go"),
 						Line:        github.Int(1),
-						Body:        github.String(commentutil.BodyPrefix + "existing file comment (no-line)"),
+						Body:        github.String(commentutil.BodyPrefix + "existing file comment (no-line)" + "\n<!-- __reviewdog__:ChA2ZDI2MGNmYjY3NTQ4YTgxEgl0b29sLW5hbWU= -->\n"),
 						SubjectType: github.String("file"),
 					},
 					{
 						Path:        github.String("reviewdog.go"),
 						Line:        github.Int(1),
-						Body:        github.String(commentutil.BodyPrefix + "existing file comment (outside diff-context)"),
+						Body:        github.String(commentutil.BodyPrefix + "existing file comment (outside diff-context)" + "\n<!-- __reviewdog__:ChAyMzFjY2Q1ZWRhMjRkM2ZhEgl0b29sLW5hbWU= -->\n"),
 						SubjectType: github.String("file"),
 					},
 				}
@@ -207,12 +209,43 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 					t.Fatal(err)
 				}
 			}
+		case http.MethodPost:
+			postPullRequestCommentAPICalled++
+			var req github.PullRequestComment
+			if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+				t.Error(err)
+			}
+			expects := []github.PullRequestComment{
+				{
+					Body:        github.String("<sub>reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:</sub><br>file comment (no-line)\n<!-- __reviewdog__:ChBkZDlkMDllNmM5MTllODU1Egl0b29sLW5hbWU= -->\n"),
+					Path:        github.String("reviewdog.go"),
+					Side:        github.String("RIGHT"),
+					CommitID:    github.String("sha"),
+					SubjectType: github.String("file"),
+				},
+				{
+					Body: github.String(`<sub>reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:</sub><br>file comment (outside diff-context)
+
+https://test/repo/path/blob/sha/reviewdog.go#L18
+<!-- __reviewdog__:ChA5Mzc1OWY5ZTRmMmI5NThhEgl0b29sLW5hbWU= -->
+`),
+					Path:        github.String("reviewdog.go"),
+					Side:        github.String("RIGHT"),
+					CommitID:    github.String("sha"),
+					SubjectType: github.String("file"),
+				},
+				{},
+			}
+			want := expects[postPullRequestCommentAPICalled-1]
+			if diff := cmp.Diff(req, want); diff != "" {
+				t.Errorf("result has diff (API call: %d):\n%s", postPullRequestCommentAPICalled, diff)
+			}
 		default:
 			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
 		}
 	})
 	mux.HandleFunc("/repos/o/r/pulls/14/reviews", func(w http.ResponseWriter, r *http.Request) {
-		postCommentsAPICalled++
+		postReviewCommentAPICalled++
 		if r.Method != http.MethodPost {
 			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
 		}
@@ -1253,8 +1286,11 @@ func TestGitHubPullRequest_Post_Flush_review_api(t *testing.T) {
 	if listCommentsAPICalled != 2 {
 		t.Errorf("GitHub List PullRequest comments API called %v times, want 2 times", listCommentsAPICalled)
 	}
-	if postCommentsAPICalled != 1 {
-		t.Errorf("GitHub post PullRequest comments API called %v times, want 1 times", postCommentsAPICalled)
+	if postReviewCommentAPICalled != 1 {
+		t.Errorf("GitHub post Review comments API called %v times, want 1 times", postReviewCommentAPICalled)
+	}
+	if postPullRequestCommentAPICalled != 2 {
+		t.Errorf("GitHub post PullRequest comments API called %v times, want 2 times", postPullRequestCommentAPICalled)
 	}
 	if repoAPICalled != 1 {
 		t.Errorf("GitHub Repository API called %v times, want 1 times", repoAPICalled)
@@ -1290,6 +1326,13 @@ func TestGitHubPullRequest_Post_toomany(t *testing.T) {
 			t.Errorf("PullRequestReviewRequest.Body is empty but want some summary text")
 		}
 	})
+	mux.HandleFunc("/repos/o/r", func(w http.ResponseWriter, r *http.Request) {
+		if err := json.NewEncoder(w).Encode(&github.Repository{
+			HTMLURL: github.String("https://test/repo/path"),
+		}); err != nil {
+			t.Fatal(err)
+		}
+	})
 	ts := httptest.NewServer(mux)
 	defer ts.Close()
 
@@ -1392,6 +1435,13 @@ func TestGitHubPullRequest_Post_NoPermission(t *testing.T) {
 		postCommentsAPICalled++
 		w.WriteHeader(http.StatusNotFound)
 	})
+	mux.HandleFunc("/repos/o/r", func(w http.ResponseWriter, r *http.Request) {
+		if err := json.NewEncoder(w).Encode(&github.Repository{
+			HTMLURL: github.String("https://test/repo/path"),
+		}); err != nil {
+			t.Fatal(err)
+		}
+	})
 	ts := httptest.NewServer(mux)
 	defer ts.Close()