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 }