github.com/alexey-mercari/reviewdog@v0.10.1-0.20200514053941-928943b10766/cmd/reviewdog/doghouse.go (about)

     1  package main
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"log"
     9  	"net/http"
    10  	"os"
    11  	"path/filepath"
    12  	"sort"
    13  	"strings"
    14  
    15  	"golang.org/x/oauth2"
    16  	"golang.org/x/sync/errgroup"
    17  
    18  	"github.com/reviewdog/reviewdog"
    19  	"github.com/reviewdog/reviewdog/cienv"
    20  	"github.com/reviewdog/reviewdog/doghouse"
    21  	"github.com/reviewdog/reviewdog/doghouse/client"
    22  	"github.com/reviewdog/reviewdog/project"
    23  	"github.com/reviewdog/reviewdog/service/github/githubutils"
    24  	"github.com/reviewdog/reviewdog/service/serviceutil"
    25  )
    26  
    27  func runDoghouse(ctx context.Context, r io.Reader, w io.Writer, opt *option, isProject bool, forPr bool) error {
    28  	ghInfo, isPr, err := cienv.GetBuildInfo()
    29  	if err != nil {
    30  		return err
    31  	}
    32  	if !isPr && forPr {
    33  		fmt.Fprintln(os.Stderr, "reviewdog: this is not PullRequest build.")
    34  		return nil
    35  	}
    36  	resultSet, err := checkResultSet(ctx, r, opt, isProject)
    37  	if err != nil {
    38  		return err
    39  	}
    40  	cli, err := newDoghouseCli(ctx)
    41  	if err != nil {
    42  		return err
    43  	}
    44  	filteredResultSet, err := postResultSet(ctx, resultSet, ghInfo, cli, opt)
    45  	if err != nil {
    46  		return err
    47  	}
    48  	if foundResultShouldReport := reportResults(w, filteredResultSet); foundResultShouldReport {
    49  		return errors.New("found at least one result in diff")
    50  	}
    51  	return nil
    52  }
    53  
    54  func newDoghouseCli(ctx context.Context) (client.DogHouseClientInterface, error) {
    55  	// If skipDoghouseServer is true, run doghouse code directly instead of talking to
    56  	// the doghouse server because provided GitHub API Token has Check API scope.
    57  	// You can force skipping the doghouse server if you are generating your own application API token.
    58  	skipDoghouseServer := (os.Getenv("REVIEWDOG_SKIP_DOGHOUSE") == "true" || cienv.IsInGitHubAction()) && os.Getenv("REVIEWDOG_TOKEN") == ""
    59  	if skipDoghouseServer {
    60  		token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN")
    61  		if err != nil {
    62  			return nil, err
    63  		}
    64  		ghcli, err := githubClient(ctx, token)
    65  		if err != nil {
    66  			return nil, err
    67  		}
    68  		return &client.GitHubClient{Client: ghcli}, nil
    69  	}
    70  	return newDoghouseServerCli(ctx), nil
    71  }
    72  
    73  func newDoghouseServerCli(ctx context.Context) *client.DogHouseClient {
    74  	httpCli := http.DefaultClient
    75  	if token := os.Getenv("REVIEWDOG_TOKEN"); token != "" {
    76  		ts := oauth2.StaticTokenSource(
    77  			&oauth2.Token{AccessToken: token},
    78  		)
    79  		httpCli = oauth2.NewClient(ctx, ts)
    80  	}
    81  	return client.New(httpCli)
    82  }
    83  
    84  var projectRunAndParse = project.RunAndParse
    85  
    86  func checkResultSet(ctx context.Context, r io.Reader, opt *option, isProject bool) (*reviewdog.ResultMap, error) {
    87  	resultSet := new(reviewdog.ResultMap)
    88  	if isProject {
    89  		conf, err := projectConfig(opt.conf)
    90  		if err != nil {
    91  			return nil, err
    92  		}
    93  		resultSet, err = projectRunAndParse(ctx, conf, buildRunnersMap(opt.runners), opt.level, opt.tee)
    94  		if err != nil {
    95  			return nil, err
    96  		}
    97  	} else {
    98  		p, err := newParserFromOpt(opt)
    99  		if err != nil {
   100  			return nil, err
   101  		}
   102  		rs, err := p.Parse(r)
   103  		if err != nil {
   104  			return nil, err
   105  		}
   106  		resultSet.Store(toolName(opt), &reviewdog.Result{
   107  			Level:        opt.level,
   108  			CheckResults: rs,
   109  		})
   110  	}
   111  	return resultSet, nil
   112  }
   113  
   114  func postResultSet(ctx context.Context, resultSet *reviewdog.ResultMap,
   115  	ghInfo *cienv.BuildInfo, cli client.DogHouseClientInterface, opt *option) (*reviewdog.FilteredResultMap, error) {
   116  	var g errgroup.Group
   117  	wd, _ := os.Getwd()
   118  	gitRelWd, err := serviceutil.GitRelWorkdir()
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	filteredResultSet := new(reviewdog.FilteredResultMap)
   123  	resultSet.Range(func(name string, result *reviewdog.Result) {
   124  		checkResults := result.CheckResults
   125  		as := make([]*doghouse.Annotation, 0, len(checkResults))
   126  		for _, r := range checkResults {
   127  			as = append(as, checkResultToAnnotation(r, wd, gitRelWd))
   128  		}
   129  		req := &doghouse.CheckRequest{
   130  			Name:        name,
   131  			Owner:       ghInfo.Owner,
   132  			Repo:        ghInfo.Repo,
   133  			PullRequest: ghInfo.PullRequest,
   134  			SHA:         ghInfo.SHA,
   135  			Branch:      ghInfo.Branch,
   136  			Annotations: as,
   137  			Level:       result.Level,
   138  			FilterMode:  opt.filterMode,
   139  		}
   140  		g.Go(func() error {
   141  			res, err := cli.Check(ctx, req)
   142  			if err != nil {
   143  				return fmt.Errorf("post failed for %s: %v", name, err)
   144  			}
   145  			if res.ReportURL != "" {
   146  				conclusion := ""
   147  				if res.Conclusion != "" {
   148  					conclusion = fmt.Sprintf(" (conclusion=%s)", res.Conclusion)
   149  				}
   150  				log.Printf("[%s] reported: %s%s", name, res.ReportURL, conclusion)
   151  			} else if res.CheckedResults != nil {
   152  				// Fill results only when report URL is missing, which probably means
   153  				// it failed to report results with Check API.
   154  				filteredResultSet.Store(name, &reviewdog.FilteredResult{
   155  					Level:         result.Level,
   156  					FilteredCheck: res.CheckedResults,
   157  				})
   158  			}
   159  			if res.ReportURL == "" && res.CheckedResults == nil {
   160  				return fmt.Errorf("[%s] no result found", name)
   161  			}
   162  			// If failOnError is on, return error when at least one report
   163  			// returns failure conclusion (status). Users can check this
   164  			// reviewdoc run status (#446) to merge PRs for example.
   165  			//
   166  			// Also, the individual report conclusions are associated to random check
   167  			// suite due to the GitHub bug (#403), so actually users cannot depends
   168  			// on each report as of writing.
   169  			if opt.failOnError && (res.Conclusion == "failure") {
   170  				return fmt.Errorf("[%s] Check conclusion is %q", name, res.Conclusion)
   171  			}
   172  			return nil
   173  		})
   174  	})
   175  	return filteredResultSet, g.Wait()
   176  }
   177  
   178  func checkResultToAnnotation(c *reviewdog.CheckResult, wd, gitRelWd string) *doghouse.Annotation {
   179  	return &doghouse.Annotation{
   180  		Path:       filepath.ToSlash(filepath.Join(gitRelWd, reviewdog.CleanPath(c.Path, wd))),
   181  		Line:       c.Lnum,
   182  		Message:    c.Message,
   183  		RawMessage: strings.Join(c.Lines, "\n"),
   184  	}
   185  }
   186  
   187  // reportResults reports results to given io.Writer and possibly to GitHub
   188  // Actions log using logging command.
   189  //
   190  // It returns true if reviewdog should exit with 1.
   191  // e.g. At least one annotation result is in diff.
   192  func reportResults(w io.Writer, filteredResultSet *reviewdog.FilteredResultMap) bool {
   193  	if filteredResultSet.Len() != 0 && isPRFromForkedRepo() {
   194  		fmt.Fprintln(w, `reviewdog: This is Pull-Request from forked repository.
   195  GitHub token doesn't have write permission of Check API, so reviewdog will
   196  report results via logging command [1].
   197  
   198  [1]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands`)
   199  	}
   200  
   201  	// Sort names to get deterministic result.
   202  	var names []string
   203  	filteredResultSet.Range(func(name string, results *reviewdog.FilteredResult) {
   204  		names = append(names, name)
   205  	})
   206  	sort.Strings(names)
   207  
   208  	shouldFail := false
   209  	foundNumOverall := 0
   210  	for _, name := range names {
   211  		results, err := filteredResultSet.Load(name)
   212  		if err != nil {
   213  			// Should not happen.
   214  			log.Printf("reviewdog: result not found for %q", name)
   215  			continue
   216  		}
   217  		fmt.Fprintf(w, "reviewdog: Reporting results for %q\n", name)
   218  		foundResultPerName := false
   219  		filteredNum := 0
   220  		for _, result := range results.FilteredCheck {
   221  			if !result.ShouldReport {
   222  				filteredNum++
   223  				continue
   224  			}
   225  			foundNumOverall++
   226  			// If it's not running in GitHub Actions, reviewdog should exit with 1
   227  			// if there are at least one result in diff regardless of error level.
   228  			shouldFail = shouldFail || !cienv.IsInGitHubAction() ||
   229  				!(results.Level == "warning" || results.Level == "info")
   230  
   231  			if foundNumOverall == githubutils.MaxLoggingAnnotationsPerStep {
   232  				githubutils.WarnTooManyAnnotationOnce()
   233  				shouldFail = true
   234  			}
   235  
   236  			foundResultPerName = true
   237  			if cienv.IsInGitHubAction() {
   238  				githubutils.ReportAsGitHubActionsLog(name, results.Level, result.CheckResult)
   239  			} else {
   240  				// Output original lines.
   241  				for _, line := range result.Lines {
   242  					fmt.Fprintln(w, line)
   243  				}
   244  			}
   245  		}
   246  		if !foundResultPerName {
   247  			fmt.Fprintf(w, "reviewdog: No results found for %q. %d results found outside diff.\n", name, filteredNum)
   248  		}
   249  	}
   250  	return shouldFail
   251  }
   252  
   253  func isPRFromForkedRepo() bool {
   254  	event, err := cienv.LoadGitHubEvent()
   255  	if err != nil {
   256  		return false
   257  	}
   258  	return event.PullRequest.Head.Repo.Fork
   259  }