Repository: reviewdog/reviewdog
Pull request #1798 (2024-06-22 13:22:36)

Refactor doghouse / github-[pr-]check related code
1. Implement GitHub Check API flow as CommentService / DiffService
2. Use the above {Comment,Diff}Service if reviewdog won't talk to
   the doghouse server (reviewdog GitHub App)
3. Implement github-pr-annotations reporter as {Comment,Diff}Service
4. It means doghouse related code (code under doghouse/ dir) should be used only when reviewdog
   needs to talk to the doghouse server

- [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 e6848f6f..a20265cc 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -209,6 +209,30 @@ jobs:
           locale: "US"
           reporter: github-check
 
+  reviewdog-github-annotations:
+    permissions:
+      contents: read
+    name: reviewdog (github-pr-annotations)
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - uses: actions/setup-go@v5
+        with:
+          go-version-file: "go.mod"
+
+      - name: Setup reviewdog
+        # uses: reviewdog/action-setup@v1
+        run: |
+          go install ./cmd/reviewdog
+
+      - name: Custom rdjson test
+        env:
+          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+        run: |
+          cat ./_testdata/custom_rdjson.json | \
+            reviewdog -name="custom-rdjson" -f=rdjson -reporter=github-pr-annotations
+
   typos:
     name: runner / typos
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 1ded0e87..d217a81a 100644
--- a/README.md
+++ b/README.md
@@ -48,7 +48,7 @@
   <a href="https://github.com/reviewdog/.github/blob/master/CODE_OF_CONDUCT.md">
     <img alt="Contributor Covenant" src="https://img.shields.io/badge/Contributor%20Covenant-v2.0%20adopted-ff69b4.svg">
   </a>
-  <a href="https://haya14busa.github.io/github-release-stats/#reviewdog/reviewdog">
+  <a href="https://somsubhra.github.io/github-release-stats/?username=reviewdog&repository=reviewdog&per_page=30">
     <img alt="GitHub Releases Stats" src="https://img.shields.io/github/downloads/reviewdog/reviewdog/total.svg?logo=github">
   </a>
   <a href="https://starchart.cc/reviewdog/reviewdog"><img alt="Stars" src="https://img.shields.io/github/stars/reviewdog/reviewdog.svg?style=social"></a>
diff --git a/cmd/reviewdog/doghouse.go b/cmd/reviewdog/doghouse.go
index ae7578e8..8a544efd 100644
--- a/cmd/reviewdog/doghouse.go
+++ b/cmd/reviewdog/doghouse.go
@@ -8,7 +8,6 @@ import (
 	"log"
 	"net/http"
 	"os"
-	"sort"
 
 	"golang.org/x/oauth2"
 	"golang.org/x/sync/errgroup"
@@ -20,57 +19,37 @@ import (
 	"github.com/reviewdog/reviewdog/pathutil"
 	"github.com/reviewdog/reviewdog/project"
 	"github.com/reviewdog/reviewdog/proto/rdf"
-	"github.com/reviewdog/reviewdog/service/github/githubutils"
 	"github.com/reviewdog/reviewdog/service/serviceutil"
 )
 
-func runDoghouse(ctx context.Context, r io.Reader, w io.Writer, opt *option, isProject bool, forPr bool, useAnnotationReporter bool) error {
-	ghInfo, isPr, err := cienv.GetBuildInfo()
+func runDoghouse(ctx context.Context, r io.Reader, w io.Writer, opt *option, isProject bool) error {
+	ghInfo, _, err := cienv.GetBuildInfo()
 	if err != nil {
 		return err
 	}
-	if !isPr && forPr {
-		fmt.Fprintln(os.Stderr, "reviewdog: this is not PullRequest build.")
-		return nil
-	}
 	resultSet, err := checkResultSet(ctx, r, opt, isProject)
 	if err != nil {
 		return err
 	}
-	cli, err := newDoghouseCli(ctx)
-	if err != nil {
-		return err
+	cli := newDoghouseCli(ctx)
+	if cli == nil {
+		return errors.New("failed to create a doghouse client")
 	}
-	filteredResultSet, err := postResultSet(ctx, resultSet, ghInfo, cli, opt, useAnnotationReporter)
-	if err != nil {
+	if err := postResultSet(ctx, resultSet, ghInfo, cli, opt); err != nil {
 		return err
 	}
-	if foundResultShouldReport := reportResults(w, filteredResultSet, useAnnotationReporter); foundResultShouldReport {
-		return errors.New("found at least one result in diff")
-	}
 	return nil
 }
 
-func newDoghouseCli(ctx context.Context) (client.DogHouseClientInterface, error) {
-	// If skipDoghouseServer is true, run doghouse code directly instead of talking to
-	// the doghouse server because provided GitHub API Token has Check API scope.
-	// You can force skipping the doghouse server if you are generating your own application API token.
-	skipDoghouseServer := (os.Getenv("REVIEWDOG_SKIP_DOGHOUSE") == "true" || cienv.IsInGitHubAction()) && os.Getenv("REVIEWDOG_TOKEN") == ""
-	if skipDoghouseServer {
-		token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
-		if err != nil {
-			return nil, err
-		}
-		ghcli, err := githubClient(ctx, token)
-		if err != nil {
-			return nil, err
-		}
-		return &client.GitHubClient{Client: ghcli}, nil
-	}
-	return newDoghouseServerCli(ctx), nil
+// If skipDoghouseServer is true, reviewdog won't talk to the doghouse server
+// because provided GitHub API Token has Check API scope.
+// You can force skipping the doghouse server if you are generating your own
+// application API token.
+func skipDoghouseServer() bool {
+	return (os.Getenv("REVIEWDOG_SKIP_DOGHOUSE") == "true" || cienv.IsInGitHubAction()) && os.Getenv("REVIEWDOG_TOKEN") == ""
 }
 
-func newDoghouseServerCli(ctx context.Context) *client.DogHouseClient {
+func newDoghouseCli(ctx context.Context) *client.DogHouseClient {
 	httpCli := http.DefaultClient
 	if token := os.Getenv("REVIEWDOG_TOKEN"); token != "" {
 		ts := oauth2.StaticTokenSource(
@@ -112,14 +91,13 @@ func checkResultSet(ctx context.Context, r io.Reader, opt *option, isProject boo
 }
 
 func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap,
-	ghInfo *cienv.BuildInfo, cli client.DogHouseClientInterface, opt *option, useAnnotationReporter bool) (*reviewdog.FilteredResultMap, error) {
+	ghInfo *cienv.BuildInfo, cli *client.DogHouseClient, opt *option) error {
 	var g errgroup.Group
 	wd, _ := os.Getwd()
 	gitRelWd, err := serviceutil.GitRelWorkdir()
 	if err != nil {
-		return nil, err
+		return err
 	}
-	filteredResultSet := new(reviewdog.FilteredResultMap)
 	resultSet.Range(func(name string, result *reviewdog.Result) {
 		diagnostics := result.Diagnostics
 		as := make([]*doghouse.Annotation, 0, len(diagnostics))
@@ -142,7 +120,7 @@ func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap,
 				return err
 			}
 
-			res, err := cli.Check(ctx, req, !useAnnotationReporter)
+			res, err := cli.Check(ctx, req)
 
 			if err != nil {
 				return fmt.Errorf("post failed for %s: %w", name, err)
@@ -153,15 +131,8 @@ func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap,
 					conclusion = fmt.Sprintf(" (conclusion=%s)", res.Conclusion)
 				}
 				log.Printf("[%s] reported: %s%s", name, res.ReportURL, conclusion)
-			} else if res.CheckedResults != nil {
-				// Fill results only when report URL is missing, which probably means
-				// it failed to report results with Check API.
-				filteredResultSet.Store(name, &reviewdog.FilteredResult{
-					Level:              result.Level,
-					FilteredDiagnostic: res.CheckedResults,
-				})
 			}
-			if res.ReportURL == "" && res.CheckedResults == nil {
+			if res.ReportURL == "" {
 				return fmt.Errorf("[%s] no result found", name)
 			}
 			// If failOnError is on, return error when at least one report
@@ -177,7 +148,7 @@ func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap,
 			return nil
 		})
 	})
-	return filteredResultSet, g.Wait()
+	return g.Wait()
 }
 
 func checkResultToAnnotation(d *rdf.Diagnostic, wd, gitRelWd string) *doghouse.Annotation {
@@ -186,67 +157,3 @@ func checkResultToAnnotation(d *rdf.Diagnostic, wd, gitRelWd string) *doghouse.A
 		Diagnostic: d,
 	}
 }
-
-// reportResults reports results to given io.Writer and possibly to GitHub
-// Actions log using logging command.
-//
-// It returns true if reviewdog should exit with 1.
-// e.g. At least one annotation result is in diff.
-func reportResults(w io.Writer, filteredResultSet *reviewdog.FilteredResultMap, useAnnotationReporter bool) bool {
-	if filteredResultSet.Len() != 0 && cienv.HasReadOnlyPermissionGitHubToken() {
-		fmt.Fprintln(os.Stderr, `reviewdog: This GitHub token doesn't have write permission of Review API [1], 
-so reviewdog will report results via logging command [2] and create annotations similar to
-github-pr-check reporter as a fallback.
-[1]: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target, 
-[2]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands`)
-	}
-
-	// Sort names to get deterministic result.
-	var names []string
-	filteredResultSet.Range(func(name string, results *reviewdog.FilteredResult) {
-		names = append(names, name)
-	})
-	sort.Strings(names)
-
-	shouldFail := false
-	foundNumOverall := 0
-	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, "reviewdog: Reporting results for %q\n", name)
-		foundResultPerName := false
-		filteredNum := 0
-		for _, result := range results.FilteredDiagnostic {
-			if !result.ShouldReport {
-				filteredNum++
-				continue
-			}
-			foundNumOverall++
-			// If it's not running in GitHub Actions and not using the annotation reporter, reviewdog should exit with 1
-			// if there are at least one result in diff regardless of error level.
-			shouldFail = shouldFail || (!cienv.IsInGitHubAction() && !useAnnotationReporter) ||
-				!(results.Level == "warning" || results.Level == "info")
-
-			if foundNumOverall == githubutils.MaxLoggingAnnotationsPerStep {
-				githubutils.WarnTooManyAnnotationOnce()
-				shouldFail = true
-			}
-
-			foundResultPerName = true
-			if cienv.IsInGitHubAction() || useAnnotationReporter {
-				githubutils.ReportAsGitHubActionsLog(name, results.Level, result.Diagnostic)
-			} else {
-				// Output original lines.
-				fmt.Fprintln(w, result.Diagnostic.GetOriginalOutput())
-			}
-		}
-		if !foundResultPerName {
-			fmt.Fprintf(w, "reviewdog: No results found for %q. %d results found outside diff.\n", name, filteredNum)
-		}
-	}
-	return shouldFail
-}
diff --git a/cmd/reviewdog/main.go b/cmd/reviewdog/main.go
index c47de487..8ddcef63 100644
--- a/cmd/reviewdog/main.go
+++ b/cmd/reviewdog/main.go
@@ -35,6 +35,7 @@ import (
 	gerritservice "github.com/reviewdog/reviewdog/service/gerrit"
 	giteaservice "github.com/reviewdog/reviewdog/service/gitea"
 	githubservice "github.com/reviewdog/reviewdog/service/github"
+	"github.com/reviewdog/reviewdog/service/github/githubutils"
 	gitlabservice "github.com/reviewdog/reviewdog/service/gitlab"
 )
 
@@ -75,7 +76,7 @@ const (
 
 	confDoc             = `config file path`
 	runnersDoc          = `comma separated runners name to run in config file. default: run all runners`
-	levelDoc            = `report level currently used for github-pr-check reporter ("info","warning","error").`
+	levelDoc            = `default report level for supported reporters ("info","warning","error").`
 	guessPullRequestDoc = `guess Pull Request ID by branch name and commit SHA`
 	teeDoc              = `enable "tee"-like mode which outputs tools's output as is while reporting results to -reporter. Useful for debugging as well.`
 	filterModeDoc       = `how to filter checks results. [added, diff_context, file, nofilter].
@@ -299,12 +300,32 @@ func run(r io.Reader, w io.Writer, opt *option) error {
 	switch opt.reporter {
 	default:
 		return fmt.Errorf("unknown -reporter: %s", opt.reporter)
-	case "github-check":
-		return runDoghouse(ctx, r, w, opt, isProject, false, false)
-	case "github-pr-check":
-		return runDoghouse(ctx, r, w, opt, isProject, true, false)
+	case "github-check", "github-pr-check":
+		if !skipDoghouseServer() {
+			return runDoghouse(ctx, r, w, opt, isProject)
+		}
+		var err error
+		cs, ds, err = githubCheckService(ctx, opt)
+		if err != nil {
+			return err
+		}
 	case "github-pr-annotations":
-		return runDoghouse(ctx, r, w, opt, isProject, true, true)
+		g, client, err := githubBuildInfoWithClient(ctx)
+		if err != nil {
+			return err
+		}
+		ds = &reviewdog.EmptyDiff{}
+		if g.PullRequest != 0 {
+			ds = &githubservice.PullRequestDiffService{
+				Cli:              client,
+				Owner:            g.Owner,
+				Repo:             g.Repo,
+				PR:               g.PullRequest,
+				SHA:              g.SHA,
+				FallBackToGitCLI: true,
+			}
+		}
+		cs = githubutils.NewGitHubActionLogWriter(opt.level)
 	case "github-pr-review":
 		gs, isPR, err := githubService(ctx, opt)
 		if err != nil {
@@ -589,21 +610,11 @@ func giteaClient(ctx context.Context, url, token string) (*gitea.Client, error)
 }
 
 func githubService(ctx context.Context, opt *option) (gs *githubservice.PullRequest, isPR bool, err error) {
-	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
+	g, client, err := githubBuildInfoWithClient(ctx)
 	if err != nil {
-		return nil, isPR, err
-	}
-	g, isPR, err := cienv.GetBuildInfo()
-	if err != nil {
-		return nil, isPR, err
-	}
-
-	client, err := githubClient(ctx, token)
-	if err != nil {
-		return nil, isPR, err
+		return nil, false, err
 	}
-
-	if !isPR {
+	if g.PullRequest == 0 {
 		if !opt.guessPullRequest {
 			return nil, false, nil
 		}
@@ -627,6 +638,56 @@ func githubService(ctx context.Context, opt *option) (gs *githubservice.PullRequ
 	return gs, true, nil
 }
 
+func githubCheckService(ctx context.Context, opt *option) (reviewdog.CommentService, reviewdog.DiffService, error) {
+	g, client, err := githubBuildInfoWithClient(ctx)
+	if err != nil {
+		return nil, nil, err
+	}
+	var ds reviewdog.DiffService = &reviewdog.EmptyDiff{}
+	if g.PullRequest != 0 {
+		ds = &githubservice.PullRequestDiffService{
+			Cli:              client,
+			Owner:            g.Owner,
+			Repo:             g.Repo,
+			PR:               g.PullRequest,
+			SHA:              g.SHA,
+			FallBackToGitCLI: true,
+		}
+	}
+	return &githubservice.Check{
+		CLI:      client,
+		Owner:    g.Owner,
+		Repo:     g.Repo,
+		PR:       g.PullRequest,
+		SHA:      g.SHA,
+		ToolName: opt.name,
+		Level:    opt.level,
+	}, ds, nil
+}
+
+func githubBuildInfoWithClient(ctx context.Context) (*cienv.BuildInfo, *github.Client, error) {
+	token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
+	if err != nil {
+		return nil, nil, err
+	}
+	g, isPR, err := cienv.GetBuildInfo()
+	if err != nil {
+		return nil, nil, err
+	}
+	client, err := githubClient(ctx, token)
+	if err != nil {
+		return nil, nil, err
+	}
+	if !isPR && opt.guessPullRequest {
+		prID, err := getPullRequestIDByBranchOrCommit(ctx, client, g)
+		if err != nil {
+			return nil, nil, err
+		}
+		g.PullRequest = prID
+	}
+	return g, client, nil
+}
+
 func getPullRequestIDByBranchOrCommit(ctx context.Context, client *github.Client, info *cienv.BuildInfo) (int, error) {
 	options := &github.SearchOptions{
 		Sort:  "updated",
diff --git a/doghouse/appengine/checker.go b/doghouse/appengine/checker.go
index a3926b61..98e5af59 100644
--- a/doghouse/appengine/checker.go
+++ b/doghouse/appengine/checker.go
@@ -59,7 +59,7 @@ func (gc *githubChecker) handleCheck(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	res, err := server.NewChecker(&req, gh, true /* inDogHouseServer */).Check(ctx, true)
+	res, err := server.NewChecker(&req, gh, true /* inDogHouseServer */).Check(ctx)
 	if err != nil {
 		aelog.Errorf(ctx, "failed to run checker: %v", err)
 		w.WriteHeader(http.StatusBadRequest)
diff --git a/doghouse/client/client.go b/doghouse/client/client.go
index 55b8d33d..89f1793a 100644
--- a/doghouse/client/client.go
+++ b/doghouse/client/client.go
@@ -17,11 +17,6 @@ import (
 
 const baseEndpoint = "https://reviewdog.app"
 
-// DogHouseClientInterface is interface for doghouse client.
-type DogHouseClientInterface interface {
-	Check(ctx context.Context, req *doghouse.CheckRequest, sendRequest bool) (*doghouse.CheckResponse, error)
-}
-
 // DogHouseClient is client for doghouse server.
 type DogHouseClient struct {
 	Client *http.Client
@@ -48,17 +43,16 @@ func New(client *http.Client) *DogHouseClient {
 }
 
 // Check send check requests to doghouse.
-func (c *DogHouseClient) Check(ctx context.Context, req *doghouse.CheckRequest, sendRequest bool) (*doghouse.CheckResponse, error) {
+func (c *DogHouseClient) Check(ctx context.Context, req *doghouse.CheckRequest) (*doghouse.CheckResponse, error) {
 	checkURL := c.BaseURL.String() + "/check"
 	b, err := json.Marshal(req)
 	if err != nil {
 		return nil, err
 	}
-	httpReq, err := http.NewRequest(http.MethodPost, checkURL, bytes.NewReader(b))
+	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, checkURL, bytes.NewReader(b))
 	if err != nil {
 		return nil, err
 	}
-	httpReq = httpReq.WithContext(ctx)
 	httpReq.Header.Set("Content-Type", "application/json")
 	httpReq.Header.Set("User-Agent", fmt.Sprintf("reviewdog/%s", commands.Version))
 
diff --git a/doghouse/client/github_client.go b/doghouse/client/github_client.go
deleted file mode 100644
index 2a9b80d1..00000000
--- a/doghouse/client/github_client.go
+++ /dev/null
@@ -1,20 +0,0 @@
-package client
-
-import (
-	"context"
-
-	"github.com/google/go-github/v60/github"
-
-	"github.com/reviewdog/reviewdog/doghouse"
-	"github.com/reviewdog/reviewdog/doghouse/server"
-)
-
-// GitHubClient is client which talks to GitHub directly instead of talking to
-// doghouse server.
-type GitHubClient struct {
-	Client *github.Client
-}
-
-func (c *GitHubClient) Check(ctx context.Context, req *doghouse.CheckRequest, makeRequest bool) (*doghouse.CheckResponse, error) {
-	return server.NewChecker(req, c.Client, false /* inDogHouseServer */).Check(ctx, makeRequest)
-}
diff --git a/doghouse/server/doghouse.go b/doghouse/server/doghouse.go
index 693604ff..33230665 100644
--- a/doghouse/server/doghouse.go
+++ b/doghouse/server/doghouse.go
@@ -3,49 +3,35 @@ package server
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
-	"net/http"
-	"strings"
-	"time"
 
 	"github.com/google/go-github/v60/github"
 
+	"github.com/reviewdog/reviewdog"
 	"github.com/reviewdog/reviewdog/diff"
 	"github.com/reviewdog/reviewdog/doghouse"
 	"github.com/reviewdog/reviewdog/filter"
 	"github.com/reviewdog/reviewdog/proto/rdf"
-	"github.com/reviewdog/reviewdog/service/github/githubutils"
+	ghService "github.com/reviewdog/reviewdog/service/github"
 )
 
-// GitHub check runs API cannot handle too large requests.
-// Set max number of filtered findings to be shown in check-run summary.
-// ERROR:
-//
-//	https://api.github.com/repos/easymotion/vim-easymotion/check-runs: 422
-//	Invalid request.
-//	Only 65535 characters are allowed; 250684 were supplied. []
-const maxAllowedSize = 65535
-
-// > The Checks API limits the number of annotations to a maximum of 50 per API
-// > request.
-// https://developer.github.com/v3/checks/runs/#output-object
-const maxAnnotationsPerRequest = 50
-
 type Checker struct {
 	req              *doghouse.CheckRequest
-	gh               checkerGitHubClientInterface
+	cli              *github.Client
 	inDogHouseServer bool // If true, this checker runs in the DogHouse server.
 }
 
 func NewChecker(req *doghouse.CheckRequest, gh *github.Client, inDogHouseServer bool) *Checker {
 	return &Checker{
 		req:              req,
-		gh:               &checkerGitHubClient{Client: gh},
+		cli:              gh,
 		inDogHouseServer: inDogHouseServer,
 	}
 }
 
-func (ch *Checker) Check(ctx context.Context, makeRequest bool) (*doghouse.CheckResponse, error) {
+func (ch *Checker) Check(ctx context.Context) (*doghouse.CheckResponse, error) {
+	// Get Diff
 	var filediffs []*diff.FileDiff
 	if ch.req.PullRequest != 0 {
 		var err error
@@ -55,7 +41,10 @@ func (ch *Checker) Check(ctx context.Context, makeRequest bool) (*doghouse.Check
 		}
 	}
 
+	// Convert annotations
 	results := annotationsToDiagnostics(ch.req.Annotations)
+
+	// Filter results
 	filterMode := ch.req.FilterMode
 	//lint:ignore SA1019 Need to support OutsideDiff for backward compatibility.
 	if ch.req.PullRequest == 0 || ch.req.OutsideDiff {
@@ -65,281 +54,35 @@ func (ch *Checker) Check(ctx context.Context, makeRequest bool) (*doghouse.Check
 	}
 	filtered := filter.FilterCheck(results, filediffs, 1, "", filterMode)
 
-	// If we're not making a request, we can just return the filtered results.
-	if !makeRequest {
-		return &doghouse.CheckResponse{CheckedResults: filtered}, nil
-	}
-
-	var check *github.CheckRun
-	var err error
-
-	check, err = ch.createCheck(ctx)
-	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
+	// Post annotations
+	checkService := &ghService.Check{
+		CLI:      ch.cli,
+		Owner:    ch.req.Owner,
+		Repo:     ch.req.Repo,
+		PR:       ch.req.PullRequest,
+		SHA:      ch.req.SHA,
+		ToolName: ch.req.Name,
+		Level:    ch.req.Level,
+	}
+	for _, f := range filtered {
+		if err := checkService.Post(ctx, &reviewdog.Comment{
+			Result:   f,
+			ToolName: ch.req.Name,
+		}); err != nil {
+			return nil, err
 		}
-		return nil, fmt.Errorf("failed to create check: %w", err)
-	}
-
-	checkRun, conclusion, err := ch.postCheck(ctx, check.GetID(), filtered)
-	if err != nil {
-		return nil, fmt.Errorf("failed to post result: %w", err)
-	}
-	res := &doghouse.CheckResponse{
-		ReportURL:  checkRun.GetHTMLURL(),
-		Conclusion: conclusion,
 	}
-	return res, nil
-}
-
-func (ch *Checker) postCheck(ctx context.Context, checkID int64, checks []*filter.FilteredDiagnostic) (*github.CheckRun, string, error) {
-	var annotations []*github.CheckRunAnnotation
-	for _, c := range checks {
-		if !c.ShouldReport {
-			continue
-		}
-		annotations = append(annotations, ch.toCheckRunAnnotation(c))
-	}
-	if len(annotations) > 0 {
-		if err := ch.postAnnotations(ctx, checkID, annotations); err != nil {
-			return nil, "", fmt.Errorf("failed to post annotations: %w", err)
-		}
-	}
-
-	conclusion := ch.conclusion(annotations)
-	opt := github.UpdateCheckRunOptions{
-		Name:        ch.checkName(),
-		Status:      github.String("completed"),
-		Conclusion:  github.String(conclusion),
-		CompletedAt: &github.Timestamp{Time: time.Now()},
-		Output: &github.CheckRunOutput{
-			Title:   github.String(ch.checkTitle()),
-			Summary: github.String(ch.summary(checks)),
-		},
-	}
-	checkRun, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt)
-	if err != nil {
-		return nil, "", err
-	}
-	return checkRun, conclusion, nil
-}
-
-func (ch *Checker) createCheck(ctx context.Context) (*github.CheckRun, error) {
-	opt := github.CreateCheckRunOptions{
-		Name:    ch.checkName(),
-		HeadSHA: ch.req.SHA,
-		Status:  github.String("in_progress"),
-	}
-	return ch.gh.CreateCheckRun(ctx, ch.req.Owner, ch.req.Repo, opt)
-}
-
-func (ch *Checker) postAnnotations(ctx context.Context, checkID int64, annotations []*github.CheckRunAnnotation) error {
-	opt := github.UpdateCheckRunOptions{
-		Name: ch.checkName(),
-		Output: &github.CheckRunOutput{
-			Title:       github.String(ch.checkTitle()),
-			Summary:     github.String(""), // Post summary with the last request.
-			Annotations: annotations[:min(maxAnnotationsPerRequest, len(annotations))],
-		},
-	}
-	if _, err := ch.gh.UpdateCheckRun(ctx, ch.req.Owner, ch.req.Repo, checkID, opt); err != nil {
-		return err
-	}
-	if len(annotations) > maxAnnotationsPerRequest {
-		return ch.postAnnotations(ctx, checkID, annotations[maxAnnotationsPerRequest:])
-	}
-	return nil
-}
-
-func (ch *Checker) checkName() string {
-	if ch.req.Name != "" {
-		return ch.req.Name
-	}
-	return "reviewdog"
-}
-
-func (ch *Checker) checkTitle() string {
-	if name := ch.checkName(); name != "reviewdog" {
-		return fmt.Sprintf("reviewdog [%s] report", name)
-	}
-	return "reviewdog report"
-}
-
-// https://developer.github.com/v3/checks/runs/#parameters-1
-func (ch *Checker) conclusion(annotations []*github.CheckRunAnnotation) string {
-	checkResult := "success"
-
-	if ch.req.Level != "" {
-		// Level takes precedence when configured (for backwards compatibility)
-		if len(annotations) == 0 {
-			return checkResult
-		}
-		switch strings.ToLower(ch.req.Level) {
-		case "info", "warning":
-			return "neutral"
-		}
-		return "failure"
-	} else {
-		precedence := map[string]int{
-			"success": 0,
-			"notice":  1,
-			"warning": 2,
-			"failure": 3,
-		}
-
-		highestLevel := "success"
-		for _, a := range annotations {
-			annotationLevel := *a.AnnotationLevel
-			if precedence[annotationLevel] > precedence[highestLevel] {
-				highestLevel = annotationLevel
-			}
-		}
-		checkResult = highestLevel
-	}
-
-	switch checkResult {
-	case "success":
-		return "success"
-	case "notice", "warning":
-		return "neutral"
-	}
-	return "failure"
-}
-
-// https://developer.github.com/v3/checks/runs/#annotations-object
-func (ch *Checker) annotationLevel(s rdf.Severity) string {
-	switch s {
-	case rdf.Severity_ERROR:
-		return "failure"
-	case rdf.Severity_WARNING:
-		return "warning"
-	case rdf.Severity_INFO:
-		return "notice"
-	default:
-		return ch.reqAnnotationLevel()
-	}
-}
-
-func (ch *Checker) reqAnnotationLevel() string {
-	switch strings.ToLower(ch.req.Level) {
-	case "info":
-		return "notice"
-	case "warning":
-		return "warning"
-	case "failure":
-		return "failure"
-	}
-	return "failure"
-}
-
-func (ch *Checker) summary(checks []*filter.FilteredDiagnostic) string {
-	var lines []string
-	var usedBytes int
-	lines = append(lines, "reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:")
-	usedBytes += len(lines[0]) + 1
-	var findings []*filter.FilteredDiagnostic
-	var filteredFindings []*filter.FilteredDiagnostic
-	for _, c := range checks {
-		if c.ShouldReport {
-			findings = append(findings, c)
-		} else {
-			filteredFindings = append(filteredFindings, c)
-		}
-	}
-
-	findingMsgs, usedBytes := ch.summaryFindings("Findings", usedBytes, findings)
-	lines = append(lines, findingMsgs...)
-	filteredFindingsMsgs, _ := ch.summaryFindings("Filtered Findings", usedBytes, filteredFindings)
-	lines = append(lines, filteredFindingsMsgs...)
-	return strings.Join(lines, "\n")
-}
-
-func (ch *Checker) summaryFindings(name string, usedBytes int, checks []*filter.FilteredDiagnostic) ([]string, int) {
-	var lines []string
-	lines = append(lines, fmt.Sprintf("<details>\n<summary>%s (%d)</summary>\n", name, len(checks)))
-	if len(lines[0])+1+usedBytes > maxAllowedSize {
-		// bail out if we're already over the limit
-		return nil, usedBytes
-	}
-	usedBytes += len(lines[0]) + 1
-	for _, c := range checks {
-		nextLine := githubutils.LinkedMarkdownDiagnostic(ch.req.Owner, ch.req.Repo, ch.req.SHA, c.Diagnostic)
-		// existing lines + newline + closing details tag must be smaller than the max allowed size
-		if usedBytes+len(nextLine)+1+10 >= maxAllowedSize {
-			cutoffMsg := "... (Too many findings. Dropped some findings)"
-			if usedBytes+len(cutoffMsg)+1+10 <= maxAllowedSize {
-				lines = append(lines, cutoffMsg)
-				usedBytes += len(cutoffMsg) + 1
-			}
-			break
-		}
-		lines = append(lines, nextLine)
-		usedBytes += len(nextLine) + 1
-	}
-	lines = append(lines, "</details>")
-	usedBytes += 10 + 1
-	return lines, usedBytes
-}
-
-func (ch *Checker) toCheckRunAnnotation(c *filter.FilteredDiagnostic) *github.CheckRunAnnotation {
-	loc := c.Diagnostic.GetLocation()
-	startLine := int(loc.GetRange().GetStart().GetLine())
-	endLine := int(loc.GetRange().GetEnd().GetLine())
-	if endLine == 0 {
-		endLine = startLine
-	}
-	a := &github.CheckRunAnnotation{
-		Path:            github.String(loc.GetPath()),
-		StartLine:       github.Int(startLine),
-		EndLine:         github.Int(endLine),
-		AnnotationLevel: github.String(ch.annotationLevel(c.Diagnostic.Severity)),
-		Message:         github.String(c.Diagnostic.GetMessage()),
-		Title:           github.String(ch.buildTitle(c)),
-	}
-	// Annotations only support start_column and end_column on the same line.
-	if startLine == endLine {
-		if s, e := loc.GetRange().GetStart().GetColumn(), loc.GetRange().GetEnd().GetColumn(); s != 0 && e != 0 {
-			a.StartColumn = github.Int(int(s))
-			a.EndColumn = github.Int(int(e))
-		}
-	}
-	if s := c.Diagnostic.GetOriginalOutput(); s != "" {
-		a.RawDetails = github.String(s)
-	}
-	return a
-}
-
-func (ch *Checker) buildTitle(c *filter.FilteredDiagnostic) string {
-	var sb strings.Builder
-	toolName := c.Diagnostic.GetSource().GetName()
-	if toolName == "" {
-		toolName = ch.req.Name
-	}
-	if toolName != "" {
-		sb.WriteString(fmt.Sprintf("[%s] ", toolName))
-	}
-	loc := c.Diagnostic.GetLocation()
-	sb.WriteString(loc.GetPath())
-	if startLine := int(loc.GetRange().GetStart().GetLine()); startLine > 0 {
-		sb.WriteString(fmt.Sprintf("#L%d", startLine))
-		if endLine := int(loc.GetRange().GetEnd().GetLine()); startLine < endLine {
-			sb.WriteString(fmt.Sprintf("-L%d", endLine))
-		}
+	if err := checkService.Flush(ctx); err != nil {
+		return nil, err
 	}
-	if code := c.Diagnostic.GetCode().GetValue(); code != "" {
-		if url := c.Diagnostic.GetCode().GetUrl(); url != "" {
-			sb.WriteString(fmt.Sprintf(" <%s>(%s)", code, url))
-		} else {
-			sb.WriteString(fmt.Sprintf(" <%s>", code))
-		}
+	result := checkService.GetResult()
+	if result == nil {
+		return nil, errors.New("empty check service result")
 	}
-	return sb.String()
+	return &doghouse.CheckResponse{
+		ReportURL:  result.ReportURL,
+		Conclusion: result.Conclusion,
+	}, nil
 }
 
 func (ch *Checker) pullRequestDiff(ctx context.Context, pr int) ([]*diff.FileDiff, error) {
@@ -355,12 +98,14 @@ func (ch *Checker) pullRequestDiff(ctx context.Context, pr int) ([]*diff.FileDif
 }
 
 func (ch *Checker) rawPullRequestDiff(ctx context.Context, pr int) ([]byte, error) {
-	fallbackToGitCLI := !ch.inDogHouseServer
-	d, err := ch.gh.GetPullRequestDiff(ctx, ch.req.Owner, ch.req.Repo, pr, fallbackToGitCLI)
-	if err != nil {
-		return nil, err
-	}
-	return d, nil
+	return (&ghService.PullRequestDiffService{
+		Cli:              ch.cli,
+		Owner:            ch.req.Owner,
+		Repo:             ch.req.Repo,
+		PR:               pr,
+		SHA:              ch.req.SHA,
+		FallBackToGitCLI: !ch.inDogHouseServer,
+	}).Diff(ctx)
 }
 
 func annotationsToDiagnostics(as []*doghouse.Annotation) []*rdf.Diagnostic {
diff --git a/doghouse/server/github_checker.go b/doghouse/server/github_checker.go
deleted file mode 100644
index 82b7b6af..00000000
--- a/doghouse/server/github_checker.go
+++ /dev/null
@@ -1,102 +0,0 @@
-package server
-
-import (
-	"context"
-	"fmt"
-	"io"
-	"log"
-	"net/http"
-	"os"
-	"os/exec"
-	"time"
-
-	"github.com/google/go-github/v60/github"
-	"github.com/reviewdog/reviewdog/service/serviceutil"
-	"github.com/vvakame/sdlog/aelog"
-)
-
-type checkerGitHubClientInterface interface {
-	GetPullRequestDiff(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error)
-	CreateCheckRun(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error)
-	UpdateCheckRun(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error)
-}
-
-type checkerGitHubClient struct {
-	*github.Client
-}
-
-func (c *checkerGitHubClient) GetPullRequestDiff(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-	opt := github.RawOptions{Type: github.Diff}
-	d, resp, err := c.PullRequests.GetRaw(ctx, owner, repo, number, opt)
-	if err != nil {
-		if fallbackToGitCLI && resp != nil && resp.StatusCode == http.StatusNotAcceptable && serviceutil.GitCommandExists() {
-			log.Print("fallback to use git command")
-			return c.getPullRequestDiffUsingGitCommand(ctx, owner, repo, number)
-		}
-
-		return nil, err
-	}
-	return []byte(d), err
-}
-
-// getPullRequestDiffUsingGitCommand returns a diff of PullRequest using git command.
-func (c *checkerGitHubClient) getPullRequestDiffUsingGitCommand(ctx context.Context, owner, repo string, number int) ([]byte, error) {
-	pr, _, err := c.PullRequests.Get(ctx, owner, repo, number)
-	if err != nil {
-		return nil, err
-	}
-
-	head := pr.GetHead()
-	headSha := head.GetSHA()
-
-	commitsComparison, _, err := c.Repositories.CompareCommits(ctx, owner, repo, headSha, pr.GetBase().GetSHA(), nil)
-	if err != nil {
-		return nil, err
-	}
-
-	mergeBaseSha := commitsComparison.GetMergeBaseCommit().GetSHA()
-
-	if os.Getenv("REVIEWDOG_SKIP_GIT_FETCH") != "true" {
-		for _, sha := range []string{mergeBaseSha, headSha} {
-			_, err := exec.Command("git", "fetch", "--depth=1", head.GetRepo().GetHTMLURL(), sha).CombinedOutput()
-			if err != nil {
-				return nil, fmt.Errorf("failed to run git fetch: %w", err)
-			}
-		}
-	}
-
-	bytes, err := exec.Command("git", "diff", "--find-renames", mergeBaseSha, headSha).CombinedOutput()
-	if err != nil {
-		return nil, fmt.Errorf("failed to run git diff: %w", err)
-	}
-
-	return bytes, nil
-}
-
-func (c *checkerGitHubClient) CreateCheckRun(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-	checkRun, _, err := c.Checks.CreateCheckRun(ctx, owner, repo, opt)
-	return checkRun, err
-}
-
-func (c *checkerGitHubClient) UpdateCheckRun(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
-	// Retry requests because GitHub API somehow returns 401 Bad credentials from
-	// time to time...
-	var err error
-	for i := 0; i < 5; i++ {
-		checkRun, resp, err1 := c.Checks.UpdateCheckRun(ctx, owner, repo, checkID, opt)
-		if err1 != nil {
-			err = err1
-			b, err1 := io.ReadAll(resp.Body)
-			if err1 != nil {
-				aelog.Errorf(ctx, "failed to read error response body: %v", err1)
-			}
-			aelog.Errorf(ctx, "UpdateCheckRun failed: %s", string(b))
-			aelog.Debugf(ctx, "Retrying UpdateCheckRun...: %d", i+1)
-			time.Sleep(time.Second)
-			continue
-		}
-		return checkRun, nil
-	}
-
-	return nil, err
-}
diff --git a/doghouse/service.go b/doghouse/service.go
index 21569082..a1d52b90 100644
--- a/doghouse/service.go
+++ b/doghouse/service.go
@@ -57,16 +57,6 @@ type CheckResponse struct {
 	// 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 doesn'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 []*filter.FilteredDiagnostic `json:"checked_results"`
-
 	// Conclusion of check result, which is same as GitHub's conclusion of Check
 	// API. https://developer.github.com/v3/checks/runs/#parameters-1
 	Conclusion string `json:"conclusion,omitempty"`
diff --git a/go.mod b/go.mod
index 0f2fe7c6..b4830a88 100644
--- a/go.mod
+++ b/go.mod
@@ -54,7 +54,7 @@ require (
 	github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
 	github.com/googleapis/gax-go/v2 v2.12.4 // indirect
 	github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
-	github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
+	github.com/hashicorp/go-retryablehttp v0.7.2 // indirect
 	github.com/hashicorp/go-version v1.6.0 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index d52543c2..45c173e4 100644
--- a/go.sum
+++ b/go.sum
@@ -98,8 +98,6 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
 github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
-github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
 github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
@@ -206,10 +204,10 @@ github.com/googleapis/gax-go/v2 v2.12.4/go.mod h1:KYEYLorsnIGDi/rPC8b5TdlB9kbKoF
 github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
-github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
-github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
-github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
-github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
+github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
+github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
+github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
+github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
 github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
 github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@@ -240,10 +238,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
-github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
-github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
-github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
-github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
 github.com/mattn/go-shellwords v1.0.12 h1:M2zGm7EW6UQJvDeQxo4T51eKPurbeFbe8WtebGE2xrk=
 github.com/mattn/go-shellwords v1.0.12/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y=
 github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -268,6 +262,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
 github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
diff --git a/reviewdog.go b/reviewdog.go
index 21746105..ed324275 100644
--- a/reviewdog.go
+++ b/reviewdog.go
@@ -47,6 +47,12 @@ type CommentService interface {
 	Post(context.Context, *Comment) error
 }
 
+// FilteredCommentService is an interface which support posting filtered Comment.
+type FilteredCommentService interface {
+	CommentService
+	PostFiltered(context.Context, *Comment) error
+}
+
 // BulkCommentService posts comments all at once when Flush() is called.
 // Flush() will be called at the end of reviewdog run.
 type BulkCommentService interface {
@@ -71,17 +77,24 @@ func (w *Reviewdog) runFromResult(ctx context.Context, results []*rdf.Diagnostic
 	hasViolations := false
 
 	for _, check := range checks {
-		if !check.ShouldReport {
-			continue
-		}
 		comment := &Comment{
 			Result:   check,
 			ToolName: w.toolname,
 		}
-		if err := w.c.Post(ctx, comment); err != nil {
-			return err
+		if !check.ShouldReport {
+			if fc, ok := w.c.(FilteredCommentService); ok {
+				if err := fc.PostFiltered(ctx, comment); err != nil {
+					return err
+				}
+			} else {
+				continue
+			}
+		} else {
+			if err := w.c.Post(ctx, comment); err != nil {
+				return err
+			}
+			hasViolations = true
 		}
-		hasViolations = true
 	}
 
 	if bulk, ok := w.c.(BulkCommentService); ok {
diff --git a/service/github/check.go b/service/github/check.go
new file mode 100644
index 00000000..ad01f225
--- /dev/null
+++ b/service/github/check.go
@@ -0,0 +1,360 @@
+package github
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/google/go-github/v60/github"
+	"github.com/reviewdog/reviewdog"
+	"github.com/reviewdog/reviewdog/cienv"
+	"github.com/reviewdog/reviewdog/filter"
+	"github.com/reviewdog/reviewdog/proto/rdf"
+	"github.com/reviewdog/reviewdog/service/github/githubutils"
+)
+
+// GitHub check runs API cannot handle too large requests.
+// Set max number of filtered findings to be shown in check-run summary.
+// ERROR:
+//
+//	https://api.github.com/repos/easymotion/vim-easymotion/check-runs: 422
+//	Invalid request.
+//	Only 65535 characters are allowed; 250684 were supplied. []
+const maxAllowedSize = 65535
+
+// > The Checks API limits the number of annotations to a maximum of 50 per API
+// > request.
+// https://developer.github.com/v3/checks/runs/#output-object
+const maxAnnotationsPerRequest = 50
+
+var _ reviewdog.CommentService = (*Check)(nil)
+
+// Check is a CommentService which uses GitHub Check API.
+type Check struct {
+	CLI      *github.Client
+	Owner    string
+	Repo     string
+	PR       int // optional
+	SHA      string
+	ToolName string
+	Level    string
+
+	muComments   sync.Mutex
+	postComments []*reviewdog.Comment
+
+	muResult sync.Mutex
+	result   *CheckResult
+}
+
+// CheckResult represents result of Check.
+type CheckResult struct {
+	ReportURL  string
+	Conclusion string
+}
+
+// Post posts a reviewdog comment.
+func (ch *Check) Post(_ context.Context, c *reviewdog.Comment) error {
+	ch.muComments.Lock()
+	defer ch.muComments.Unlock()
+	ch.postComments = append(ch.postComments, c)
+	return nil
+}
+
+// PostFiltered posts a filtered reviewdog comment.
+func (ch *Check) PostFiltered(_ context.Context, c *reviewdog.Comment) error {
+	ch.muComments.Lock()
+	defer ch.muComments.Unlock()
+	ch.postComments = append(ch.postComments, c)
+	return nil
+}
+
+// GetResult returns result of Check run.
+func (ch *Check) GetResult() *CheckResult {
+	ch.muResult.Lock()
+	defer ch.muResult.Unlock()
+	return ch.result
+}
+
+// Flush actually posts comments.
+func (ch *Check) Flush(ctx context.Context) error {
+	ch.muComments.Lock()
+	defer ch.muComments.Unlock()
+	check, err := ch.createCheck(ctx)
+	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 && cienv.IsInGitHubAction() {
+			logWriter := githubutils.NewGitHubActionLogWriter(ch.Level)
+			for _, c := range ch.postComments {
+				if err := logWriter.Post(ctx, c); err != nil {
+					return err
+				}
+			}
+			return logWriter.Flush(ctx)
+		}
+		return fmt.Errorf("failed to create check: %w", err)
+	}
+	checkRun, conclusion, err := ch.postCheck(ctx, check.GetID())
+	if err != nil {
+		return fmt.Errorf("failed to post result: %w", err)
+	}
+	ch.muResult.Lock()
+	defer ch.muResult.Unlock()
+	ch.result = &CheckResult{
+		ReportURL:  checkRun.GetHTMLURL(),
+		Conclusion: conclusion,
+	}
+	return nil
+}
+
+func (ch *Check) createCheck(ctx context.Context) (*github.CheckRun, error) {
+	opt := github.CreateCheckRunOptions{
+		Name:    ch.ToolName,
+		HeadSHA: ch.SHA,
+		Status:  github.String("in_progress"),
+	}
+	checkRun, _, err := ch.CLI.Checks.CreateCheckRun(ctx, ch.Owner, ch.Repo, opt)
+	return checkRun, err
+}
+
+func (ch *Check) postCheck(ctx context.Context, checkID int64) (*github.CheckRun, string, error) {
+	var annotations []*github.CheckRunAnnotation
+	for _, c := range ch.postComments {
+		if !c.Result.ShouldReport {
+			continue
+		}
+		annotations = append(annotations, ch.toCheckRunAnnotation(c.Result))
+	}
+	if len(annotations) > 0 {
+		if err := ch.postAnnotations(ctx, checkID, annotations); err != nil {
+			return nil, "", fmt.Errorf("failed to post annotations: %w", err)
+		}
+	}
+
+	conclusion := ch.conclusion(annotations)
+	opt := github.UpdateCheckRunOptions{
+		Name:        ch.checkName(),
+		Status:      github.String("completed"),
+		Conclusion:  github.String(conclusion),
+		CompletedAt: &github.Timestamp{Time: time.Now()},
+		Output: &github.CheckRunOutput{
+			Title:   github.String(ch.checkTitle()),
+			Summary: github.String(ch.summary(ch.postComments)),
+		},
+	}
+	checkRun, _, err := ch.CLI.Checks.UpdateCheckRun(ctx, ch.Owner, ch.Repo, checkID, opt)
+	if err != nil {
+		return nil, "", err
+	}
+	return checkRun, conclusion, nil
+}
+
+func (ch *Check) toCheckRunAnnotation(c *filter.FilteredDiagnostic) *github.CheckRunAnnotation {
+	loc := c.Diagnostic.GetLocation()
+	startLine := int(loc.GetRange().GetStart().GetLine())
+	endLine := int(loc.GetRange().GetEnd().GetLine())
+	if endLine == 0 {
+		endLine = startLine
+	}
+	a := &github.CheckRunAnnotation{
+		Path:            github.String(loc.GetPath()),
+		StartLine:       github.Int(startLine),
+		EndLine:         github.Int(endLine),
+		AnnotationLevel: github.String(ch.annotationLevel(c.Diagnostic.Severity)),
+		Message:         github.String(c.Diagnostic.GetMessage()),
+		Title:           github.String(ch.buildTitle(c)),
+	}
+	// Annotations only support start_column and end_column on the same line.
+	if startLine == endLine {
+		if s, e := loc.GetRange().GetStart().GetColumn(), loc.GetRange().GetEnd().GetColumn(); s != 0 && e != 0 {
+			a.StartColumn = github.Int(int(s))
+			a.EndColumn = github.Int(int(e))
+		}
+	}
+	if s := c.Diagnostic.GetOriginalOutput(); s != "" {
+		a.RawDetails = github.String(s)
+	}
+	return a
+}
+
+func (ch *Check) buildTitle(c *filter.FilteredDiagnostic) string {
+	var sb strings.Builder
+	toolName := c.Diagnostic.GetSource().GetName()
+	if toolName == "" {
+		toolName = ch.ToolName
+	}
+	if toolName != "" {
+		sb.WriteString(fmt.Sprintf("[%s] ", toolName))
+	}
+	loc := c.Diagnostic.GetLocation()
+	sb.WriteString(loc.GetPath())
+	if startLine := int(loc.GetRange().GetStart().GetLine()); startLine > 0 {
+		sb.WriteString(fmt.Sprintf("#L%d", startLine))
+		if endLine := int(loc.GetRange().GetEnd().GetLine()); startLine < endLine {
+			sb.WriteString(fmt.Sprintf("-L%d", endLine))
+		}
+	}
+	if code := c.Diagnostic.GetCode().GetValue(); code != "" {
+		if url := c.Diagnostic.GetCode().GetUrl(); url != "" {
+			sb.WriteString(fmt.Sprintf(" <%s>(%s)", code, url))
+		} else {
+			sb.WriteString(fmt.Sprintf(" <%s>", code))
+		}
+	}
+	return sb.String()
+}
+
+func (ch *Check) postAnnotations(ctx context.Context, checkID int64, annotations []*github.CheckRunAnnotation) error {
+	opt := github.UpdateCheckRunOptions{
+		Name: ch.checkName(),
+		Output: &github.CheckRunOutput{
+			Title:       github.String(ch.checkTitle()),
+			Summary:     github.String(""), // Post summary with the last request.
+			Annotations: annotations[:min(maxAnnotationsPerRequest, len(annotations))],
+		},
+	}
+	if _, _, err := ch.CLI.Checks.UpdateCheckRun(ctx, ch.Owner, ch.Repo, checkID, opt); err != nil {
+		return err
+	}
+	if len(annotations) > maxAnnotationsPerRequest {
+		return ch.postAnnotations(ctx, checkID, annotations[maxAnnotationsPerRequest:])
+	}
+	return nil
+}
+
+// https://developer.github.com/v3/checks/runs/#parameters-1
+func (ch *Check) conclusion(annotations []*github.CheckRunAnnotation) string {
+	checkResult := "success"
+
+	if ch.Level != "" {
+		// Level takes precedence when configured (for backwards compatibility)
+		if len(annotations) == 0 {
+			return checkResult
+		}
+		switch strings.ToLower(ch.Level) {
+		case "info", "warning":
+			return "neutral"
+		}
+		return "failure"
+	}
+
+	precedence := map[string]int{
+		"success": 0,
+		"notice":  1,
+		"warning": 2,
+		"failure": 3,
+	}
+
+	highestLevel := "success"
+	for _, a := range annotations {
+		annotationLevel := *a.AnnotationLevel
+		if precedence[annotationLevel] > precedence[highestLevel] {
+			highestLevel = annotationLevel
+		}
+	}
+	checkResult = highestLevel
+
+	switch checkResult {
+	case "success":
+		return "success"
+	case "notice", "warning":
+		return "neutral"
+	}
+	return "failure"
+}
+
+func (ch *Check) checkName() string {
+	if ch.ToolName != "" {
+		return ch.ToolName
+	}
+	return "reviewdog"
+}
+
+func (ch *Check) checkTitle() string {
+	if name := ch.checkName(); name != "reviewdog" {
+		return fmt.Sprintf("reviewdog [%s] report", name)
+	}
+	return "reviewdog report"
+}
+
+// https://developer.github.com/v3/checks/runs/#annotations-object
+func (ch *Check) annotationLevel(s rdf.Severity) string {
+	switch s {
+	case rdf.Severity_ERROR:
+		return "failure"
+	case rdf.Severity_WARNING:
+		return "warning"
+	case rdf.Severity_INFO:
+		return "notice"
+	default:
+		return ch.reqAnnotationLevel()
+	}
+}
+
+func (ch *Check) reqAnnotationLevel() string {
+	switch strings.ToLower(ch.Level) {
+	case "info":
+		return "notice"
+	case "warning":
+		return "warning"
+	case "failure":
+		return "failure"
+	}
+	return "failure"
+}
+
+func (ch *Check) summary(checks []*reviewdog.Comment) string {
+	var lines []string
+	var usedBytes int
+	lines = append(lines, "reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:")
+	usedBytes += len(lines[0]) + 1
+	var findings []*filter.FilteredDiagnostic
+	var filteredFindings []*filter.FilteredDiagnostic
+	for _, c := range checks {
+		if c.Result.ShouldReport {
+			findings = append(findings, c.Result)
+		} else {
+			filteredFindings = append(filteredFindings, c.Result)
+		}
+	}
+
+	findingMsgs, usedBytes := ch.summaryFindings("Findings", usedBytes, findings)
+	lines = append(lines, findingMsgs...)
+	filteredFindingsMsgs, _ := ch.summaryFindings("Filtered Findings", usedBytes, filteredFindings)
+	lines = append(lines, filteredFindingsMsgs...)
+	return strings.Join(lines, "\n")
+}
+
+func (ch *Check) summaryFindings(name string, usedBytes int, checks []*filter.FilteredDiagnostic) ([]string, int) {
+	var lines []string
+	lines = append(lines, fmt.Sprintf("<details>\n<summary>%s (%d)</summary>\n", name, len(checks)))
+	if len(lines[0])+1+usedBytes > maxAllowedSize {
+		// bail out if we're already over the limit
+		return nil, usedBytes
+	}
+	usedBytes += len(lines[0]) + 1
+	for _, c := range checks {
+		nextLine := githubutils.LinkedMarkdownDiagnostic(ch.Owner, ch.Repo, ch.SHA, c.Diagnostic)
+		// existing lines + newline + closing details tag must be smaller than the max allowed size
+		if usedBytes+len(nextLine)+1+10 >= maxAllowedSize {
+			cutoffMsg := "... (Too many findings. Dropped some findings)"
+			if usedBytes+len(cutoffMsg)+1+10 <= maxAllowedSize {
+				lines = append(lines, cutoffMsg)
+				usedBytes += len(cutoffMsg) + 1
+			}
+			break
+		}
+		lines = append(lines, nextLine)
+		usedBytes += len(nextLine) + 1
+	}
+	lines = append(lines, "</details>")
+	usedBytes += 10 + 1
+	return lines, usedBytes
+}
diff --git a/service/github/diff.go b/service/github/diff.go
new file mode 100644
index 00000000..d513d490
--- /dev/null
+++ b/service/github/diff.go
@@ -0,0 +1,79 @@
+package github
+
+import (
+	"context"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+	"os/exec"
+
+	"github.com/google/go-github/v60/github"
+	"github.com/reviewdog/reviewdog"
+)
+
+var _ reviewdog.DiffService = (*PullRequestDiffService)(nil)
+
+// PullRequestDiffService is a DiffService which uses GitHub Diff API.
+type PullRequestDiffService struct {
+	Cli              *github.Client
+	Owner            string
+	Repo             string
+	PR               int
+	SHA              string
+	FallBackToGitCLI bool
+}
+
+// Strip returns 1 as a strip of git diff.
+func (p *PullRequestDiffService) Strip() int {
+	return 1
+}
+
+// Diff returns a diff of PullRequest.
+func (p *PullRequestDiffService) Diff(ctx context.Context) ([]byte, error) {
+	opt := github.RawOptions{Type: github.Diff}
+	d, resp, err := p.Cli.PullRequests.GetRaw(ctx, p.Owner, p.Repo, p.PR, opt)
+	if err != nil {
+		if resp != nil && p.FallBackToGitCLI && resp.StatusCode == http.StatusNotAcceptable {
+			log.Print("reviwedog: fallback to use git command")
+			return p.diffUsingGitCommand(ctx)
+		}
+
+		return nil, err
+	}
+	return []byte(d), nil
+}
+
+// diffUsingGitCommand returns a diff of PullRequest using git command.
+func (p *PullRequestDiffService) diffUsingGitCommand(ctx context.Context) ([]byte, error) {
+	pr, _, err := p.Cli.PullRequests.Get(ctx, p.Owner, p.Repo, p.PR)
+	if err != nil {
+		return nil, err
+	}
+
+	head := pr.GetHead()
+	headSha := head.GetSHA()
+
+	commitsComparison, _, err := p.Cli.Repositories.CompareCommits(ctx, p.Owner, p.Repo, headSha, pr.GetBase().GetSHA(), nil)
+	if err != nil {
+		return nil, err
+	}
+
+	mergeBaseSha := commitsComparison.GetMergeBaseCommit().GetSHA()
+
+	if os.Getenv("REVIEWDOG_SKIP_GIT_FETCH") != "true" {
+		for _, sha := range []string{mergeBaseSha, headSha} {
+			_, err := exec.Command("git", "fetch", "--depth=1", head.GetRepo().GetHTMLURL(), sha).CombinedOutput()
+			if err != nil {
+				return nil, fmt.Errorf("failed to run git fetch: %w", err)
+			}
+		}
+	}
+
+	bytes, err := exec.Command("git", "diff", "--find-renames", mergeBaseSha, headSha).CombinedOutput()
+	if err != nil {
+		return nil, fmt.Errorf("failed to run git diff: %w", err)
+	}
+
+	return bytes, nil
+}
diff --git a/service/github/github.go b/service/github/github.go
index a7ddaaa6..3d15c93d 100644
--- a/service/github/github.go
+++ b/service/github/github.go
@@ -9,7 +9,6 @@ import (
 	"log"
 	"net/http"
 	"os"
-	"os/exec"
 	"path/filepath"
 	"strings"
 	"sync"
@@ -353,52 +352,14 @@ func DecodeMetaComment(metaBase64 string) (*metacomment.MetaComment, error) {
 
 // Diff returns a diff of PullRequest.
 func (g *PullRequest) Diff(ctx context.Context) ([]byte, error) {
-	opt := github.RawOptions{Type: github.Diff}
-	d, resp, err := g.cli.PullRequests.GetRaw(ctx, g.owner, g.repo, g.pr, opt)
-	if err != nil {
-		if resp != nil && resp.StatusCode == http.StatusNotAcceptable {
-			// git command should exist here. See NewGitHubPullRequest.
-			log.Print("fallback to use git command")
-			return g.diffUsingGitCommand(ctx)
-		}
-
-		return nil, err
-	}
-	return []byte(d), nil
-}
-
-// diffUsingGitCommand returns a diff of PullRequest using git command.
-func (g *PullRequest) diffUsingGitCommand(ctx context.Context) ([]byte, error) {
-	pr, _, err := g.cli.PullRequests.Get(ctx, g.owner, g.repo, g.pr)
-	if err != nil {
-		return nil, err
-	}
-
-	head := pr.GetHead()
-	headSha := head.GetSHA()
-
-	commitsComparison, _, err := g.cli.Repositories.CompareCommits(ctx, g.owner, g.repo, headSha, pr.GetBase().GetSHA(), nil)
-	if err != nil {
-		return nil, err
-	}
-
-	mergeBaseSha := commitsComparison.GetMergeBaseCommit().GetSHA()
-
-	if os.Getenv("REVIEWDOG_SKIP_GIT_FETCH") != "true" {
-		for _, sha := range []string{mergeBaseSha, headSha} {
-			_, err := exec.Command("git", "fetch", "--depth=1", head.GetRepo().GetHTMLURL(), sha).CombinedOutput()
-			if err != nil {
-				return nil, fmt.Errorf("failed to run git fetch: %w", err)
-			}
-		}
-	}
-
-	bytes, err := exec.Command("git", "diff", "--find-renames", mergeBaseSha, headSha).CombinedOutput()
-	if err != nil {
-		return nil, fmt.Errorf("failed to run git diff: %w", err)
-	}
-
-	return bytes, nil
+	return (&PullRequestDiffService{
+		Cli:              g.cli,
+		Owner:            g.owner,
+		Repo:             g.repo,
+		PR:               g.pr,
+		SHA:              g.sha,
+		FallBackToGitCLI: true,
+	}).Diff(ctx)
 }
 
 // Strip returns 1 as a strip of git diff.


Test patch:
diff --git a/_testdata/custom.txt b/_testdata/custom.txt
index 5a8cbdd6..1a0ddc66 100644
--- a/_testdata/custom.txt
+++ b/_testdata/custom.txt
@@ -3,7 +3,7 @@ Custom text for testing
 Line 3: Counter=1
 
 --- Range
-	a = 10
+	a = 11
 	b = 20
 	c = 30
 ---
diff --git a/cmd/reviewdog/doghouse_test.go b/cmd/reviewdog/doghouse_test.go
index e7ee60af..c15f7ba7 100644
--- a/cmd/reviewdog/doghouse_test.go
+++ b/cmd/reviewdog/doghouse_test.go
@@ -1,16 +1,18 @@
 package main
 
 import (
-	"bytes"
 	"context"
+	"encoding/json"
 	"fmt"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
 	"os"
 	"path/filepath"
 	"strings"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
-	"golang.org/x/oauth2"
 	"google.golang.org/protobuf/testing/protocmp"
 
 	"github.com/reviewdog/reviewdog"
@@ -22,98 +24,6 @@ import (
 	"github.com/reviewdog/reviewdog/proto/rdf"
 )
 
-func setupEnvs(testEnvs map[string]string) (cleanup func()) {
-	saveEnvs := make(map[string]string)
-	for key, value := range testEnvs {
-		saveEnvs[key] = os.Getenv(key)
-		if value == "" {
-			os.Unsetenv(key)
-		} else {
-			os.Setenv(key, value)
-		}
-	}
-	return func() {
-		for key, value := range saveEnvs {
-			os.Setenv(key, value)
-		}
-	}
-}
-
-func TestNewDoghouseCli_returnGitHubClient(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"REVIEWDOG_TOKEN":            "",
-		"GITHUB_ACTIONS":             "xxx",
-		"REVIEWDOG_GITHUB_API_TOKEN": "xxx",
-	})
-	defer cleanup()
-	cli, err := newDoghouseCli(context.Background())
-	if err != nil {
-		t.Fatalf("failed to create new client: %v", err)
-	}
-	if _, ok := cli.(*client.GitHubClient); !ok {
-		t.Errorf("got %T client, want *client.GitHubClient client", cli)
-	}
-}
-
-func TestNewDoghouseCli_returnErrorForGitHubClient(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"REVIEWDOG_TOKEN":            "",
-		"GITHUB_ACTIONS":             "true",
-		"REVIEWDOG_GITHUB_API_TOKEN": "", // missing
-	})
-	defer cleanup()
-	if _, err := newDoghouseCli(context.Background()); err == nil {
-		t.Error("got no error but want REVIEWDOG_GITHUB_API_TOKEN missing error")
-	}
-}
-
-func TestNewDoghouseCli_returnDogHouseClientWithReviewdogToken(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"REVIEWDOG_TOKEN":            "xxx",
-		"GITHUB_ACTIONS":             "true",
-		"REVIEWDOG_GITHUB_API_TOKEN": "xxx",
-	})
-	defer cleanup()
-	cli, err := newDoghouseCli(context.Background())
-	if err != nil {
-		t.Fatalf("failed to create new client: %v", err)
-	}
-	if _, ok := cli.(*client.DogHouseClient); !ok {
-		t.Errorf("got %T client, want *client.DogHouseClient client", cli)
-	}
-}
-
-func TestNewDoghouseCli_returnDogHouseClient(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"REVIEWDOG_TOKEN":            "",
-		"GITHUB_ACTIONS":             "",
-		"REVIEWDOG_GITHUB_API_TOKEN": "",
-	})
-	defer cleanup()
-	cli, err := newDoghouseCli(context.Background())
-	if err != nil {
-		t.Fatalf("failed to create new client: %v", err)
-	}
-	if _, ok := cli.(*client.DogHouseClient); !ok {
-		t.Errorf("got %T client, want *client.DogHouseClient client", cli)
-	}
-}
-
-func TestNewDoghouseServerCli(t *testing.T) {
-	if _, ok := newDoghouseServerCli(context.Background()).Client.Transport.(*oauth2.Transport); ok {
-		t.Error("got oauth2 http client, want default client")
-	}
-
-	cleanup := setupEnvs(map[string]string{
-		"REVIEWDOG_TOKEN": "xxx",
-	})
-	defer cleanup()
-
-	if _, ok := newDoghouseServerCli(context.Background()).Client.Transport.(*oauth2.Transport); !ok {
-		t.Error("w/ TOKEN: got unexpected http client, want oauth client")
-	}
-}
-
 func TestDiagnosticResultSet_Project(t *testing.T) {
 	defer func(f func(ctx context.Context, conf *project.Config, runners map[string]bool, level string, tee bool) (*reviewdog.ResultMap, error)) {
 		projectRunAndParse = f
@@ -194,15 +104,6 @@ func TestDiagnosticResultSet_NonProject(t *testing.T) {
 	})
 }
 
-type fakeDoghouseServerCli struct {
-	client.DogHouseClientInterface
-	FakeCheck func(context.Context, *doghouse.CheckRequest, bool) (*doghouse.CheckResponse, error)
-}
-
-func (f *fakeDoghouseServerCli) Check(ctx context.Context, req *doghouse.CheckRequest, makeRequest bool) (*doghouse.CheckResponse, error) {
-	return f.FakeCheck(ctx, req, makeRequest)
-}
-
 func TestPostResultSet_withReportURL(t *testing.T) {
 	const (
 		owner = "haya14busa"
@@ -210,9 +111,15 @@ func TestPostResultSet_withReportURL(t *testing.T) {
 		prNum = 14
 		sha   = "1414"
 	)
-
-	fakeCli := &fakeDoghouseServerCli{}
-	fakeCli.FakeCheck = func(ctx context.Context, req *doghouse.CheckRequest, makeRequest bool) (*doghouse.CheckResponse, error) {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/check", func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodPost {
+			t.Errorf("unexpected method=%s", r.Method)
+		}
+		var req doghouse.CheckRequest
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			t.Error(err)
+		}
 		if req.Owner != owner {
 			t.Errorf("req.Owner = %q, want %q", req.Owner, owner)
 		}
@@ -270,8 +177,19 @@ func TestPostResultSet_withReportURL(t *testing.T) {
 		default:
 			t.Errorf("unexpected req.Name: %s", req.Name)
 		}
-		return &doghouse.CheckResponse{ReportURL: "xxx"}, nil
-	}
+
+		if err := json.NewEncoder(w).Encode(&doghouse.CheckResponse{
+			ReportURL: "xxx",
+		}); err != nil {
+			t.Fatal(err)
+		}
+		w.WriteHeader(http.StatusOK)
+	})
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := client.New(ts.Client())
+	cli.BaseURL, _ = url.Parse(ts.URL)
 
 	// It assumes the current dir is ./cmd/reviewdog/
 	var resultSet reviewdog.ResultMap
@@ -313,45 +231,9 @@ func TestPostResultSet_withReportURL(t *testing.T) {
 	}
 
 	opt := &option{filterMode: filter.ModeAdded}
-	if _, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli, opt, true); err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestPostResultSet_withoutReportURL(t *testing.T) {
-	const (
-		owner = "haya14busa"
-		repo  = "reviewdog"
-		prNum = 14
-		sha   = "1414"
-	)
-
-	wantResults := []*filter.FilteredDiagnostic{{ShouldReport: true}}
-	fakeCli := &fakeDoghouseServerCli{}
-	fakeCli.FakeCheck = func(ctx context.Context, req *doghouse.CheckRequest, makeRequest bool) (*doghouse.CheckResponse, error) {
-		return &doghouse.CheckResponse{CheckedResults: wantResults}, nil
-	}
-
-	var resultSet reviewdog.ResultMap
-	resultSet.Store("name1", &reviewdog.Result{Diagnostics: []*rdf.Diagnostic{}})
-
-	ghInfo := &cienv.BuildInfo{Owner: owner, Repo: repo, PullRequest: prNum, SHA: sha}
-
-	opt := &option{filterMode: filter.ModeAdded}
-	resp, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli, opt, true)
-	if err != nil {
+	if err := postResultSet(context.Background(), &resultSet, ghInfo, cli, opt); 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.FilteredDiagnostic, wantResults, protocmp.Transform()); diff != "" {
-		t.Errorf("results has diff:\n%s", diff)
-	}
 }
 
 func TestPostResultSet_conclusion(t *testing.T) {
@@ -362,11 +244,6 @@ func TestPostResultSet_conclusion(t *testing.T) {
 		sha   = "1414"
 	)
 
-	fakeCli := &fakeDoghouseServerCli{}
-	var resultSet reviewdog.ResultMap
-	resultSet.Store("name1", &reviewdog.Result{Diagnostics: []*rdf.Diagnostic{}})
-	ghInfo := &cienv.BuildInfo{Owner: owner, Repo: repo, PullRequest: prNum, SHA: sha}
-
 	tests := []struct {
 		conclusion  string
 		failOnError bool
@@ -380,170 +257,41 @@ func TestPostResultSet_conclusion(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		tt := tt
-		fakeCli.FakeCheck = func(ctx context.Context, req *doghouse.CheckRequest, makeRequest bool) (*doghouse.CheckResponse, error) {
-			return &doghouse.CheckResponse{ReportURL: "xxx", Conclusion: tt.conclusion}, nil
-		}
-		opt := &option{filterMode: filter.ModeAdded, failOnError: tt.failOnError}
 		id := fmt.Sprintf("[conclusion=%s, failOnError=%v]", tt.conclusion, tt.failOnError)
-		_, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli, opt, true)
-		if tt.wantErr && err == nil {
-			t.Errorf("[%s] want err, but got nil.", id)
-		} else if !tt.wantErr && err != nil {
-			t.Errorf("[%s] got unexpected error: %v", id, err)
-		}
-	}
-}
-
-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, makeRequest bool) (*doghouse.CheckResponse, error) {
-		return &doghouse.CheckResponse{}, nil
-	}
-
-	var resultSet reviewdog.ResultMap
-	resultSet.Store("name1", &reviewdog.Result{Diagnostics: []*rdf.Diagnostic{}})
-
-	ghInfo := &cienv.BuildInfo{Owner: owner, Repo: repo, PullRequest: prNum, SHA: sha}
-
-	opt := &option{filterMode: filter.ModeAdded}
-	if _, err := postResultSet(context.Background(), &resultSet, ghInfo, fakeCli, opt, true); err == nil {
-		t.Error("got no error but want report missing error")
-	}
-}
-
-func TestReportResults(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"GITHUB_ACTIONS":    "",
-		"GITHUB_EVENT_PATH": "",
-	})
-	defer cleanup()
-	filteredResultSet := new(reviewdog.FilteredResultMap)
-	filteredResultSet.Store("name1", &reviewdog.FilteredResult{
-		FilteredDiagnostic: []*filter.FilteredDiagnostic{
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1-L1\nname1-L2",
-				},
-				ShouldReport: true,
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1.2-L1\nname1.2-L2",
-				},
-				ShouldReport: false,
-			},
-		},
-	})
-	filteredResultSet.Store("name2", &reviewdog.FilteredResult{
-		FilteredDiagnostic: []*filter.FilteredDiagnostic{
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1-L1\nname1-L2",
-				},
-				ShouldReport: false,
-			},
-		},
-	})
-	stdout := new(bytes.Buffer)
-	foundResultShouldReport := reportResults(stdout, filteredResultSet, false)
-	if !foundResultShouldReport {
-		t.Errorf("foundResultShouldReport = %v, want true", foundResultShouldReport)
-	}
-	want := `reviewdog: Reporting results for "name1"
-name1-L1
-name1-L2
-reviewdog: Reporting results for "name2"
-reviewdog: 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_inGitHubAction(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"GITHUB_ACTIONS":    "true",
-		"GITHUB_EVENT_PATH": "",
-	})
-	defer cleanup()
-	filteredResultSet := new(reviewdog.FilteredResultMap)
-	filteredResultSet.Store("name1", &reviewdog.FilteredResult{
-		FilteredDiagnostic: []*filter.FilteredDiagnostic{
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1-L1\nname1-L2",
-				},
-				ShouldReport: true,
-			},
-		},
-		Level: "info",
-	})
-	stdout := new(bytes.Buffer)
-	foundResultShouldReport := reportResults(stdout, filteredResultSet, false)
-	if foundResultShouldReport {
-		t.Errorf("foundResultShouldReport = %v, want false", foundResultShouldReport)
-	}
-
-	want := `reviewdog: Reporting results for "name1"
-`
-	if got := stdout.String(); got != want {
-		t.Errorf("diff found for report:\ngot:\n%s\nwant:\n%s", got, want)
-	}
-}
+		t.Run(id, func(t *testing.T) {
+			mux := http.NewServeMux()
+			mux.HandleFunc("/check", func(w http.ResponseWriter, r *http.Request) {
+				if err := json.NewEncoder(w).Encode(&doghouse.CheckResponse{
+					ReportURL:  "xxx",
+					Conclusion: tt.conclusion,
+				}); err != nil {
+					t.Fatal(err)
+				}
+				w.WriteHeader(http.StatusOK)
+			})
+			ts := httptest.NewServer(mux)
+			defer ts.Close()
+
+			cli := client.New(&http.Client{})
+			cli.BaseURL, _ = url.Parse(ts.URL)
+			var resultSet reviewdog.ResultMap
+			resultSet.Store("name1", &reviewdog.Result{Diagnostics: []*rdf.Diagnostic{}})
+
+			ghInfo := &cienv.BuildInfo{
+				Owner:       owner,
+				Repo:        repo,
+				PullRequest: prNum,
+				SHA:         sha,
+			}
 
-func TestReportResults_noResultsShouldReport(t *testing.T) {
-	cleanup := setupEnvs(map[string]string{
-		"GITHUB_ACTIONS":    "",
-		"GITHUB_EVENT_PATH": "",
-	})
-	defer cleanup()
-	filteredResultSet := new(reviewdog.FilteredResultMap)
-	filteredResultSet.Store("name1", &reviewdog.FilteredResult{
-		FilteredDiagnostic: []*filter.FilteredDiagnostic{
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1-L1\nname1-L2",
-				},
-				ShouldReport: false,
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1.2-L1\nname1.2-L2",
-				},
-				ShouldReport: false,
-			},
-		},
-	})
-	filteredResultSet.Store("name2", &reviewdog.FilteredResult{
-		FilteredDiagnostic: []*filter.FilteredDiagnostic{
-			{
-				Diagnostic: &rdf.Diagnostic{
-					OriginalOutput: "name1-L1\nname1-L2",
-				},
-				ShouldReport: false,
-			},
-		},
-	})
-	stdout := new(bytes.Buffer)
-	foundResultShouldReport := reportResults(stdout, filteredResultSet, false)
-	if foundResultShouldReport {
-		t.Errorf("foundResultShouldReport = %v, want false", foundResultShouldReport)
-	}
-	want := `reviewdog: Reporting results for "name1"
-reviewdog: No results found for "name1". 2 results found outside diff.
-reviewdog: Reporting results for "name2"
-reviewdog: 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)
+			opt := &option{filterMode: filter.ModeAdded, failOnError: tt.failOnError}
+			err := postResultSet(context.Background(), &resultSet, ghInfo, cli, opt)
+			if tt.wantErr && err == nil {
+				t.Errorf("[%s] want err, but got nil.", id)
+			} else if !tt.wantErr && err != nil {
+				t.Errorf("[%s] got unexpected error: %v", id, err)
+			}
+		})
 	}
 }
 
diff --git a/doghouse/client/client_test.go b/doghouse/client/client_test.go
index ab5c497d..a39c8847 100644
--- a/doghouse/client/client_test.go
+++ b/doghouse/client/client_test.go
@@ -25,7 +25,7 @@ func TestDogHouseClient_Check(t *testing.T) {
 	cli.BaseURL, _ = url.Parse(ts.URL)
 
 	req := &doghouse.CheckRequest{}
-	resp, err := cli.Check(context.Background(), req, true)
+	resp, err := cli.Check(context.Background(), req)
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -47,7 +47,7 @@ func TestDogHouseClient_Check_failure(t *testing.T) {
 	cli.BaseURL, _ = url.Parse(ts.URL)
 
 	req := &doghouse.CheckRequest{}
-	_, err := cli.Check(context.Background(), req, true)
+	_, err := cli.Check(context.Background(), req)
 	if err == nil {
 		t.Error("got no error, but want bad request error")
 	}
diff --git a/doghouse/server/doghouse_test.go b/doghouse/server/doghouse_test.go
index 967ec63f..4922e28a 100644
--- a/doghouse/server/doghouse_test.go
+++ b/doghouse/server/doghouse_test.go
@@ -2,38 +2,18 @@ package server
 
 import (
 	"context"
-	"errors"
-	"fmt"
+	"encoding/json"
 	"net/http"
-	"strings"
+	"net/http/httptest"
+	"net/url"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/google/go-github/v60/github"
-
 	"github.com/reviewdog/reviewdog/doghouse"
-	"github.com/reviewdog/reviewdog/filter"
 	"github.com/reviewdog/reviewdog/proto/rdf"
 )
 
-type fakeCheckerGitHubCli struct {
-	FakeGetPullRequestDiff func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error)
-	FakeCreateCheckRun     func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error)
-	FakeUpdateCheckRun     func(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error)
-}
-
-func (f *fakeCheckerGitHubCli) GetPullRequestDiff(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-	return f.FakeGetPullRequestDiff(ctx, owner, repo, number, fallbackToGitCLI)
-}
-
-func (f *fakeCheckerGitHubCli) CreateCheckRun(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-	return f.FakeCreateCheckRun(ctx, owner, repo, opt)
-}
-
-func (f *fakeCheckerGitHubCli) UpdateCheckRun(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
-	return f.FakeUpdateCheckRun(ctx, owner, repo, checkID, opt)
-}
-
 const sampleDiff = `--- a/sample.old.txt	2016-10-13 05:09:35.820791185 +0900
 +++ b/sample.new.txt	2016-10-13 05:15:26.839245048 +0900
 @@ -1,3 +1,4 @@
