github.com/mistwind/reviewdog@v0.0.0-20230322024206-9cfa11856d58/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  	"sort"
    12  
    13  	"golang.org/x/oauth2"
    14  	"golang.org/x/sync/errgroup"
    15  
    16  	"github.com/mistwind/reviewdog"
    17  	"github.com/mistwind/reviewdog/cienv"
    18  	"github.com/mistwind/reviewdog/doghouse"
    19  	"github.com/mistwind/reviewdog/doghouse/client"
    20  	"github.com/mistwind/reviewdog/filter"
    21  	"github.com/mistwind/reviewdog/project"
    22  	"github.com/mistwind/reviewdog/proto/rdf"
    23  	"github.com/mistwind/reviewdog/service/github/githubutils"
    24  	"github.com/mistwind/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  		diagnostics, 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  			Diagnostics: diagnostics,
   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  		diagnostics := result.Diagnostics
   125  		as := make([]*doghouse.Annotation, 0, len(diagnostics))
   126  		for _, d := range diagnostics {
   127  			as = append(as, checkResultToAnnotation(d, 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  			if err := result.CheckUnexpectedFailure(); err != nil {
   142  				return err
   143  			}
   144  			res, err := cli.Check(ctx, req)
   145  			if err != nil {
   146  				return fmt.Errorf("post failed for %s: %w", name, err)
   147  			}
   148  			if res.ReportURL != "" {
   149  				conclusion := ""
   150  				if res.Conclusion != "" {
   151  					conclusion = fmt.Sprintf(" (conclusion=%s)", res.Conclusion)
   152  				}
   153  				log.Printf("[%s] reported: %s%s", name, res.ReportURL, conclusion)
   154  			} else if res.CheckedResults != nil {
   155  				// Fill results only when report URL is missing, which probably means
   156  				// it failed to report results with Check API.
   157  				filteredResultSet.Store(name, &reviewdog.FilteredResult{
   158  					Level:              result.Level,
   159  					FilteredDiagnostic: res.CheckedResults,
   160  				})
   161  			}
   162  			if res.ReportURL == "" && res.CheckedResults == nil {
   163  				return fmt.Errorf("[%s] no result found", name)
   164  			}
   165  			// If failOnError is on, return error when at least one report
   166  			// returns failure conclusion (status). Users can check this
   167  			// reviewdoc run status (#446) to merge PRs for example.
   168  			//
   169  			// Also, the individual report conclusions are associated to random check
   170  			// suite due to the GitHub bug (#403), so actually users cannot depends
   171  			// on each report as of writing.
   172  			if opt.failOnError && (res.Conclusion == "failure") {
   173  				return fmt.Errorf("[%s] Check conclusion is %q", name, res.Conclusion)
   174  			}
   175  			return nil
   176  		})
   177  	})
   178  	return filteredResultSet, g.Wait()
   179  }
   180  
   181  func checkResultToAnnotation(d *rdf.Diagnostic, wd, gitRelWd string) *doghouse.Annotation {
   182  	d.GetLocation().Path = filter.NormalizePath(d.GetLocation().GetPath(), wd, gitRelWd)
   183  	return &doghouse.Annotation{
   184  		Diagnostic: d,
   185  	}
   186  }
   187  
   188  // reportResults reports results to given io.Writer and possibly to GitHub
   189  // Actions log using logging command.
   190  //
   191  // It returns true if reviewdog should exit with 1.
   192  // e.g. At least one annotation result is in diff.
   193  func reportResults(w io.Writer, filteredResultSet *reviewdog.FilteredResultMap) bool {
   194  	if filteredResultSet.Len() != 0 && cienv.HasReadOnlyPermissionGitHubToken() {
   195  		fmt.Fprintln(os.Stderr, `reviewdog: This GitHub token doesn't have write permission of Review API [1], 
   196  so reviewdog will report results via logging command [2] and create annotations similar to
   197  github-pr-check reporter as a fallback.
   198  [1]: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target, 
   199  [2]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands`)
   200  	}
   201  
   202  	// Sort names to get deterministic result.
   203  	var names []string
   204  	filteredResultSet.Range(func(name string, results *reviewdog.FilteredResult) {
   205  		names = append(names, name)
   206  	})
   207  	sort.Strings(names)
   208  
   209  	shouldFail := false
   210  	foundNumOverall := 0
   211  	for _, name := range names {
   212  		results, err := filteredResultSet.Load(name)
   213  		if err != nil {
   214  			// Should not happen.
   215  			log.Printf("reviewdog: result not found for %q", name)
   216  			continue
   217  		}
   218  		fmt.Fprintf(w, "reviewdog: Reporting results for %q\n", name)
   219  		foundResultPerName := false
   220  		filteredNum := 0
   221  		for _, result := range results.FilteredDiagnostic {
   222  			if !result.ShouldReport {
   223  				filteredNum++
   224  				continue
   225  			}
   226  			foundNumOverall++
   227  			// If it's not running in GitHub Actions, reviewdog should exit with 1
   228  			// if there are at least one result in diff regardless of error level.
   229  			shouldFail = shouldFail || !cienv.IsInGitHubAction() ||
   230  				!(results.Level == "warning" || results.Level == "info")
   231  
   232  			if foundNumOverall == githubutils.MaxLoggingAnnotationsPerStep {
   233  				githubutils.WarnTooManyAnnotationOnce()
   234  				shouldFail = true
   235  			}
   236  
   237  			foundResultPerName = true
   238  			if cienv.IsInGitHubAction() {
   239  				githubutils.ReportAsGitHubActionsLog(name, results.Level, result.Diagnostic)
   240  			} else {
   241  				// Output original lines.
   242  				fmt.Fprintln(w, result.Diagnostic.GetOriginalOutput())
   243  			}
   244  		}
   245  		if !foundResultPerName {
   246  			fmt.Fprintf(w, "reviewdog: No results found for %q. %d results found outside diff.\n", name, filteredNum)
   247  		}
   248  	}
   249  	return shouldFail
   250  }