Repository: reviewdog/reviewdog
Pull request #281 (2019-09-14 07:31:02)

Support PR from fork for github-pr-check on GitHub Actions

haya14busa: Report result to console for p-r from forked repo.
![image](https://user-images.githubusercontent.com/3797062/64906419-4ac7a000-d721-11e9-8901-7053512c2e34.png)


Patch:
diff --git a/README.md b/README.md
index e14f4836..1f39cdac 100644
--- a/README.md
+++ b/README.md
@@ -254,10 +254,12 @@ Example: [.github/workflows/reviewdog.yml](.github/workflows/reviewdog.yml)
 You don't need to specify `CI_*` environment variables once
 [#279](https://github.com/reviewdog/reviewdog/issues/279) is fixed.
 
-Note that it won't work for Pull Requests from fork repository due to
-[GitHub Actions restriction](https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret).
-Could show results to GitHub Action log console as a fallback in the future
-([#280](https://github.com/reviewdog/reviewdog/issues/280)).
+Note that it reports result to GitHub Actions log consle for Pull
+Requests from fork repository because due to [GitHub Actions
+restriction](https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret),
+`GITHUB_TOKEN` for PullRequest from forked repository doesn't have write access
+to Check API.
+[![github-pr-check from forked repo example](https://user-images.githubusercontent.com/3797062/64907277-2a044800-d72b-11e9-82b1-f739414f9d48.png)](https://github.com/reviewdog/reviewdog/pull/281/checks?check_run_id=222108016#step:7:15)
 
 #### Option 2) Install reviewdog GitHub Apps
 reviewdog CLI send a request to reviewdog GitHub App server and the server post
@@ -457,13 +459,6 @@ $ REVIEWDOG_GITHUB_API_TOKEN="<token>"
 $ REVIEWDOG_GITLAB_API_TOKEN="<token>"
 ```
 
-If a CI service doesn't provide information such as Pull Request ID - reviewdog can guess it by branch name and commit SHA.
-Just pass the flag `guess`:
- 
-```shell
-$ reviewdog -conf=.reviewdog.yml -reporter=github-pr-check -guess
-```
-
 #### Jenkins with Github pull request builder plugin
 - [GitHub pull request builder plugin - Jenkins - Jenkins Wiki](https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin)
 
diff --git a/cmd/reviewdog/doghouse.go b/cmd/reviewdog/doghouse.go
index be37b192..845a9552 100644
--- a/cmd/reviewdog/doghouse.go
+++ b/cmd/reviewdog/doghouse.go
@@ -2,11 +2,13 @@ package main
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"log"
 	"net/http"
 	"os"
+	"sort"
 	"strings"
 
 	"github.com/reviewdog/reviewdog"
@@ -18,7 +20,7 @@ import (
 	"golang.org/x/sync/errgroup"
 )
 
-func runDoghouse(ctx context.Context, r io.Reader, opt *option, isProject bool) error {
+func runDoghouse(ctx context.Context, r io.Reader, w io.Writer, opt *option, isProject bool) error {
 	ghInfo, isPr, err := cienv.GetBuildInfo()
 	if err != nil {
 		return err
@@ -35,7 +37,14 @@ func runDoghouse(ctx context.Context, r io.Reader, opt *option, isProject bool)
 	if err != nil {
 		return err
 	}
-	return postResultSet(ctx, resultSet, ghInfo, cli)
+	filteredResultSet, err := postResultSet(ctx, resultSet, ghInfo, cli)
+	if err != nil {
+		return err
+	}
+	if foundResultInDiff := reportResults(w, filteredResultSet); foundResultInDiff {
+		return errors.New("found at least one result in diff")
+	}
+	return nil
 }
 
 func newDoghouseCli(ctx context.Context) (client.DogHouseClientInterface, error) {
@@ -94,9 +103,10 @@ func checkResultSet(ctx context.Context, r io.Reader, opt *option, isProject boo
 	return resultSet, nil
 }
 
-func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap, ghInfo *cienv.BuildInfo, cli client.DogHouseClientInterface) error {
+func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap, ghInfo *cienv.BuildInfo, cli client.DogHouseClientInterface) (*reviewdog.FilteredCheckMap, error) {
 	var g errgroup.Group
 	wd, _ := os.Getwd()
+	filteredResultSet := new(reviewdog.FilteredCheckMap)
 	resultSet.Range(func(name string, results []*reviewdog.CheckResult) {
 		as := make([]*doghouse.Annotation, 0, len(results))
 		for _, r := range results {
@@ -116,11 +126,19 @@ func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap, ghInfo *
 			if err != nil {
 				return fmt.Errorf("post failed for %s: %v", name, err)
 			}
-			log.Printf("[%s] reported: %s", name, res.ReportURL)
+			if res.ReportURL != "" {
+				log.Printf("[%s] reported: %s", name, res.ReportURL)
+			}
+			if res.CheckedResults != nil {
+				filteredResultSet.Store(name, res.CheckedResults)
+			}
+			if res.ReportURL == "" && res.CheckedResults == nil {
+				return fmt.Errorf("No result found for %q", name)
+			}
 			return nil
 		})
 	})
-	return g.Wait()
+	return filteredResultSet, g.Wait()
 }
 
 func checkResultToAnnotation(c *reviewdog.CheckResult, wd string) *doghouse.Annotation {
@@ -136,3 +154,43 @@ func isInGitHubAction() bool {
 	// https://help.github.com/en/articles/virtual-environments-for-github-actions#default-environment-variables
 	return os.Getenv("GITHUB_ACTION") != ""
 }
+
+// reportResults reports results to given io.Writer and return true if at least
+// one annotation result is in diff.
+func reportResults(w io.Writer, filteredResultSet *reviewdog.FilteredCheckMap) bool {
+	// Get sorted names to get determinisitic result.
+	var names []string
+	filteredResultSet.Range(func(name string, results []*reviewdog.FilteredCheck) {
+		names = append(names, name)
+	})
+	sort.Strings(names)
+
+	foundInDiff := false
+	for _, name := range names {
+		results, err := filteredResultSet.Load(name)
+		if err != nil {
+			// Should not happen.
+			log.Printf("reviewdog: result not found for %q", name)
+			continue
+		}
+		fmt.Fprintf(w, "reviwedog: Reporting results for %q\n", name)
+		foundResultPerName := false
+		filteredNum := 0
+		for _, result := range results {
+			if !result.InDiff {
+				filteredNum++
+				continue
+			}
+			foundInDiff = true
+			foundResultPerName = true
+			// Output original lines.
+			for _, line := range result.Lines {
+				fmt.Fprintln(w, line)
+			}
+		}
+		if !foundResultPerName {
+			fmt.Fprintf(w, "reviwedog: No results found for %q. %d results found outside diff.\n", name, filteredNum)
+		}
+	}
+	return foundInDiff
+}
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index eb90626b..f5ca7164 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -12,7 +12,6 @@ import (
 	"os"
 	"os/exec"
 	"sort"
-	"strings"
 	"text/tabwriter"
 
 	"golang.org/x/net/context" // "context"
@@ -39,31 +38,29 @@ const usageMessage = "" +
 `
 
 type option struct {
-	version          bool
-	diffCmd          string
-	diffStrip        int
-	efms             strslice
-	f                string // errorformat name
-	list             bool   // list supported errorformat name
-	name             string // tool name which is used in comment
-	ci               string
-	conf             string
-	reporter         string
-	guessPullRequest bool
+	version   bool
+	diffCmd   string
+	diffStrip int
+	efms      strslice
+	f         string // errorformat name
+	list      bool   // list supported errorformat name
+	name      string // tool name which is used in comment
+	ci        string
+	conf      string
+	reporter  string
 }
 
 // flags doc
 const (
-	diffCmdDoc          = `diff command (e.g. "git diff"). diff flag is ignored if you pass "ci" flag`
-	diffStripDoc        = "strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)"
-	efmsDoc             = `list of errorformat (https://github.com/reviewdog/errorformat)`
-	fDoc                = `format name (run -list to see supported format name) for input. It's also used as tool name in review comment if -name is empty`
-	listDoc             = `list supported pre-defined format names which can be used as -f arg`
-	nameDoc             = `tool name in review comment. -f is used as tool name if -name is empty`
-	ciDoc               = `[deprecated] reviewdog automatically get necessary data. See also -reporter for migration`
-	confDoc             = `config file path`
-	guessPullRequestDoc = `guess Pull Request ID by branch name and commit SHA`
-	reporterDoc         = `reporter of reviewdog results. (local, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit)
+	diffCmdDoc   = `diff command (e.g. "git diff"). diff flag is ignored if you pass "ci" flag`
+	diffStripDoc = "strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)"
+	efmsDoc      = `list of errorformat (https://github.com/reviewdog/errorformat)`
+	fDoc         = `format name (run -list to see supported format name) for input. It's also used as tool name in review comment if -name is empty`
+	listDoc      = `list supported pre-defined format names which can be used as -f arg`
+	nameDoc      = `tool name in review comment. -f is used as tool name if -name is empty`
+	ciDoc        = `[deprecated] reviewdog automatically get necessary data. See also -reporter for migration`
+	confDoc      = `config file path`
+	reporterDoc  = `reporter of reviewdog results. (local, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit)
 	"local" (default)
 		Report results to stdout.
 
@@ -72,8 +69,8 @@ const (
 		There are two options to use this reporter.
 
 		Option 1) Run reviewdog from GitHub Actions w/ secrets.GITHUB_TOKEN
-			Note that it won't work for Pull Requests from fork repository due to
-			GitHub Actions restriction.
+			Note that it reports result to GitHub Actions log consle for Pull
+			Requests from fork repository due to GitHub Actions restriction.
 			https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret
 
 			Set REVIEWDOG_GITHUB_API_TOKEN with secrets.GITHUB_TOKEN. e.g.
@@ -138,7 +135,6 @@ func init() {
 	flag.StringVar(&opt.ci, "ci", "", ciDoc)
 	flag.StringVar(&opt.conf, "conf", "", confDoc)
 	flag.StringVar(&opt.reporter, "reporter", "local", reporterDoc)
-	flag.BoolVar(&opt.guessPullRequest, "guess", false, guessPullRequestDoc)
 }
 
 func usage() {
@@ -193,13 +189,13 @@ See -reporter flag for migration and set -reporter="github-pr-review" or -report
 	default:
 		return fmt.Errorf("unknown -reporter: %s", opt.reporter)
 	case "github-pr-check":
-		return runDoghouse(ctx, r, opt, isProject)
+		return runDoghouse(ctx, r, w, opt, isProject)
 	case "github-pr-review":
 		if os.Getenv("REVIEWDOG_GITHUB_API_TOKEN") == "" {
 			fmt.Fprintln(os.Stderr, "REVIEWDOG_GITHUB_API_TOKEN is not set")
 			return nil
 		}
-		gs, isPR, err := githubService(ctx, opt)
+		gs, isPR, err := githubService(ctx)
 		if err != nil {
 			return err
 		}
@@ -323,7 +319,7 @@ func insecureSkipVerify() bool {
 	return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true"
 }
 
-func githubService(ctx context.Context, opt *option) (gs *githubservice.GitHubPullRequest, isPR bool, err error) {
+func githubService(ctx context.Context) (gs *githubservice.GitHubPullRequest, isPR bool, err error) {
 	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
 	if err != nil {
 		return nil, isPR, err
@@ -332,65 +328,21 @@ func githubService(ctx context.Context, opt *option) (gs *githubservice.GitHubPu
 	if err != nil {
 		return nil, isPR, err
 	}
+	// TODO: support commit build
+	if !isPR {
+		return nil, isPR, nil
+	}
 
 	client, err := githubClient(ctx, token)
 	if err != nil {
 		return nil, isPR, err
 	}
 
-	if !isPR {
-		if !opt.guessPullRequest {
-			return nil, false, nil
-		}
-
-		if g.Branch == "" && g.SHA == "" {
-			return nil, false, nil
-		}
-
-		prID, err := getPullRequestIDByBranchOrCommit(ctx, client, g)
-		if err != nil {
-			fmt.Fprintln(os.Stderr, err)
-			return nil, false, nil
-		}
-		g.PullRequest = prID
-	}
-
 	gs, err = githubservice.NewGitHubPullRequest(client, g.Owner, g.Repo, g.PullRequest, g.SHA)
 	if err != nil {
-		return nil, false, err
-	}
-	return gs, true, nil
-}
-
-func getPullRequestIDByBranchOrCommit(ctx context.Context, client *github.Client, info *cienv.BuildInfo) (int, error) {
-	options := &github.SearchOptions{
-		Sort:  "updated",
-		Order: "desc",
-	}
-
-	query := []string{
-		"type:pr",
-		"state:open",
-		fmt.Sprintf("repo:%s/%s", info.Owner, info.Repo),
-	}
-	if info.Branch != "" {
-		query = append(query, fmt.Sprintf("head:%s", info.Branch))
-	}
-	if info.SHA != "" {
-		query = append(query, info.SHA)
-	}
-
-	preparedQuery := strings.Join(query, " ")
-	pullRequests, _, err := client.Search.Issues(ctx, preparedQuery, options)
-	if err != nil {
-		return 0, err
-	}
-
-	if *pullRequests.Total == 0 {
-		return 0, fmt.Errorf("reviewdog: PullRequest not found, query: %s", preparedQuery)
+		return nil, isPR, err
 	}
-
-	return *pullRequests.Issues[0].Number, nil
+	return gs, isPR, nil
 }
 
 func githubClient(ctx context.Context, token string) (*github.Client, error) {
diff --git a/doghouse/appengine/checker.go b/doghouse/appengine/checker.go
index 7d344dc0..a29f1ffb 100644
--- a/doghouse/appengine/checker.go
+++ b/doghouse/appengine/checker.go
@@ -74,13 +74,6 @@ func (gc *githubChecker) handleCheck(w http.ResponseWriter, r *http.Request) {
 }
 
 func (gc *githubChecker) validateCheckRequest(ctx context.Context, w http.ResponseWriter, r *http.Request, owner, repo string) bool {
-	if extractBearerToken(r) == "" {
-		// Update Travis IP Addresss before checking IP to reduce the # of
-		// flaky errors when token is not present.
-		if err := ciutil.UpdateTravisCIIPAddrs(urlfetch.Client(ctx)); err != nil {
-			log.Errorf(ctx, "failed to update travis CI IP addresses: %v", err)
-		}
-	}
 	log.Infof(ctx, "Remote Addr: %s", r.RemoteAddr)
 	if ciutil.IsFromCI(r) {
 		// Skip token validation if it's from trusted CI providers.
diff --git a/doghouse/server/doghouse.go b/doghouse/server/doghouse.go
index 3c76f037..1d0a3119 100644
--- a/doghouse/server/doghouse.go
+++ b/doghouse/server/doghouse.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"net/http"
 	"strings"
 	"time"
 
@@ -65,6 +66,15 @@ func (ch *Checker) Check(ctx context.Context) (*doghouse.CheckResponse, error) {
 	filtered := reviewdog.FilterCheck(results, filediffs, 1, "")
 	checkRun, err := ch.postCheck(ctx, branch, filtered)
 	if err != nil {
+		// If this error is StatusForbidden (403) here, it means reviewdog is
+		// running on GitHub Actions and has only read permission (because it's
+		// running for Pull Requests from forked repository). If the token itself
+		// is invalid, reviewdog should return an error earlier (e.g. when reading
+		// Pull Requests diff), so it should be ok not to return error here and
+		// return results instead.
+		if err, ok := err.(*github.ErrorResponse); ok && err.Response.StatusCode == http.StatusForbidden {
+			return &doghouse.CheckResponse{CheckedResults: filtered}, nil
+		}
 		return nil, fmt.Errorf("failed to post result: %v", err)
 	}
 	res := &doghouse.CheckResponse{
diff --git a/doghouse/service.go b/doghouse/service.go
index fdf66507..41f87312 100644
--- a/doghouse/service.go
+++ b/doghouse/service.go
@@ -1,5 +1,7 @@
 package doghouse
 
+import "github.com/reviewdog/reviewdog"
+
 // CheckRequest represents doghouse GitHub check request.
 type CheckRequest struct {
 	// Commit SHA.
@@ -28,7 +30,19 @@ type CheckRequest struct {
 
 // CheckResponse represents doghouse GitHub check response.
 type CheckResponse struct {
+	// ReportURL is report URL of check run.
+	// Optional.
 	ReportURL string `json:"report_url,omitempty"`
+
+	// CheckedResults is checked annotations result.
+	// This field is expected to be filled for GitHub Actions integration and
+	// filled when ReportURL is not available. i.e. reviewdog doens't have write
+	// permission to Check API.
+	// It's also not expected to be passed over network via JSON.
+	// TODO(haya14busa): Consider to move this type to this package to avoid
+	// (cyclic) import.
+	// Optional.
+	CheckedResults []*reviewdog.FilteredCheck
 }
 
 // Annotation represents an annotaion to file or specific line.
diff --git a/resultmap.go b/resultmap.go
index 9c6589a2..3c4a1864 100644
--- a/resultmap.go
+++ b/resultmap.go
@@ -48,3 +48,46 @@ func (rm *ResultMap) Len() int {
 	})
 	return l
 }
+
+// FilteredCheckMap represents a concurrent-safe map to store CheckResults generated by concurrent jobs.
+type FilteredCheckMap struct {
+	sm sync.Map
+}
+
+// Store saves a new []*FilteredCheck into FilteredCheckMap.
+func (rm *FilteredCheckMap) Store(key string, crs []*FilteredCheck) {
+	rm.sm.Store(key, crs)
+}
+
+// Load fetches []*FilteredCheck from FilteredCheckMap
+func (rm *FilteredCheckMap) Load(key string) ([]*FilteredCheck, error) {
+	v, ok := rm.sm.Load(key)
+	if !ok {
+		return nil, fmt.Errorf("fail to get the value of key %q from results", key)
+	}
+
+	t, ok := v.([]*FilteredCheck)
+	if !ok {
+		return nil, errors.New("stored type in FilteredCheckMap is invalid")
+	}
+
+	return t, nil
+}
+
+// Range retrieves `key` and `values` from FilteredCheckMap iteratively.
+func (rm *FilteredCheckMap) Range(f func(key string, val []*FilteredCheck)) {
+	rm.sm.Range(func(k, v interface{}) bool {
+		f(k.(string), v.([]*FilteredCheck))
+		return true
+	})
+}
+
+// Len returns the length of FilteredCheckMap count. Len() is not yet officially not supported by Go. (ref: https://github.com/golang/go/issues/20680)
+func (rm *FilteredCheckMap) Len() int {
+	l := 0
+	rm.sm.Range(func(_, _ interface{}) bool {
+		l++
+		return true
+	})
+	return l
+}


Test patch:
diff --git a/cmd/reviewdog/doghouse_test.go b/cmd/reviewdog/doghouse_test.go
index 5daff2d7..b81e1ad6 100644
--- a/cmd/reviewdog/doghouse_test.go
+++ b/cmd/reviewdog/doghouse_test.go
@@ -1,6 +1,7 @@
 package main
 
 import (
+	"bytes"
 	"context"
 	"io/ioutil"
 	"os"
@@ -189,7 +190,7 @@ func (f *fakeDoghouseServerCli) Check(ctx context.Context, req *doghouse.CheckRe
 	return f.FakeCheck(ctx, req)
 }
 
-func TestPostResultSet(t *testing.T) {
+func TestPostResultSet_withReportURL(t *testing.T) {
 	const (
 		owner = "haya14busa"
 		repo  = "reviewdog"
@@ -240,7 +241,7 @@ func TestPostResultSet(t *testing.T) {
 		default:
 			t.Errorf("unexpected req.Name: %s", req.Name)
 		}
-		return &doghouse.CheckResponse{}, nil
+		return &doghouse.CheckResponse{ReportURL: "xxx"}, nil
 	}
 
 	var resultSet reviewdog.ResultMap
@@ -271,7 +272,144 @@ func TestPostResultSet(t *testing.T) {
 		SHA:         sha,
 	}
 
-	if err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli); err != nil {
+	if _, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli); err != nil {
 		t.Fatal(err)
 	}
 }
+
+func TestPostResultSet_withoutReportURL(t *testing.T) {
+	const (
+		owner = "haya14busa"
+		repo  = "reviewdog"
+		prNum = 14
+		sha   = "1414"
+	)
+
+	wantResults := []*reviewdog.FilteredCheck{{LnumDiff: 1}}
+	fakeCli := &fakeDoghouseServerCli{}
+	fakeCli.FakeCheck = func(ctx context.Context, req *doghouse.CheckRequest) (*doghouse.CheckResponse, error) {
+		return &doghouse.CheckResponse{CheckedResults: wantResults}, nil
+	}
+
+	var resultSet reviewdog.ResultMap
+	resultSet.Store("name1", []*reviewdog.CheckResult{})
+
+	ghInfo := &cienv.BuildInfo{Owner: owner, Repo: repo, PullRequest: prNum, SHA: sha}
+
+	resp, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli)
+	if err != nil {
+		t.Fatal(err)
+	}
+	if resp.Len() == 0 {
+		t.Fatal("result should not be empty")
+	}
+	results, err := resp.Load("name1")
+	if err != nil {
+		t.Fatalf("should have result for name1: %v", err)
+	}
+	if diff := cmp.Diff(results, wantResults); diff != "" {
+		t.Errorf("results has diff:\n%s", diff)
+	}
+}
+
+func TestPostResultSet_withEmptyResponse(t *testing.T) {
+	const (
+		owner = "haya14busa"
+		repo  = "reviewdog"
+		prNum = 14
+		sha   = "1414"
+	)
+
+	fakeCli := &fakeDoghouseServerCli{}
+	fakeCli.FakeCheck = func(ctx context.Context, req *doghouse.CheckRequest) (*doghouse.CheckResponse, error) {
+		return &doghouse.CheckResponse{}, nil
+	}
+
+	var resultSet reviewdog.ResultMap
+	resultSet.Store("name1", []*reviewdog.CheckResult{})
+
+	ghInfo := &cienv.BuildInfo{Owner: owner, Repo: repo, PullRequest: prNum, SHA: sha}
+
+	if _, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli); err == nil {
+		t.Error("got no error but want report missing error")
+	}
+}
+
+func TestReportResults(t *testing.T) {
+	filteredResultSet := new(reviewdog.FilteredCheckMap)
+	filteredResultSet.Store("name1", []*reviewdog.FilteredCheck{
+		{
+			CheckResult: &reviewdog.CheckResult{
+				Lines: []string{"name1-L1", "name1-L2"},
+			},
+			InDiff: true,
+		},
+		{
+			CheckResult: &reviewdog.CheckResult{
+				Lines: []string{"name1.2-L1", "name1.2-L2"},
+			},
+			InDiff: false,
+		},
+	})
+	filteredResultSet.Store("name2", []*reviewdog.FilteredCheck{
+		{
+			CheckResult: &reviewdog.CheckResult{
+				Lines: []string{"name1-L1", "name1-L2"},
+			},
+			InDiff: false,
+		},
+	})
+	stdout := new(bytes.Buffer)
+	foundResultInDiff := reportResults(stdout, filteredResultSet)
+	if !foundResultInDiff {
+		t.Errorf("foundResultInDiff = %v, want true", foundResultInDiff)
+	}
+	want := `reviwedog: Reporting results for "name1"
+name1-L1
+name1-L2
+reviwedog: Reporting results for "name2"
+reviwedog: No results found for "name2". 1 results found outside diff.
+`
+	if got := stdout.String(); got != want {
+		t.Errorf("diff found for report:\ngot:\n%s\nwant:\n%s", got, want)
+	}
+}
+
+func TestReportResults_noResultsInDiff(t *testing.T) {
+	filteredResultSet := new(reviewdog.FilteredCheckMap)
+	filteredResultSet.Store("name1", []*reviewdog.FilteredCheck{
+		{
+			CheckResult: &reviewdog.CheckResult{
+				Lines: []string{"name1-L1", "name1-L2"},
+			},
+			InDiff: false,
+		},
+		{
+			CheckResult: &reviewdog.CheckResult{
+				Lines: []string{"name1.2-L1", "name1.2-L2"},
+			},
+			InDiff: false,
+		},
+	})
+	filteredResultSet.Store("name2", []*reviewdog.FilteredCheck{
+		{
+			CheckResult: &reviewdog.CheckResult{
+				Lines: []string{"name1-L1", "name1-L2"},
+			},
+			InDiff: false,
+		},
+	})
+	stdout := new(bytes.Buffer)
+	foundResultInDiff := reportResults(stdout, filteredResultSet)
+	if foundResultInDiff {
+		t.Errorf("foundResultInDiff = %v, want false", foundResultInDiff)
+	}
+	want := `reviwedog: Reporting results for "name1"
+reviwedog: No results found for "name1". 2 results found outside diff.
+reviwedog: Reporting results for "name2"
+reviwedog: No results found for "name2". 1 results found outside diff.
+`
+	if got := stdout.String(); got != want {
+		t.Errorf("diff found for report:\ngot:\n%s\nwant:\n%s", got, want)
+	}
+}
diff --git a/doghouse/server/doghouse_test.go b/doghouse/server/doghouse_test.go
index d442c912..5a647f0e 100644
--- a/doghouse/server/doghouse_test.go
+++ b/doghouse/server/doghouse_test.go
@@ -3,6 +3,7 @@ package server
 import (
 	"context"
 	"errors"
+	"net/http"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -297,3 +298,34 @@ func TestCheck_fail_check(t *testing.T) {
 		t.Log(err)
 	}
 }
+
+func TestCheck_fail_check_with_403(t *testing.T) {
+	req := &doghouse.CheckRequest{}
+	cli := &fakeCheckerGitHubCli{}
+	cli.FakeGetPullRequest = func(ctx context.Context, owner, repo string, number int) (*github.PullRequest, error) {
+		return &github.PullRequest{
+			Head: &github.PullRequestBranch{
+				Ref: github.String("branch"),
+			},
+		}, nil
+	}
+	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int) ([]byte, error) {
+		return []byte(sampleDiff), nil
+	}
+	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
+		return nil, &github.ErrorResponse{
+			Response: &http.Response{
+				StatusCode: http.StatusForbidden,
+			},
+		}
+	}
+	checker := &Checker{req: req, gh: cli}
+
+	resp, err := checker.Check(context.Background())
+	if err != nil {
+		t.Fatalf("got unexpected error: %v", err)
+	}
+	if resp.CheckedResults == nil {
+		t.Error("resp.CheckedResults should not be nil")
+	}
+}