@@ -98,88 +78,6 @@ func TestCheck_OK(t *testing.T) {
 					OriginalOutput: "raw test message outside diff",
 				},
 			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test multiline",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2},
-							End:   &rdf.Position{Line: 3},
-						},
-					},
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test multiline with column",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2, Column: 1},
-							End:   &rdf.Position{Line: 3, Column: 5},
-						},
-					},
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test range comment",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2, Column: 1},
-							End:   &rdf.Position{Line: 2, Column: 5},
-						},
-					},
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message:  "test severity override",
-					Severity: rdf.Severity_ERROR,
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2},
-						},
-					},
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "source test",
-					Source: &rdf.Source{
-						Name: "awesome-linter",
-					},
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2},
-						},
-					},
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "code test w/o URL",
-					Location: &rdf.Location{
-						Path:  "sample.new.txt",
-						Range: &rdf.Range{Start: &rdf.Position{Line: 2}},
-					},
-					Code: &rdf.Code{Value: "CODE14"},
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "code test w/ URL",
-					Location: &rdf.Location{
-						Path:  "sample.new.txt",
-						Range: &rdf.Range{Start: &rdf.Position{Line: 2}},
-					},
-					Code: &rdf.Code{Value: "CODE14", Url: "https://github.com/reviewdog#CODE14"},
-				},
-			},
 			{
 				Path:       "sample.new.txt",
 				Line:       2,
@@ -190,605 +88,171 @@ func TestCheck_OK(t *testing.T) {
 		Level: "warning",
 	}
 
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		return []byte(sampleDiff), nil
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		if opt.Name != name {
-			t.Errorf("CreateCheckRunOptions.Name = %q, want %q", opt.Name, name)
-		}
-		if opt.HeadSHA != sha {
-			t.Errorf("CreateCheckRunOptions.HeadSHA = %q, want %q", opt.HeadSHA, sha)
-		}
-		return &github.CheckRun{ID: github.Int64(wantCheckID)}, nil
-	}
-	cli.FakeUpdateCheckRun = func(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
-		if checkID != wantCheckID {
-			t.Errorf("UpdateCheckRun: checkID = %d, want %d", checkID, wantCheckID)
-		}
-		if opt.Name != name {
-			t.Errorf("UpdateCheckRunOptions.Name = %q, want %q", opt.Name, name)
+	// cli := &fakeCheckerGitHubCli{}
+	// cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
+	// 	return []byte(sampleDiff), nil
+	// }
+	// cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
+	// 	if opt.Name != name {
+	// 		t.Errorf("CreateCheckRunOptions.Name = %q, want %q", opt.Name, name)
+	// 	}
+	// 	if opt.HeadSHA != sha {
+	// 		t.Errorf("CreateCheckRunOptions.HeadSHA = %q, want %q", opt.HeadSHA, sha)
+	// 	}
+	// 	return &github.CheckRun{ID: github.Int64(wantCheckID)}, nil
+	// }
+	// cli.FakeUpdateCheckRun = func(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
+	// 	if checkID != wantCheckID {
+	// 		t.Errorf("UpdateCheckRun: checkID = %d, want %d", checkID, wantCheckID)
+	// 	}
+	// 	if opt.Name != name {
+	// 		t.Errorf("UpdateCheckRunOptions.Name = %q, want %q", opt.Name, name)
+	// 	}
+	// 	annotations := opt.Output.Annotations
+	// 	if len(annotations) == 0 {
+	// 		if *opt.Conclusion != conclusion {
+	// 			t.Errorf("UpdateCheckRunOptions.Conclusion = %q, want %q", *opt.Conclusion, conclusion)
+	// 		}
+	// 	} else {
+	// 		wantAnnotations := []*github.CheckRunAnnotation{
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("test message"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+	// 				RawDetails:      github.String("raw test message"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(3),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("test multiline"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2-L3"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(3),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("test multiline with column"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2-L3"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				StartColumn:     github.Int(1),
+	// 				EndColumn:       github.Int(5),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("test range comment"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				AnnotationLevel: github.String("failure"),
+	// 				Message:         github.String("test severity override"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("source test"),
+	// 				Title:           github.String("[awesome-linter] sample.new.txt#L2"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("code test w/o URL"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2 <CODE14>"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("code test w/ URL"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2 <CODE14>(https://github.com/reviewdog#CODE14)"),
+	// 			},
+	// 			{
+	// 				Path:            github.String("sample.new.txt"),
+	// 				StartLine:       github.Int(2),
+	// 				EndLine:         github.Int(2),
+	// 				AnnotationLevel: github.String("warning"),
+	// 				Message:         github.String("request from old clients"),
+	// 				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+	// 				RawDetails:      github.String("raw message from old clients"),
+	// 			},
+	// 		}
+	// 		if d := cmp.Diff(annotations, wantAnnotations); d != "" {
+	// 			t.Errorf("Annotation diff found:\n%s", d)
+	// 		}
+	// 	}
+	// 	return &github.CheckRun{HTMLURL: github.String(reportURL)}, nil
+	// }
+	mux := http.NewServeMux()
+	mux.HandleFunc("/repos/haya14busa/reviewdog/pulls/14", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(sampleDiff))
+	})
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs", func(w http.ResponseWriter, r *http.Request) {
+		if err := json.NewEncoder(w).Encode(&github.CheckRun{ID: github.Int64(wantCheckID)}); err != nil {
+			t.Fatal(err)
 		}
-		annotations := opt.Output.Annotations
-		if len(annotations) == 0 {
-			if *opt.Conclusion != conclusion {
-				t.Errorf("UpdateCheckRunOptions.Conclusion = %q, want %q", *opt.Conclusion, conclusion)
-			}
-		} else {
-			wantAnnotations := []*github.CheckRunAnnotation{
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test message"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
-					RawDetails:      github.String("raw test message"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(3),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test multiline"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2-L3"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(3),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test multiline with column"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2-L3"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					StartColumn:     github.Int(1),
-					EndColumn:       github.Int(5),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test range comment"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("failure"),
-					Message:         github.String("test severity override"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("source test"),
-					Title:           github.String("[awesome-linter] sample.new.txt#L2"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("code test w/o URL"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2 <CODE14>"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("code test w/ URL"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2 <CODE14>(https://github.com/reviewdog#CODE14)"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("request from old clients"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
-					RawDetails:      github.String("raw message from old clients"),
-				},
-			}
-			if d := cmp.Diff(annotations, wantAnnotations); d != "" {
-				t.Errorf("Annotation diff found:\n%s", d)
-			}
+	})
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs/1414", func(w http.ResponseWriter, r *http.Request) {
+		var req github.CheckRun
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			t.Error(err)
 		}
-		return &github.CheckRun{HTMLURL: github.String(reportURL)}, nil
-	}
-	checker := &Checker{req: req, gh: cli}
-	res, err := checker.Check(context.Background(), true)
-	if err != nil {
-		t.Fatal(err)
-	}
-
-	if res.ReportURL != reportURL {
-		t.Errorf("res.reportURL = %q, want %q", res.ReportURL, reportURL)
-	}
-}
-
-func testOutsideDiff(t *testing.T, outsideDiff bool, filterMode filter.Mode) {
-	const (
-		name        = "haya14busa-linter"
-		owner       = "haya14busa"
-		repo        = "reviewdog"
-		prNum       = 14
-		sha         = "1414"
-		reportURL   = "http://example.com/report_url"
-		conclusion  = "neutral"
-		wantCheckID = 1414
-	)
-
-	req := &doghouse.CheckRequest{
-		Name:        name,
-		Owner:       owner,
-		Repo:        repo,
-		PullRequest: prNum,
-		SHA:         sha,
-		Annotations: []*doghouse.Annotation{
+		wantAnnotations := []*github.CheckRunAnnotation{
 			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test message",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2},
-						},
-					},
-					OriginalOutput: "raw test message",
-				},
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("test message"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+				RawDetails:      github.String("raw test message"),
 			},
 			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test message outside diff",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 14},
-						},
-					},
-					OriginalOutput: "raw test message outside diff",
-				},
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("request from old clients"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+				RawDetails:      github.String("raw message from old clients"),
 			},
