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 }