-		},
-		Level:       "warning",
-		OutsideDiff: outsideDiff,
-		FilterMode:  filterMode,
-	}
-
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		return []byte(sampleDiff), nil
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		return &github.CheckRun{ID: github.Int64(wantCheckID)}, nil
-	}
-	cli.FakeUpdateCheckRun = func(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
-		annotations := opt.Output.Annotations
-		if len(annotations) == 0 {
-			if *opt.Conclusion != conclusion {
-				t.Errorf("UpdateCheckRunOptions.Conclusion = %q, want %q", *opt.Conclusion, conclusion)
-			}
-		} else {
-			wantAnnotations := []*github.CheckRunAnnotation{
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test message"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
-					RawDetails:      github.String("raw test message"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(14),
-					EndLine:         github.Int(14),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test message outside diff"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L14"),
-					RawDetails:      github.String("raw test message outside diff"),
-				},
-			}
-			if d := cmp.Diff(annotations, wantAnnotations); d != "" {
+		}
+		if req.GetStatus() != "completed" {
+			if d := cmp.Diff(req.Output.Annotations, wantAnnotations); d != "" {
 				t.Errorf("Annotation diff found:\n%s", d)
 			}
 		}
-		return &github.CheckRun{HTMLURL: github.String(reportURL)}, nil
-	}
-	checker := &Checker{req: req, gh: cli}
-	if _, err := checker.Check(context.Background(), true); err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestCheck_OK_deprecated_outsidediff(t *testing.T) {
-	t.Run("deprecated: outside_diff", func(t *testing.T) {
-		testOutsideDiff(t, true, filter.ModeDefault)
-	})
-	t.Run("filter-mode=NoFilter", func(t *testing.T) {
-		testOutsideDiff(t, false, filter.ModeNoFilter)
-	})
-}
-
-func TestCheck_OK_multiple_update_runs(t *testing.T) {
-	const (
-		name        = "haya14busa-linter"
-		owner       = "haya14busa"
-		repo        = "reviewdog"
-		prNum       = 14
-		sha         = "1414"
-		reportURL   = "http://example.com/report_url"
-		conclusion  = "neutral"
-		wantCheckID = 1414
-	)
-
-	req := &doghouse.CheckRequest{
-		Name:        name,
-		Owner:       owner,
-		Repo:        repo,
-		PullRequest: prNum,
-		SHA:         sha,
-		Level:       "warning",
-	}
-	for i := 0; i < 101; i++ {
-		req.Annotations = append(req.Annotations, &doghouse.Annotation{
-			Diagnostic: &rdf.Diagnostic{
-				Message: "test message",
-				Location: &rdf.Location{
-					Path: "sample.new.txt",
-					Range: &rdf.Range{
-						Start: &rdf.Position{Line: 2},
-					},
-				},
-				OriginalOutput: "raw test message",
-			},
-		})
-	}
-
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		return []byte(sampleDiff), nil
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		if opt.Name != name {
-			t.Errorf("CreateCheckRunOptions.Name = %q, want %q", opt.Name, name)
-		}
-		if opt.HeadSHA != sha {
-			t.Errorf("CreateCheckRunOptions.HeadSHA = %q, want %q", opt.HeadSHA, sha)
-		}
-		return &github.CheckRun{ID: github.Int64(wantCheckID)}, nil
-	}
-	cli.FakeUpdateCheckRun = func(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
-		if checkID != wantCheckID {
-			t.Errorf("UpdateCheckRun: checkID = %d, want %d", checkID, wantCheckID)
+		if err := json.NewEncoder(w).Encode(&github.CheckRun{HTMLURL: github.String(reportURL)}); err != nil {
+			t.Fatal(err)
 		}
-		annotations := opt.Output.Annotations
-		switch len(annotations) {
-		case 0:
-			if *opt.Conclusion != conclusion {
-				t.Errorf("UpdateCheckRunOptions.Conclusion = %q, want %q", *opt.Conclusion, conclusion)
-			}
-		case maxAnnotationsPerRequest, 1: // Expected
-		default:
-			t.Errorf("UpdateCheckRun: len(annotations) = %d, but it's unexpected", len(annotations))
-		}
-		return &github.CheckRun{HTMLURL: github.String(reportURL)}, nil
-	}
-	checker := &Checker{req: req, gh: cli}
-	if _, err := checker.Check(context.Background(), true); err != nil {
-		t.Fatal(err)
-	}
-}
-
-func TestCheck_OK_nonPullRequests(t *testing.T) {
-	const (
-		name        = "haya14busa-linter"
-		owner       = "haya14busa"
-		repo        = "reviewdog"
-		sha         = "1414"
-		reportURL   = "http://example.com/report_url"
-		conclusion  = "neutral"
-		wantCheckID = 1414
-	)
+	})
 
-	req := &doghouse.CheckRequest{
-		// Do not set PullRequest
-		Name:  name,
-		Owner: owner,
-		Repo:  repo,
-		SHA:   sha,
-		Annotations: []*doghouse.Annotation{
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test message",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 2},
-						},
-					},
-					OriginalOutput: "raw test message",
-				},
-			},
-			{
-				Diagnostic: &rdf.Diagnostic{
-					Message: "test message2",
-					Location: &rdf.Location{
-						Path: "sample.new.txt",
-						Range: &rdf.Range{
-							Start: &rdf.Position{Line: 14},
-						},
-					},
-					OriginalOutput: "raw test message2",
-				},
-			},
-		},
-		Level: "warning",
-	}
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
 
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		t.Errorf("GetPullRequestDiff should not be called")
-		return nil, nil
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		return &github.CheckRun{ID: github.Int64(wantCheckID)}, nil
-	}
-	cli.FakeUpdateCheckRun = func(ctx context.Context, owner, repo string, checkID int64, opt github.UpdateCheckRunOptions) (*github.CheckRun, error) {
-		if checkID != wantCheckID {
-			t.Errorf("UpdateCheckRun: checkID = %d, want %d", checkID, wantCheckID)
-		}
-		annotations := opt.Output.Annotations
-		if len(annotations) == 0 {
-			if *opt.Conclusion != conclusion {
-				t.Errorf("UpdateCheckRunOptions.Conclusion = %q, want %q", *opt.Conclusion, conclusion)
-			}
-		} else {
-			wantAnnotations := []*github.CheckRunAnnotation{
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(2),
-					EndLine:         github.Int(2),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test message"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
-					RawDetails:      github.String("raw test message"),
-				},
-				{
-					Path:            github.String("sample.new.txt"),
-					StartLine:       github.Int(14),
-					EndLine:         github.Int(14),
-					AnnotationLevel: github.String("warning"),
-					Message:         github.String("test message2"),
-					Title:           github.String("[haya14busa-linter] sample.new.txt#L14"),
-					RawDetails:      github.String("raw test message2"),
-				},
-			}
-			if d := cmp.Diff(annotations, wantAnnotations); d != "" {
-				t.Errorf("Annotation diff found:\n%s", d)
-			}
-		}
-		return &github.CheckRun{HTMLURL: github.String(reportURL)}, nil
-	}
-	checker := &Checker{req: req, gh: cli}
-	res, err := checker.Check(context.Background(), true)
+	cli := github.NewClient(nil)
+	cli.BaseURL, _ = url.Parse(ts.URL + "/")
+	checker := NewChecker(req, cli, true /* inDogHouseServer */)
+	res, err := checker.Check(context.Background())
 	if err != nil {
 		t.Fatal(err)
 	}
-
 	if res.ReportURL != reportURL {
 		t.Errorf("res.reportURL = %q, want %q", res.ReportURL, reportURL)
 	}
 }
-
-func TestCheck_fail_diff(t *testing.T) {
-	req := &doghouse.CheckRequest{PullRequest: 1}
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		return nil, errors.New("test diff failure")
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		return &github.CheckRun{}, nil
-	}
-	checker := &Checker{req: req, gh: cli}
-
-	if _, err := checker.Check(context.Background(), true); err == nil {
-		t.Fatalf("got no error, want some error")
-	} else {
-		t.Log(err)
-	}
-}
-
-func TestCheck_fail_invalid_diff(t *testing.T) {
-	t.Skip("Parse invalid diff function somehow doesn't return error")
-	req := &doghouse.CheckRequest{PullRequest: 1}
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		return []byte("invalid diff"), nil
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		return &github.CheckRun{}, nil
-	}
-	checker := &Checker{req: req, gh: cli}
-
-	if _, err := checker.Check(context.Background(), true); err == nil {
-		t.Fatalf("got no error, want some error")
-	} else {
-		t.Log(err)
-	}
-}
-
-func TestCheck_fail_check(t *testing.T) {
-	req := &doghouse.CheckRequest{PullRequest: 1}
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]byte, error) {
-		return []byte(sampleDiff), nil
-	}
-	cli.FakeCreateCheckRun = func(ctx context.Context, owner, repo string, opt github.CreateCheckRunOptions) (*github.CheckRun, error) {
-		return nil, errors.New("test check failure")
-	}
-	checker := &Checker{req: req, gh: cli}
-
-	if _, err := checker.Check(context.Background(), true); err == nil {
-		t.Fatalf("got no error, want some error")
-	} else {
-		t.Log(err)
-	}
-}
-
-func TestCheck_fail_check_with_403(t *testing.T) {
-	req := &doghouse.CheckRequest{PullRequest: 1}
-	cli := &fakeCheckerGitHubCli{}
-	cli.FakeGetPullRequestDiff = func(ctx context.Context, owner, repo string, number int, fallbackToGitCLI bool) ([]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(), true)
-	if err != nil {
-		t.Fatalf("got unexpected error: %v", err)
-	}
-	if resp.CheckedResults == nil {
-		t.Error("resp.CheckedResults should not be nil")
-	}
-}
-func TestCheck_too_many_findings_cut_off_correctly(t *testing.T) {
-	checker := &Checker{req: &doghouse.CheckRequest{}}
-
-	var diagnostics []*filter.FilteredDiagnostic
-	for i := 0; i < 1000; i++ {
-		diagnostics = append(diagnostics, &filter.FilteredDiagnostic{
-			ShouldReport: true,
-			Diagnostic: &rdf.Diagnostic{
-				Message: "this is a pretty long test message that will lead to overshooting the maximum allowed size",
-			},
-		})
-	}
-	summaryText := checker.summary(diagnostics)
-	if len(summaryText) > maxAllowedSize {
-		t.Errorf("summary text is %d bytes long, but the maximum allowed size is %d", len(summaryText), maxAllowedSize)
-	}
-	if !strings.Contains(summaryText, "... (Too many findings. Dropped some findings)\n</details>") {
-		t.Error("summary text was not cut off correctly")
-	}
-}
-
-func TestConclusion_calculate_level_from_annotations(t *testing.T) {
-	req := &doghouse.CheckRequest{PullRequest: 1}
-	checker := &Checker{req: req}
-
-	// Highest level = failure
-	annotations := []*github.CheckRunAnnotation{
-		{
-			AnnotationLevel: github.String("notice"),
-		},
-		{
-			AnnotationLevel: github.String("warning"),
-		},
-		{
-			AnnotationLevel: github.String("failure"),
-		},
-	}
-
-	conclusion := checker.conclusion(annotations)
-
-	expected := "failure"
-	if conclusion != expected {
-		t.Errorf("got conclusion %s, want %s", conclusion, expected)
-	}
-
-	// Highest level = warning
-	annotations = []*github.CheckRunAnnotation{
-		{
-			AnnotationLevel: github.String("notice"),
-		},
-		{
-			AnnotationLevel: github.String("warning"),
-		},
-	}
-
-	conclusion = checker.conclusion(annotations)
-
-	expected = "neutral"
-	if conclusion != expected {
-		t.Errorf("got conclusion %s, want %s", conclusion, expected)
-	}
-
-	// Highest level = notice
-	annotations = []*github.CheckRunAnnotation{
-		{
-			AnnotationLevel: github.String("notice"),
-		},
-	}
-
-	conclusion = checker.conclusion(annotations)
-
-	expected = "neutral"
-	if conclusion != expected {
-		t.Errorf("got conclusion %s, want %s", conclusion, expected)
-	}
-
-	// No annotations = success
-	annotations = []*github.CheckRunAnnotation{}
-
-	conclusion = checker.conclusion(annotations)
-
-	expected = "success"
-	if conclusion != expected {
-		t.Errorf("got conclusion %s, want %s", conclusion, expected)
-	}
-}
-
-func TestConclusion_with_level_config(t *testing.T) {
-	testcases := []struct {
-		level    string
-		expected string
-	}{
-		{"info", "neutral"},
-		{"warning", "neutral"},
-		{"error", "failure"},
-	}
-
-	for _, test := range testcases {
-		test := test
-		t.Run(fmt.Sprintf("level: %s", test.level), func(t *testing.T) {
-			req := &doghouse.CheckRequest{Level: test.level}
-			checker := &Checker{req: req}
-
-			annotations := []*github.CheckRunAnnotation{
-				{
-					AnnotationLevel: github.String("notice"),
-				},
-			}
-
-			conclusion := checker.conclusion(annotations)
-
-			expected := test.expected
-			if conclusion != expected {
-				t.Errorf("got conclusion %s, want %s", conclusion, expected)
-			}
-
-			// No annotations = success
-			annotations = []*github.CheckRunAnnotation{}
-
-			conclusion = checker.conclusion(annotations)
-
-			expected = "success"
-			if conclusion != expected {
-				t.Errorf("got conclusion %s, want %s", conclusion, expected)
-			}
-		})
-	}
-}
diff --git a/doghouse/server/github_checker_test.go b/doghouse/server/github_checker_test.go
deleted file mode 100644
index d95cd5d2..00000000
--- a/doghouse/server/github_checker_test.go
+++ /dev/null
@@ -1,169 +0,0 @@
-package server
-
-import (
-	"context"
-	"encoding/json"
-	"net/http"
-	"net/http/httptest"
-	"net/url"
-	"os"
-	"strings"
-	"testing"
-
-	"github.com/google/go-github/v60/github"
-	"github.com/reviewdog/reviewdog/doghouse"
-	"golang.org/x/oauth2"
-)
-
-const notokenSkipTestMes = "skipping test (requires actual Personal access tokens. export REVIEWDOG_TEST_GITHUB_API_TOKEN=<GitHub Personal Access Token>)"
-
-func setupGitHubClient() *github.Client {
-	token := os.Getenv("REVIEWDOG_TEST_GITHUB_API_TOKEN")
-	if token == "" {
-		return nil
-	}
-	ts := oauth2.StaticTokenSource(
-		&oauth2.Token{AccessToken: token},
-	)
-	tc := oauth2.NewClient(context.TODO(), ts)
-	return github.NewClient(tc)
-}
-
-func TestChecker_GetPullRequestDiff(t *testing.T) {
-	if testing.Short() {
-		t.Skip("skipping test which contains actual API requests in short mode")
-	}
-	client := setupGitHubClient()
-	if client == nil {
-		t.Skip(notokenSkipTestMes)
-	}
-
-	want := `diff --git a/.codecov.yml b/.codecov.yml
-index aa49124774..781ee2492f 100644
---- a/.codecov.yml
-+++ b/.codecov.yml
-@@ -7,5 +7,4 @@ coverage:
-       default:
-         target: 0%
- 
--comment:
--  layout: "header"
-+comment: false
-`
-
-	// https://github.com/reviewdog/reviewdog/pull/73
-	owner := "haya14busa"
-	repo := "reviewdog"
-	pr := 73
-	b, err := NewChecker(&doghouse.CheckRequest{}, client, false /* inDogHouseServer */).gh.GetPullRequestDiff(context.Background(), owner, repo, pr, false /* fallbackToGitCLI */)
-	if err != nil {
-		t.Fatal(err)
-	}
-	if got := string(b); got != want {
-		t.Errorf("got:\n%v\nwant:\n%v", got, want)
-	}
-}
-
-func TestChecker_GetPullRequestDiff_fake(t *testing.T) {
-	apiCalled := 0
-	mux := http.NewServeMux()
-	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
-		apiCalled++
-		if r.Method != http.MethodGet {
-			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
-		}
-		if accept := r.Header.Get("Accept"); !strings.Contains(accept, "diff") {
-			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
-		}
-		w.Write([]byte("Pull Request diff"))
-	})
-	ts := httptest.NewServer(mux)
-	defer ts.Close()
-
-	cli := github.NewClient(nil)
-	cli.BaseURL, _ = url.Parse(ts.URL + "/")
-
-	gh := NewChecker(&doghouse.CheckRequest{}, cli, false /* inDoghouseServer */).gh
-
-	if _, err := gh.GetPullRequestDiff(context.Background(), "o", "r", 14, false /* fallbackToGitCli */); err != nil {
-		t.Fatal(err)
-	}
-	if apiCalled != 1 {
-		t.Errorf("GitHub API should be called once; called %v times", apiCalled)
-	}
-}
-
-func TestChecker_GetPullRequestDiff_fake_fallback(t *testing.T) {
-	apiCalled := 0
-	mux := http.NewServeMux()
-	headSHA := "HEAD^"
-	baseSHA := "HEAD"
-	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
-		apiCalled++
-		if r.Method != http.MethodGet {
-			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
-		}
-		if accept := r.Header.Get("Accept"); strings.Contains(accept, "diff") {
-			w.WriteHeader(http.StatusNotAcceptable)
-			return
-		}
-		if accept := r.Header.Get("Accept"); accept != "application/vnd.github.v3+json" {
-			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
-		}
-
-		pullRequestJSON, err := json.Marshal(github.PullRequest{
-			Head: &github.PullRequestBranch{
-				SHA: &headSHA,
-			},
-			Base: &github.PullRequestBranch{
-				SHA: &baseSHA,
-			},
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		if _, err := w.Write(pullRequestJSON); err != nil {
-			t.Fatal(err)
-		}
-	})
-	mux.HandleFunc("/repos/o/r/compare/"+headSHA+"..."+baseSHA, func(w http.ResponseWriter, r *http.Request) {
-		if r.Method != http.MethodGet {
-			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
-		}
-		if accept := r.Header.Get("Accept"); accept != "application/vnd.github.v3+json" {
-			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
-		}
-
-		mergeBaseSha := "HEAD^"
-
-		commitsComparisonJSON, err := json.Marshal(github.CommitsComparison{
-			MergeBaseCommit: &github.RepositoryCommit{
-				SHA: &mergeBaseSha,
-			},
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		if _, err := w.Write(commitsComparisonJSON); err != nil {
-			t.Fatal(err)
-		}
-	})
-	ts := httptest.NewServer(mux)
-	defer ts.Close()
-
-	t.Setenv("REVIEWDOG_SKIP_GIT_FETCH", "true")
-
-	cli := github.NewClient(nil)
-	cli.BaseURL, _ = url.Parse(ts.URL + "/")
-
-	gh := NewChecker(&doghouse.CheckRequest{}, cli, false /* inDogHouseServer */).gh
-
-	if _, err := gh.GetPullRequestDiff(context.Background(), "o", "r", 14, true /* fallbackToGitCli */); err != nil {
-		t.Fatal(err)
-	}
-	if apiCalled != 2 {
-		t.Errorf("GitHub API should be called twice; called %v times", apiCalled)
-	}
-}
diff --git a/service/github/check_test.go b/service/github/check_test.go
new file mode 100644
index 00000000..713392a6
--- /dev/null
+++ b/service/github/check_test.go
@@ -0,0 +1,440 @@
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-github/v60/github"
+	"github.com/reviewdog/reviewdog"
+	"github.com/reviewdog/reviewdog/filter"
+	"github.com/reviewdog/reviewdog/proto/rdf"
+)
+
+func TestCheck_OK(t *testing.T) {
+	const (
+		name        = "haya14busa-linter"
+		owner       = "haya14busa"
+		repo        = "reviewdog"
+		prNum       = 14
+		sha         = "1414"
+		reportURL   = "http://example.com/report_url"
+		conclusion  = "neutral"
+		level       = "warning"
+		wantCheckID = 1414
+	)
+
+	comments := []*reviewdog.Comment{
+		{
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "test message",
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2, Column: 1},
+						},
+					},
+					OriginalOutput: "raw test message",
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "test message outside diff",
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 14},
+						},
+					},
+					OriginalOutput: "raw test message outside diff",
+				},
+				ShouldReport: false,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "test multiline",
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2},
+							End:   &rdf.Position{Line: 3},
+						},
+					},
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "test multiline with column",
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2, Column: 1},
+							End:   &rdf.Position{Line: 3, Column: 5},
+						},
+					},
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "test range comment",
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2, Column: 1},
+							End:   &rdf.Position{Line: 2, Column: 5},
+						},
+					},
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message:  "test severity override",
+					Severity: rdf.Severity_ERROR,
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2},
+						},
+					},
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "source test",
+					Source: &rdf.Source{
+						Name: "awesome-linter",
+					},
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2},
+						},
+					},
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "code test w/o URL",
+					Location: &rdf.Location{
+						Path:  "sample.new.txt",
+						Range: &rdf.Range{Start: &rdf.Position{Line: 2}},
+					},
+					Code: &rdf.Code{Value: "CODE14"},
+				},
+				ShouldReport: true,
+			},
+		}, {
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "code test w/ URL",
+					Location: &rdf.Location{
+						Path:  "sample.new.txt",
+						Range: &rdf.Range{Start: &rdf.Position{Line: 2}},
+					},
+					Code: &rdf.Code{Value: "CODE14", Url: "https://github.com/reviewdog#CODE14"},
+				},
+				ShouldReport: true,
+			},
+		},
+	}
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs", func(w http.ResponseWriter, r *http.Request) {
+		if err := json.NewEncoder(w).Encode(&github.CheckRun{ID: github.Int64(wantCheckID)}); err != nil {
+			t.Fatal(err)
+		}
+	})
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs/1414", func(w http.ResponseWriter, r *http.Request) {
+		var req github.CheckRun
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			t.Error(err)
+		}
+
+		wantAnnotations := []*github.CheckRunAnnotation{
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("test message"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+				RawDetails:      github.String("raw test message"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(3),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("test multiline"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2-L3"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(3),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("test multiline with column"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2-L3"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				StartColumn:     github.Int(1),
+				EndColumn:       github.Int(5),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("test range comment"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("failure"),
+				Message:         github.String("test severity override"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("source test"),
+				Title:           github.String("[awesome-linter] sample.new.txt#L2"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("code test w/o URL"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2 <CODE14>"),
+			},
+			{
+				Path:            github.String("sample.new.txt"),
+				StartLine:       github.Int(2),
+				EndLine:         github.Int(2),
+				AnnotationLevel: github.String("warning"),
+				Message:         github.String("code test w/ URL"),
+				Title:           github.String("[haya14busa-linter] sample.new.txt#L2 <CODE14>(https://github.com/reviewdog#CODE14)"),
+			},
+		}
+
+		if req.GetStatus() == "completed" {
+			if want := "neutral"; req.GetConclusion() != want {
+				t.Errorf("conclusion = %s, want %s", req.GetConclusion(), want)
+			}
+			if wantTitle := "reviewdog [haya14busa-linter] report"; req.GetOutput().GetTitle() != wantTitle {
+				t.Errorf("title = %s, want %s", req.GetOutput().GetTitle(), wantTitle)
+			}
+			if wantSummary := `reported by [reviewdog](https://github.com/reviewdog/reviewdog) :dog:
+<details>
+<summary>Findings (8)</summary>
+
+[sample.new.txt|2 col 1|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) test message
+[sample.new.txt|2|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) test multiline
+[sample.new.txt|2 col 1|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) test multiline with column
+[sample.new.txt|2 col 1|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) test range comment
+[sample.new.txt|2|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) test severity override
+[sample.new.txt|2|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) source test
+[sample.new.txt|2|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) code test w/o URL
+[sample.new.txt|2|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L2) code test w/ URL
+</details>
+<details>
+<summary>Filtered Findings (1)</summary>
+
+[sample.new.txt|14|](https://github.com/haya14busa/reviewdog/blob/1414/sample.new.txt#L14) test message outside diff
+</details>`; req.GetOutput().GetSummary() != wantSummary {
+				t.Errorf("summary =\n%s\n\nwant\n%s", req.GetOutput().GetSummary(), wantSummary)
+			}
+		} else {
+			if d := cmp.Diff(req.Output.Annotations, wantAnnotations); d != "" {
+				t.Errorf("Annotation diff found:\n%s", d)
+			}
+		}
+	})
+
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := github.NewClient(nil)
+	cli.BaseURL, _ = url.Parse(ts.URL + "/")
+
+	check := &Check{
+		CLI:      cli,
+		Owner:    owner,
+		Repo:     repo,
+		PR:       prNum,
+		SHA:      sha,
+		ToolName: name,
+		Level:    level,
+	}
+
+	for _, c := range comments {
+		if c.Result.ShouldReport {
+			if err := check.Post(context.Background(), c); err != nil {
+				t.Errorf("failed to post: error=%v\ncomment=%v", err, c)
+			}
+		} else {
+			if err := check.PostFiltered(context.Background(), c); err != nil {
+				t.Errorf("failed to post: error=%v\ncomment=%v", err, c)
+			}
+		}
+	}
+	if err := check.Flush(context.Background()); err != nil {
+		t.Error(err)
+	}
+	if check.GetResult().Conclusion != conclusion {
+		t.Errorf("conclusion = %s, want %s", check.GetResult().Conclusion, conclusion)
+	}
+}
+
+func TestCheck_OK_multiple_update_runs(t *testing.T) {
+	const (
+		name        = "haya14busa-linter"
+		owner       = "haya14busa"
+		repo        = "reviewdog"
+		prNum       = 14
+		sha         = "1414"
+		reportURL   = "http://example.com/report_url"
+		conclusion  = "neutral"
+		level       = "warning"
+		wantCheckID = 1414
+	)
+
+	mux := http.NewServeMux()
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs", func(w http.ResponseWriter, r *http.Request) {
+		if err := json.NewEncoder(w).Encode(&github.CheckRun{ID: github.Int64(wantCheckID)}); err != nil {
+			t.Fatal(err)
+		}
+	})
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs/1414", func(w http.ResponseWriter, r *http.Request) {
+		var req github.CheckRun
+		if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+			t.Error(err)
+		}
+		switch len(req.GetOutput().Annotations) {
+		case 0:
+			if req.GetConclusion() != conclusion {
+				t.Errorf("UpdateCheckRunOptions.Conclusion = %q, want %q", req.GetConclusion(), conclusion)
+			}
+		case maxAnnotationsPerRequest, 1: // Expected
+		default:
+			t.Errorf("UpdateCheckRun: len(annotations) = %d, but it's unexpected", len(req.GetOutput().Annotations))
+		}
+	})
+
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := github.NewClient(nil)
+	cli.BaseURL, _ = url.Parse(ts.URL + "/")
+
+	check := &Check{
+		CLI:      cli,
+		Owner:    owner,
+		Repo:     repo,
+		PR:       prNum,
+		SHA:      sha,
+		ToolName: name,
+		Level:    level,
+	}
+
+	for i := 0; i < 101; i++ {
+		if err := check.Post(context.Background(), &reviewdog.Comment{
+			Result: &filter.FilteredDiagnostic{
+				Diagnostic: &rdf.Diagnostic{
+					Message: "test message",
+					Location: &rdf.Location{
+						Path: "sample.new.txt",
+						Range: &rdf.Range{
+							Start: &rdf.Position{Line: 2},
+						},
+					},
+					OriginalOutput: "raw test message",
+				},
+				ShouldReport: true,
+			},
+		}); err != nil {
+			t.Error(err)
+		}
+	}
+	if err := check.Flush(context.Background()); err != nil {
+		t.Error(err)
+	}
+}
+
+func TestCheck_fail_check_with_403_in_GitHub_Actions(t *testing.T) {
+	t.Setenv("GITHUB_ACTIONS", "true")
+	const (
+		name        = "haya14busa-linter"
+		owner       = "haya14busa"
+		repo        = "reviewdog"
+		prNum       = 14
+		sha         = "1414"
+		reportURL   = "http://example.com/report_url"
+		conclusion  = "neutral"
+		level       = "warning"
+		wantCheckID = 1414
+	)
+	mux := http.NewServeMux()
+	mux.HandleFunc("/repos/haya14busa/reviewdog/check-runs", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusForbidden)
+	})
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+	cli := github.NewClient(nil)
+	cli.BaseURL, _ = url.Parse(ts.URL + "/")
+	check := &Check{
+		CLI:      cli,
+		Owner:    owner,
+		Repo:     repo,
+		PR:       prNum,
+		SHA:      sha,
+		ToolName: name,
+		Level:    level,
+	}
+	if err := check.Flush(context.Background()); err != nil {
+		t.Error(err)
+	}
+}
+
+func TestCheck_summary_too_many_findings_cut_off_correctly(t *testing.T) {
+	check := &Check{}
+	var comment []*reviewdog.Comment
+	for i := 0; i < 1000; i++ {
+		comment = append(comment, &reviewdog.Comment{
+			Result: &filter.FilteredDiagnostic{
+				ShouldReport: true,
+				Diagnostic: &rdf.Diagnostic{
+					Message: "this is a pretty long test message that will lead to overshooting the maximum allowed size",
+				},
+			}})
+	}
+	summaryText := check.summary(comment)
+	if len(summaryText) > maxAllowedSize {
+		t.Errorf("summary text is %d bytes long, but the maximum allowed size is %d", len(summaryText), maxAllowedSize)
+	}
+	if !strings.Contains(summaryText, "... (Too many findings. Dropped some findings)\n</details>") {
+		t.Error("summary text was not cut off correctly")
+	}
+}
diff --git a/service/github/diff_test.go b/service/github/diff_test.go
new file mode 100644
index 00000000..81f02eee
--- /dev/null
+++ b/service/github/diff_test.go
@@ -0,0 +1,144 @@
+package github
+
+import (
+	"context"
+	"encoding/json"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+	"testing"
+
+	"github.com/google/go-github/v60/github"
+)
+
+const sampleDiff = `--- a/sample.old.txt	2016-10-13 05:09:35.820791185 +0900
++++ b/sample.new.txt	2016-10-13 05:15:26.839245048 +0900
+@@ -1,3 +1,4 @@
+ unchanged, contextual line
+-deleted line
++added line
++added line
+ unchanged, contextual line
+--- a/nonewline.old.txt	2016-10-13 15:34:14.931778318 +0900
++++ b/nonewline.new.txt	2016-10-13 15:34:14.868444672 +0900
+@@ -1,4 +1,4 @@
+ " vim: nofixeol noendofline
+ No newline at end of both the old and new file
+-a
+-a
+\ No newline at end of file
++b
++b
+\ No newline at end of file
+`
+
+func TestDiff_success(t *testing.T) {
+	mux := http.NewServeMux()
+	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte(sampleDiff))
+	})
+
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := github.NewClient(nil)
+	cli.BaseURL, _ = url.Parse(ts.URL + "/")
+
+	diffService := &PullRequestDiffService{
+		Cli:              cli,
+		Owner:            "o",
+		Repo:             "r",
+		PR:               14,
+		SHA:              "sha",
+		FallBackToGitCLI: false,
+	}
+
+	_, err := diffService.Diff(context.Background())
+	if err != nil {
+		t.Error(err)
+	}
+}
+
+func TestDiff_fallbackToGitCli(t *testing.T) {
+	t.Setenv("REVIEWDOG_SKIP_GIT_FETCH", "true")
+	apiCalled := 0
+	headSHA := "HEAD^"
+	baseSHA := "HEAD"
+	mux := http.NewServeMux()
+	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
+		apiCalled++
+		if r.Method != http.MethodGet {
+			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
+		}
+		if accept := r.Header.Get("Accept"); strings.Contains(accept, "diff") {
+			w.WriteHeader(http.StatusNotAcceptable)
+			return
+		}
+		if accept := r.Header.Get("Accept"); accept != "application/vnd.github.v3+json" {
+			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
+		}
+
+		pullRequestJSON, err := json.Marshal(github.PullRequest{
+			Head: &github.PullRequestBranch{
+				SHA: &headSHA,
+			},
+			Base: &github.PullRequestBranch{
+				SHA: &baseSHA,
+			},
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if _, err := w.Write(pullRequestJSON); err != nil {
+			t.Fatal(err)
+		}
+	})
+	mux.HandleFunc("/repos/o/r/compare/"+headSHA+"..."+baseSHA, func(w http.ResponseWriter, r *http.Request) {
+		if r.Method != http.MethodGet {
+			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
+		}
+		if accept := r.Header.Get("Accept"); accept != "application/vnd.github.v3+json" {
+			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
+		}
+
+		mergeBaseSha := "HEAD^"
+
+		commitsComparisonJSON, err := json.Marshal(github.CommitsComparison{
+			MergeBaseCommit: &github.RepositoryCommit{
+				SHA: &mergeBaseSha,
+			},
+		})
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		if _, err := w.Write(commitsComparisonJSON); err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	ts := httptest.NewServer(mux)
+	defer ts.Close()
+
+	cli := github.NewClient(nil)
+	cli.BaseURL, _ = url.Parse(ts.URL + "/")
+
+	diffService := &PullRequestDiffService{
+		Cli:              cli,
+		Owner:            "o",
+		Repo:             "r",
+		PR:               14,
+		SHA:              "sha",
+		FallBackToGitCLI: true,
+	}
+
+	_, err := diffService.Diff(context.Background())
+	if err != nil {
+		t.Error(err)
+	}
+	if apiCalled != 2 {
+		t.Errorf("GitHub API should be called twice; called %v times", apiCalled)
+	}
+}
diff --git a/service/github/github_test.go b/service/github/github_test.go
index 51626fe4..82358576 100644
--- a/service/github/github_test.go
+++ b/service/github/github_test.go
@@ -94,45 +94,6 @@ func TestGitHubPullRequest_Post(t *testing.T) {
 	}
 }
 
-func TestGitHubPullRequest_Diff(t *testing.T) {
-	if testing.Short() {
-		t.Skip("skipping test which contains actual API requests in short mode")
-	}
-	client := setupGitHubClient()
-	if client == nil {
-		t.Skip(notokenSkipTestMes)
-	}
-
-	want := `diff --git a/.codecov.yml b/.codecov.yml
-index aa49124774..781ee2492f 100644
---- a/.codecov.yml
-+++ b/.codecov.yml
-@@ -7,5 +7,4 @@ coverage:
-       default:
-         target: 0%
- 
--comment:
--  layout: "header"
-+comment: false
-`
-
-	// https://github.com/reviewdog/reviewdog/pull/73
-	owner := "haya14busa"
-	repo := "reviewdog"
-	pr := 73
-	g, err := NewGitHubPullRequest(client, owner, repo, pr, "", "warning")
-	if err != nil {
-		t.Fatal(err)
-	}
-	b, err := g.Diff(context.Background())
-	if err != nil {
-		t.Fatal(err)
-	}
-	if got := string(b); got != want {
-		t.Errorf("got:\n%v\nwant:\n%v", got, want)
-	}
-}
-
 func TestGitHubPullRequest_comment(t *testing.T) {
 	if testing.Short() {
 		t.Skip("skipping test which contains actual API requests in short mode")
@@ -1387,112 +1348,6 @@ func TestGitHubPullRequest_workdir(t *testing.T) {
 	}
 }
 
-func TestGitHubPullRequest_Diff_fake(t *testing.T) {
-	apiCalled := 0
-	mux := http.NewServeMux()
-	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
-		apiCalled++
-		if r.Method != http.MethodGet {
-			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
-		}
-		if accept := r.Header.Get("Accept"); !strings.Contains(accept, "diff") {
-			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
-		}
-		w.Write([]byte("Pull Request diff"))
-	})
-	ts := httptest.NewServer(mux)
-	defer ts.Close()
-
-	cli := github.NewClient(nil)
-	cli.BaseURL, _ = url.Parse(ts.URL + "/")
-	g, err := NewGitHubPullRequest(cli, "o", "r", 14, "sha", "warning")
-	if err != nil {
-		t.Fatal(err)
-	}
-	if _, err := g.Diff(context.Background()); err != nil {
-		t.Fatal(err)
-	}
-	if apiCalled != 1 {
-		t.Errorf("GitHub API should be called once; called %v times", apiCalled)
-	}
-}
-
-func TestGitHubPullRequest_Diff_fake_fallback(t *testing.T) {
-	apiCalled := 0
-	mux := http.NewServeMux()
-	headSHA := "HEAD^"
-	baseSHA := "HEAD"
-	mux.HandleFunc("/repos/o/r/pulls/14", func(w http.ResponseWriter, r *http.Request) {
-		apiCalled++
-		if r.Method != http.MethodGet {
-			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
-		}
-		if accept := r.Header.Get("Accept"); strings.Contains(accept, "diff") {
-			w.WriteHeader(http.StatusNotAcceptable)
-			return
-		}
-		if accept := r.Header.Get("Accept"); accept != "application/vnd.github.v3+json" {
-			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
-		}
-
-		pullRequestJSON, err := json.Marshal(github.PullRequest{
-			Head: &github.PullRequestBranch{
-				SHA: &headSHA,
-			},
-			Base: &github.PullRequestBranch{
-				SHA: &baseSHA,
-			},
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		if _, err := w.Write(pullRequestJSON); err != nil {
-			t.Fatal(err)
-		}
-	})
-	mux.HandleFunc("/repos/o/r/compare/"+headSHA+"..."+baseSHA, func(w http.ResponseWriter, r *http.Request) {
-		if r.Method != http.MethodGet {
-			t.Errorf("unexpected access: %v %v", r.Method, r.URL)
-		}
-		if accept := r.Header.Get("Accept"); accept != "application/vnd.github.v3+json" {
-			t.Errorf("Accept header doesn't contain 'diff': %v", accept)
-		}
-
-		mergeBaseSha := "HEAD^"
-
-		commitsComparisonJSON, err := json.Marshal(github.CommitsComparison{
-			MergeBaseCommit: &github.RepositoryCommit{
-				SHA: &mergeBaseSha,
-			},
-		})
-		if err != nil {
-			t.Fatal(err)
-		}
-
-		if _, err := w.Write(commitsComparisonJSON); err != nil {
-			t.Fatal(err)
-		}
-	})
-	ts := httptest.NewServer(mux)
-	defer ts.Close()
-
-	t.Setenv("REVIEWDOG_SKIP_GIT_FETCH", "true")
-
-	cli := github.NewClient(nil)
-	cli.BaseURL, _ = url.Parse(ts.URL + "/")
-	g, err := NewGitHubPullRequest(cli, "o", "r", 14, "sha", "warning")
-	if err != nil {
-		t.Fatal(err)
-	}
-	if _, err := g.Diff(context.Background()); err != nil {
-		t.Fatal(err)
-	}
-	if apiCalled != 2 {
-		t.Errorf("GitHub API should be called twice; called %v times", apiCalled)
-	}
-}
-
 func TestGitHubPullRequest_Post_NoPermission(t *testing.T) {
 	cwd, _ := os.Getwd()
 	defer os.Chdir(cwd)