github.com/mistwind/reviewdog@v0.0.0-20230322024206-9cfa11856d58/cmd/reviewdog/main.go (about) 1 package main 2 3 import ( 4 "context" 5 "crypto/tls" 6 "errors" 7 "flag" 8 "fmt" 9 "io" 10 "log" 11 "net/http" 12 "net/url" 13 "os" 14 "os/exec" 15 "sort" 16 "strings" 17 "text/tabwriter" 18 19 "golang.org/x/build/gerrit" 20 "golang.org/x/oauth2" 21 22 "github.com/google/go-github/v39/github" 23 "github.com/mattn/go-shellwords" 24 "github.com/reviewdog/errorformat/fmts" 25 "github.com/xanzy/go-gitlab" 26 27 "github.com/mistwind/reviewdog" 28 "github.com/mistwind/reviewdog/cienv" 29 "github.com/mistwind/reviewdog/commands" 30 "github.com/mistwind/reviewdog/filter" 31 "github.com/mistwind/reviewdog/parser" 32 "github.com/mistwind/reviewdog/project" 33 bbservice "github.com/mistwind/reviewdog/service/bitbucket" 34 gerritservice "github.com/mistwind/reviewdog/service/gerrit" 35 githubservice "github.com/mistwind/reviewdog/service/github" 36 "github.com/mistwind/reviewdog/service/github/githubutils" 37 gitlabservice "github.com/mistwind/reviewdog/service/gitlab" 38 ) 39 40 const usageMessage = "" + 41 `Usage: reviewdog [flags] 42 reviewdog accepts any compiler or linter results from stdin and filters 43 them by diff for review. reviewdog also can posts the results as a comment to 44 GitHub if you use reviewdog in CI service.` 45 46 type option struct { 47 version bool 48 diffCmd string 49 diffStrip int 50 efms strslice 51 f string // format name 52 fDiffStrip int 53 list bool // list supported errorformat name 54 name string // tool name which is used in comment 55 conf string 56 runners string 57 reporter string 58 level string 59 guessPullRequest bool 60 tee bool 61 filterMode filter.Mode 62 failOnError bool 63 } 64 65 const ( 66 diffCmdDoc = `diff command (e.g. "git diff") for local reporter. Do not use --relative flag for git command.` 67 diffStripDoc = "strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)" 68 efmsDoc = `list of supported machine-readable format and errorformat (https://github.com/reviewdog/errorformat)` 69 fDoc = `format name (run -list to see supported format name) for input. It's also used as tool name in review comment if -name is empty` 70 fDiffStripDoc = `option for -f=diff: strip NUM leading components from diff file names (equivalent to 'patch -p') (default is 1 for git diff)` 71 listDoc = `list supported pre-defined format names which can be used as -f arg` 72 nameDoc = `tool name in review comment. -f is used as tool name if -name is empty` 73 74 confDoc = `config file path` 75 runnersDoc = `comma separated runners name to run in config file. default: run all runners` 76 levelDoc = `report level currently used for github-pr-check reporter ("info","warning","error").` 77 guessPullRequestDoc = `guess Pull Request ID by branch name and commit SHA` 78 teeDoc = `enable "tee"-like mode which outputs tools's output as is while reporting results to -reporter. Useful for debugging as well.` 79 filterModeDoc = `how to filter checks results. [added, diff_context, file, nofilter]. 80 "added" (default) 81 Filter by added/modified diff lines. 82 "diff_context" 83 Filter by diff context, which can include unchanged lines. 84 i.e. changed lines +-N lines (e.g. N=3 for default git diff). 85 "file" 86 Filter by added/modified file. 87 "nofilter" 88 Do not filter any results. 89 ` 90 reporterDoc = `reporter of reviewdog results. (local, github-check, github-pr-check, github-pr-review, gitlab-mr-discussion, gitlab-mr-commit, gitlab-push-commit) 91 "local" (default) 92 Report results to stdout. 93 94 "github-check" 95 Report results to GitHub Check. It works both for Pull Requests and commits. 96 For Pull Request, you can see report results in GitHub PullRequest Check 97 tab and can control filtering mode by -filter-mode flag. 98 99 There are two options to use this reporter. 100 101 Option 1) Run reviewdog from GitHub Actions w/ secrets.GITHUB_TOKEN 102 Note that it reports result to GitHub Actions log console for Pull 103 Requests from fork repository due to GitHub Actions restriction. 104 https://help.github.com/en/articles/virtual-environments-for-github-actions#github_token-secret 105 106 Set REVIEWDOG_GITHUB_API_TOKEN with secrets.GITHUB_TOKEN. e.g. 107 REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 108 109 Option 2) Install reviewdog GitHub Apps 110 1. Install reviewdog Apps. https://github.com/apps/reviewdog 111 2. Set REVIEWDOG_TOKEN or run reviewdog CLI in trusted CI providers. 112 You can get token from https://reviewdog.app/gh/<owner>/<repo-name>. 113 $ export REVIEWDOG_TOKEN="xxxxx" 114 115 Note: Token is not required if you run reviewdog in Travis CI. 116 117 "github-pr-check" 118 Same as github-check reporter but it only supports Pull Requests. 119 120 "github-pr-review" 121 Report results to GitHub review comments. 122 123 1. Set REVIEWDOG_GITHUB_API_TOKEN environment variable. 124 Go to https://github.com/settings/tokens and create new Personal access token with repo scope. 125 126 For GitHub Enterprise: 127 $ export GITHUB_API="https://example.githubenterprise.com/api/v3" 128 129 "gitlab-mr-discussion" 130 Report results to GitLab MergeRequest discussion. 131 132 1. Set REVIEWDOG_GITLAB_API_TOKEN environment variable. 133 Go to https://gitlab.com/profile/personal_access_tokens 134 135 CI_API_V4_URL (defined by Gitlab CI) as the base URL for the Gitlab API automatically. 136 Alternatively, GITLAB_API can also be defined, and it will take precedence over the former: 137 $ export GITLAB_API="https://example.gitlab.com/api/v4" 138 139 "gitlab-mr-commit" 140 Same as gitlab-mr-discussion, but report results to GitLab comments for 141 each commits in Merge Requests. 142 143 "gitlab-push-commit" 144 Report results to the latest commit when triggered by gitlab push. 145 the diff is between the latest commit of the push(CI_COMMIT_SHA) and the commit before push(CI_COMMIT_BEFORE_SHA). 146 147 "gerrit-change-review" 148 Report results to Gerrit Change comments. 149 150 1. Set GERRIT_USERNAME and GERRIT_PASSWORD for basic authentication or 151 GIT_GITCOOKIE_PATH for git cookie based authentication. 152 2. Set GERRIT_CHANGE_ID, GERRIT_REVISION_ID GERRIT_BRANCH abd GERRIT_ADDRESS 153 154 For example: 155 $ export GERRIT_CHANGE_ID=myproject~master~I1293efab014de2 156 $ export GERRIT_REVISION_ID=ed318bf9a3c 157 $ export GERRIT_BRANCH=master 158 $ export GERRIT_ADDRESS=http://localhost:8080 159 160 "bitbucket-code-report" 161 Create Bitbucket Code Report via Code Insights 162 (https://confluence.atlassian.com/display/BITBUCKET/Code+insights). 163 You can set custom report name with: 164 165 If running as part of Bitbucket Pipelines no additional configurations is needed. 166 If running outside of Bitbucket Pipelines you need to provide git repo data 167 (see documentation below for local reporters) and BitBucket credentials: 168 - For Basic Auth you need to set following env variables: 169 BITBUCKET_USER and BITBUCKET_PASSWORD 170 - For AccessToken Auth you need to set BITBUCKET_ACCESS_TOKEN 171 172 To post results to Bitbucket Server specify BITBUCKET_SERVER_URL. 173 174 For GitHub Enterprise and self hosted GitLab, set 175 REVIEWDOG_INSECURE_SKIP_VERIFY to skip verifying SSL (please use this at your own risk) 176 $ export REVIEWDOG_INSECURE_SKIP_VERIFY=true 177 178 For non-local reporters, reviewdog automatically get necessary data from 179 environment variable in CI service (GitHub Actions, Travis CI, Circle CI, drone.io, GitLab CI, Bitbucket Pipelines). 180 You can set necessary data with following environment variable manually if 181 you want (e.g. run reviewdog in Jenkins). 182 183 $ export CI_PULL_REQUEST=14 # Pull Request number (e.g. 14) 184 $ export CI_COMMIT="$(git rev-parse @)" # SHA1 for the current build 185 $ export CI_REPO_OWNER="haya14busa" # repository owner 186 $ export CI_REPO_NAME="reviewdog" # repository name 187 ` 188 failOnErrorDoc = `Returns 1 as exit code if any errors/warnings found in input` 189 ) 190 191 var opt = &option{} 192 193 func init() { 194 flag.BoolVar(&opt.version, "version", false, "print version") 195 flag.StringVar(&opt.diffCmd, "diff", "", diffCmdDoc) 196 flag.IntVar(&opt.diffStrip, "strip", 1, diffStripDoc) 197 flag.Var(&opt.efms, "efm", efmsDoc) 198 flag.StringVar(&opt.f, "f", "", fDoc) 199 flag.IntVar(&opt.fDiffStrip, "f.diff.strip", 1, fDiffStripDoc) 200 flag.BoolVar(&opt.list, "list", false, listDoc) 201 flag.StringVar(&opt.name, "name", "", nameDoc) 202 flag.StringVar(&opt.conf, "conf", "", confDoc) 203 flag.StringVar(&opt.runners, "runners", "", runnersDoc) 204 flag.StringVar(&opt.reporter, "reporter", "local", reporterDoc) 205 flag.StringVar(&opt.level, "level", "error", levelDoc) 206 flag.BoolVar(&opt.guessPullRequest, "guess", false, guessPullRequestDoc) 207 flag.BoolVar(&opt.tee, "tee", false, teeDoc) 208 flag.Var(&opt.filterMode, "filter-mode", filterModeDoc) 209 flag.BoolVar(&opt.failOnError, "fail-on-error", false, failOnErrorDoc) 210 } 211 212 func usage() { 213 fmt.Fprintln(os.Stderr, usageMessage) 214 fmt.Fprintln(os.Stderr, "Flags:") 215 flag.PrintDefaults() 216 fmt.Fprintln(os.Stderr, "") 217 fmt.Fprintln(os.Stderr, "See https://github.com/mistwind/reviewdog for more detail.") 218 os.Exit(2) 219 } 220 221 func main() { 222 flag.Usage = usage 223 flag.Parse() 224 if err := run(os.Stdin, os.Stdout, opt); err != nil { 225 fmt.Fprintf(os.Stderr, "reviewdog: %v\n", err) 226 os.Exit(1) 227 } 228 } 229 230 func run(r io.Reader, w io.Writer, opt *option) error { 231 ctx := context.Background() 232 233 if opt.version { 234 fmt.Fprintln(w, commands.Version) 235 return nil 236 } 237 238 if opt.list { 239 return runList(w) 240 } 241 242 if opt.tee { 243 r = io.TeeReader(r, w) 244 } 245 246 // assume it's project based run when both -efm and -f are not specified 247 isProject := len(opt.efms) == 0 && opt.f == "" 248 var projectConf *project.Config 249 250 var cs reviewdog.CommentService 251 var ds reviewdog.DiffService 252 253 if isProject { 254 var err error 255 projectConf, err = projectConfig(opt.conf) 256 if err != nil { 257 return err 258 } 259 260 for k, v := range projectConf.Runner { 261 log.Printf("reviewdog: runner is %s-%s\n", k, *v) 262 } 263 264 cs = reviewdog.NewUnifiedCommentWriter(w) 265 } else { 266 cs = reviewdog.NewRawCommentWriter(w) 267 } 268 269 switch opt.reporter { 270 default: 271 return fmt.Errorf("unknown -reporter: %s", opt.reporter) 272 case "github-check": 273 return runDoghouse(ctx, r, w, opt, isProject, false) 274 case "github-pr-check": 275 return runDoghouse(ctx, r, w, opt, isProject, true) 276 case "github-pr-review": 277 gs, isPR, err := githubService(ctx, opt) 278 if err != nil { 279 return err 280 } 281 if !isPR { 282 fmt.Fprintln(os.Stderr, "reviewdog: this is not PullRequest build.") 283 return nil 284 } 285 // If it's running in GitHub Actions and it's PR from forked repository, 286 // replace comment writer to GitHubActionLogWriter to create annotations 287 // instead of review comment because if it's PR from forked repository, 288 // GitHub token doesn't have write permission due to security concern and 289 // cannot post results via Review API. 290 if cienv.IsInGitHubAction() && cienv.HasReadOnlyPermissionGitHubToken() { 291 fmt.Fprintln(os.Stderr, `reviewdog: This GitHub token doesn't have write permission of Review API [1], 292 so reviewdog will report results via logging command [2] and create annotations similar to 293 github-pr-check reporter as a fallback. 294 [1]: https://docs.github.com/en/actions/reference/events-that-trigger-workflows#pull_request_target, 295 [2]: https://help.github.com/en/actions/automating-your-workflow-with-github-actions/development-tools-for-github-actions#logging-commands`) 296 cs = githubutils.NewGitHubActionLogWriter(opt.level) 297 } else { 298 cs = reviewdog.MultiCommentService(gs, cs) 299 } 300 ds = gs 301 case "gitlab-mr-discussion": 302 build, cli, err := gitlabBuildWithClient(opt.reporter) 303 if err != nil { 304 return err 305 } 306 if build.PullRequest == 0 { 307 fmt.Fprintln(os.Stderr, "this is not MergeRequest build.") 308 return nil 309 } 310 311 gc, err := gitlabservice.NewGitLabMergeRequestDiscussionCommenter(cli, build.Owner, build.Repo, build.PullRequest, build.SHA) 312 if err != nil { 313 return err 314 } 315 316 cs = reviewdog.MultiCommentService(gc, cs) 317 ds, err = gitlabservice.NewGitLabMergeRequestDiff(cli, build.Owner, build.Repo, build.PullRequest, build.SHA) 318 if err != nil { 319 return err 320 } 321 case "gitlab-mr-commit": 322 build, cli, err := gitlabBuildWithClient(opt.reporter) 323 if err != nil { 324 return err 325 } 326 if build.PullRequest == 0 { 327 fmt.Fprintln(os.Stderr, "this is not MergeRequest build.") 328 return nil 329 } 330 331 gc, err := gitlabservice.NewGitLabMergeRequestCommitCommenter(cli, build.Owner, build.Repo, build.PullRequest, build.SHA) 332 if err != nil { 333 return err 334 } 335 336 cs = reviewdog.MultiCommentService(gc, cs) 337 ds, err = gitlabservice.NewGitLabMergeRequestDiff(cli, build.Owner, build.Repo, build.PullRequest, build.SHA) 338 if err != nil { 339 return err 340 } 341 case "gitlab-push-commit": 342 build, cli, err := gitlabBuildWithClient(opt.reporter) 343 if err != nil { 344 return err 345 } 346 log.Printf("reviewdog: [gitlab-push-commit-report] gitlabBuildWithClient ok\n") 347 348 var gc *gitlabservice.PushCommitsCommenter 349 if build.ProjectID != "" { 350 gc, err = gitlabservice.NewGitLabPushCommitsCommenterWithProjectID(cli, build.ProjectID, build.SHA) 351 if err != nil { 352 return err 353 } 354 log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n") 355 356 cs = reviewdog.MultiCommentService(gc, cs) 357 ds, err = gitlabservice.NewGitLabPushCommitsDiffWithProjectID(cli, build.ProjectID, build.SHA, build.BeforeSHA) 358 if err != nil { 359 return err 360 } 361 log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n") 362 } else { 363 gc, err = gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA) 364 if err != nil { 365 return err 366 } 367 log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n") 368 369 cs = reviewdog.MultiCommentService(gc, cs) 370 ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA) 371 if err != nil { 372 return err 373 } 374 log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n") 375 } 376 377 case "gerrit-change-review": 378 b, cli, err := gerritBuildWithClient() 379 if err != nil { 380 return err 381 } 382 gc, err := gerritservice.NewChangeReviewCommenter(cli, b.GerritChangeID, b.GerritRevisionID) 383 if err != nil { 384 return err 385 } 386 cs = gc 387 388 d, err := gerritservice.NewChangeDiff(cli, b.Branch, b.GerritChangeID) 389 if err != nil { 390 return err 391 } 392 ds = d 393 case "bitbucket-code-report": 394 build, client, ct, err := bitbucketBuildWithClient(ctx) 395 if err != nil { 396 return err 397 } 398 ctx = ct 399 400 cs = bbservice.NewReportAnnotator(client, 401 build.Owner, build.Repo, build.SHA, getRunnersList(opt, projectConf)) 402 403 if !(opt.filterMode == filter.ModeDefault || opt.filterMode == filter.ModeNoFilter) { 404 // by default scan whole project with out diff (filter.ModeNoFilter) 405 // Bitbucket pipelines doesn't give an easy way to know 406 // which commit run pipeline before so we can compare between them 407 // however once PR is opened, Bitbucket Reports UI will do automatic 408 // filtering of annotations dividing them in two groups: 409 // - This pull request (10) 410 // - All (50) 411 log.Printf("reviewdog: [bitbucket-code-report] supports only with filter.ModeNoFilter for now") 412 } 413 opt.filterMode = filter.ModeNoFilter 414 ds = &reviewdog.EmptyDiff{} 415 case "local": 416 if opt.diffCmd == "" && opt.filterMode == filter.ModeNoFilter { 417 ds = &reviewdog.EmptyDiff{} 418 } else { 419 d, err := diffService(opt.diffCmd, opt.diffStrip) 420 if err != nil { 421 return err 422 } 423 ds = d 424 } 425 } 426 427 if isProject { 428 return project.Run(ctx, projectConf, buildRunnersMap(opt.runners), cs, ds, opt.tee, opt.filterMode, opt.failOnError) 429 } 430 431 p, err := newParserFromOpt(opt) 432 if err != nil { 433 return err 434 } 435 log.Printf("reviewdog: newParserFromOpt ok\n") 436 437 app := reviewdog.NewReviewdog(toolName(opt), p, cs, ds, opt.filterMode, opt.failOnError) 438 return app.Run(ctx, r) 439 } 440 441 func runList(w io.Writer) error { 442 tabw := tabwriter.NewWriter(w, 0, 8, 0, '\t', 0) 443 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjson", "Reviewdog Diagnostic JSON Format (JSON of DiagnosticResult message)", "https://github.com/mistwind/reviewdog") 444 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjsonl", "Reviewdog Diagnostic JSONL Format (JSONL of Diagnostic message)", "https://github.com/mistwind/reviewdog") 445 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "diff", "Unified Diff Format", "https://en.wikipedia.org/wiki/Diff#Unified_format") 446 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "checkstyle", "checkstyle XML format", "http://checkstyle.sourceforge.net/") 447 for _, f := range sortedFmts(fmts.DefinedFmts()) { 448 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", f.Name, f.Description, f.URL) 449 } 450 return tabw.Flush() 451 } 452 453 type byFmtName []*fmts.Fmt 454 455 func (p byFmtName) Len() int { return len(p) } 456 func (p byFmtName) Less(i, j int) bool { return p[i].Name < p[j].Name } 457 func (p byFmtName) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 458 459 func sortedFmts(fs fmts.Fmts) []*fmts.Fmt { 460 r := make([]*fmts.Fmt, 0, len(fs)) 461 for _, f := range fs { 462 r = append(r, f) 463 } 464 sort.Sort(byFmtName(r)) 465 return r 466 } 467 468 func diffService(s string, strip int) (reviewdog.DiffService, error) { 469 cmds, err := shellwords.Parse(s) 470 if err != nil { 471 return nil, err 472 } 473 if len(cmds) < 1 { 474 return nil, errors.New("diff command is empty") 475 } 476 cmd := exec.Command(cmds[0], cmds[1:]...) 477 d := reviewdog.NewDiffCmd(cmd, strip) 478 return d, nil 479 } 480 481 func newHTTPClient() *http.Client { 482 tr := &http.Transport{ 483 Proxy: http.ProxyFromEnvironment, 484 TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify()}, 485 } 486 return &http.Client{Transport: tr} 487 } 488 489 func insecureSkipVerify() bool { 490 return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true" 491 } 492 493 func githubService(ctx context.Context, opt *option) (gs *githubservice.PullRequest, isPR bool, err error) { 494 token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN") 495 if err != nil { 496 return nil, isPR, err 497 } 498 g, isPR, err := cienv.GetBuildInfo() 499 if err != nil { 500 return nil, isPR, err 501 } 502 503 client, err := githubClient(ctx, token) 504 if err != nil { 505 return nil, isPR, err 506 } 507 508 if !isPR { 509 if !opt.guessPullRequest { 510 return nil, false, nil 511 } 512 513 if g.Branch == "" && g.SHA == "" { 514 return nil, false, nil 515 } 516 517 prID, err := getPullRequestIDByBranchOrCommit(ctx, client, g) 518 if err != nil { 519 fmt.Fprintln(os.Stderr, err) 520 return nil, false, nil 521 } 522 g.PullRequest = prID 523 } 524 525 gs, err = githubservice.NewGitHubPullRequest(client, g.Owner, g.Repo, g.PullRequest, g.SHA) 526 if err != nil { 527 return nil, false, err 528 } 529 return gs, true, nil 530 } 531 532 func getPullRequestIDByBranchOrCommit(ctx context.Context, client *github.Client, info *cienv.BuildInfo) (int, error) { 533 options := &github.SearchOptions{ 534 Sort: "updated", 535 Order: "desc", 536 } 537 538 query := []string{ 539 "type:pr", 540 "state:open", 541 fmt.Sprintf("repo:%s/%s", info.Owner, info.Repo), 542 } 543 if info.Branch != "" { 544 query = append(query, fmt.Sprintf("head:%s", info.Branch)) 545 } 546 if info.SHA != "" { 547 query = append(query, info.SHA) 548 } 549 550 preparedQuery := strings.Join(query, " ") 551 pullRequests, _, err := client.Search.Issues(ctx, preparedQuery, options) 552 if err != nil { 553 return 0, err 554 } 555 556 if *pullRequests.Total == 0 { 557 return 0, fmt.Errorf("reviewdog: PullRequest not found, query: %s", preparedQuery) 558 } 559 560 return *pullRequests.Issues[0].Number, nil 561 } 562 563 func githubClient(ctx context.Context, token string) (*github.Client, error) { 564 ctx = context.WithValue(ctx, oauth2.HTTPClient, newHTTPClient()) 565 ts := oauth2.StaticTokenSource( 566 &oauth2.Token{AccessToken: token}, 567 ) 568 tc := oauth2.NewClient(ctx, ts) 569 client := github.NewClient(tc) 570 var err error 571 client.BaseURL, err = githubBaseURL() 572 return client, err 573 } 574 575 const defaultGitHubAPI = "https://api.github.com/" 576 577 func githubBaseURL() (*url.URL, error) { 578 if baseURL := os.Getenv("GITHUB_API"); baseURL != "" { 579 u, err := url.Parse(baseURL) 580 if err != nil { 581 return nil, fmt.Errorf("GitHub base URL from GITHUB_API is invalid: %v, %w", baseURL, err) 582 } 583 return u, nil 584 } 585 // get GitHub base URL from GitHub Actions' default environment variable GITHUB_API_URL 586 // ref: https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables 587 if baseURL := os.Getenv("GITHUB_API_URL"); baseURL != "" { 588 u, err := url.Parse(baseURL + "/") 589 if err != nil { 590 return nil, fmt.Errorf("GitHub base URL from GITHUB_API_URL is invalid: %v, %w", baseURL, err) 591 } 592 return u, nil 593 } 594 u, err := url.Parse(defaultGitHubAPI) 595 if err != nil { 596 return nil, fmt.Errorf("GitHub base URL from reviewdog default is invalid: %v, %w", defaultGitHubAPI, err) 597 } 598 return u, nil 599 } 600 601 func gitlabBuildWithClient(reporter string) (*cienv.BuildInfo, *gitlab.Client, error) { 602 token, err := nonEmptyEnv("REVIEWDOG_GITLAB_API_TOKEN") 603 if err != nil { 604 return nil, nil, err 605 } 606 607 g, _, err := cienv.GetBuildInfo() 608 if err != nil { 609 return nil, nil, err 610 } 611 612 client, err := gitlabClient(token) 613 if err != nil { 614 return nil, nil, err 615 } 616 617 if reporter == "gitlab-mr-commit" || reporter == "gitlab-mr-discussion" { 618 if g.PullRequest == 0 { 619 prNr, err := fetchMergeRequestIDFromCommit(client, g.Owner+"/"+g.Repo, g.SHA) 620 if err != nil { 621 return nil, nil, err 622 } 623 if prNr != 0 { 624 g.PullRequest = prNr 625 } 626 } 627 } 628 629 return g, client, err 630 } 631 632 func gerritBuildWithClient() (*cienv.BuildInfo, *gerrit.Client, error) { 633 buildInfo, err := cienv.GetGerritBuildInfo() 634 if err != nil { 635 return nil, nil, err 636 } 637 638 gerritAddr := os.Getenv("GERRIT_ADDRESS") 639 if gerritAddr == "" { 640 return nil, nil, errors.New("cannot get gerrit host address from environment variable. Set GERRIT_ADDRESS ?") 641 } 642 643 username := os.Getenv("GERRIT_USERNAME") 644 password := os.Getenv("GERRIT_PASSWORD") 645 if username != "" && password != "" { 646 client := gerrit.NewClient(gerritAddr, gerrit.BasicAuth(username, password)) 647 return buildInfo, client, nil 648 } 649 650 if useGitCookiePath := os.Getenv("GERRIT_GIT_COOKIE_PATH"); useGitCookiePath != "" { 651 client := gerrit.NewClient(gerritAddr, gerrit.GitCookieFileAuth(useGitCookiePath)) 652 return buildInfo, client, nil 653 } 654 655 client := gerrit.NewClient(gerritAddr, gerrit.NoAuth) 656 return buildInfo, client, nil 657 } 658 659 func bitbucketBuildWithClient(ctx context.Context) (*cienv.BuildInfo, bbservice.APIClient, context.Context, error) { 660 build, _, err := cienv.GetBuildInfo() 661 if err != nil { 662 return nil, nil, ctx, err 663 } 664 665 bbUser := os.Getenv("BITBUCKET_USER") 666 bbPass := os.Getenv("BITBUCKET_PASSWORD") 667 bbAccessToken := os.Getenv("BITBUCKET_ACCESS_TOKEN") 668 bbServerURL := os.Getenv("BITBUCKET_SERVER_URL") 669 670 var client bbservice.APIClient 671 if bbServerURL != "" { 672 ctx, err = bbservice.BuildServerAPIContext(ctx, bbServerURL, bbUser, bbPass, bbAccessToken) 673 if err != nil { 674 return nil, nil, ctx, fmt.Errorf("failed to build context for Bitbucket API calls: %w", err) 675 } 676 client = bbservice.NewServerAPIClient() 677 } else { 678 ctx = bbservice.BuildCloudAPIContext(ctx, bbUser, bbPass, bbAccessToken) 679 client = bbservice.NewCloudAPIClient(cienv.IsInBitbucketPipeline(), cienv.IsInBitbucketPipe()) 680 } 681 682 return build, client, ctx, nil 683 } 684 685 func fetchMergeRequestIDFromCommit(cli *gitlab.Client, projectID, sha string) (id int, err error) { 686 // https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests 687 opt := &gitlab.ListProjectMergeRequestsOptions{ 688 State: gitlab.String("opened"), 689 OrderBy: gitlab.String("updated_at"), 690 } 691 mrs, _, err := cli.MergeRequests.ListProjectMergeRequests(projectID, opt) 692 if err != nil { 693 return 0, err 694 } 695 for _, mr := range mrs { 696 if mr.SHA == sha { 697 return mr.IID, nil 698 } 699 } 700 return 0, nil 701 } 702 703 func gitlabClient(token string) (*gitlab.Client, error) { 704 baseURL, err := gitlabBaseURL() 705 if err != nil { 706 return nil, err 707 } 708 client, err := gitlab.NewClient(token, gitlab.WithHTTPClient(newHTTPClient()), gitlab.WithBaseURL(baseURL.String())) 709 if err != nil { 710 return nil, err 711 } 712 return client, nil 713 } 714 715 const defaultGitLabAPI = "https://gitlab.com/api/v4" 716 717 func gitlabBaseURL() (*url.URL, error) { 718 gitlabAPI := os.Getenv("GITLAB_API") 719 gitlabV4URL := os.Getenv("CI_API_V4_URL") 720 721 var baseURL string 722 if gitlabAPI != "" { 723 baseURL = gitlabAPI 724 } else if gitlabV4URL != "" { 725 baseURL = gitlabV4URL 726 } else { 727 baseURL = defaultGitLabAPI 728 } 729 730 u, err := url.Parse(baseURL) 731 if err != nil { 732 return nil, fmt.Errorf("GitLab base URL is invalid: %v, %w", baseURL, err) 733 } 734 return u, nil 735 } 736 737 func nonEmptyEnv(env string) (string, error) { 738 v := os.Getenv(env) 739 if v == "" { 740 return "", fmt.Errorf("environment variable $%v is not set", env) 741 } 742 return v, nil 743 } 744 745 type strslice []string 746 747 func (ss *strslice) String() string { 748 return fmt.Sprintf("%v", *ss) 749 } 750 751 func (ss *strslice) Set(value string) error { 752 *ss = append(*ss, value) 753 return nil 754 } 755 756 func projectConfig(path string) (*project.Config, error) { 757 b, err := readConf(path) 758 if err != nil { 759 return nil, fmt.Errorf("fail to open config: %w", err) 760 } 761 conf, err := project.Parse(b) 762 if err != nil { 763 return nil, fmt.Errorf("config is invalid: %w", err) 764 } 765 return conf, nil 766 } 767 768 func readConf(conf string) ([]byte, error) { 769 var conffiles []string 770 if conf != "" { 771 conffiles = []string{conf} 772 } else { 773 conffiles = []string{ 774 ".reviewdog.yaml", 775 ".reviewdog.yml", 776 "reviewdog.yaml", 777 "reviewdog.yml", 778 } 779 } 780 for _, f := range conffiles { 781 bytes, err := os.ReadFile(f) 782 if err == nil { 783 return bytes, nil 784 } 785 } 786 return nil, errors.New(".reviewdog.yml not found") 787 } 788 789 func newParserFromOpt(opt *option) (parser.Parser, error) { 790 p, err := parser.New(&parser.Option{ 791 FormatName: opt.f, 792 DiffStrip: opt.fDiffStrip, 793 Errorformat: opt.efms, 794 }) 795 if err != nil { 796 return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %w", err) 797 } 798 return p, err 799 } 800 801 func toolName(opt *option) string { 802 name := opt.name 803 if name == "" && opt.f != "" { 804 name = opt.f 805 } 806 return name 807 } 808 809 func buildRunnersMap(runners string) map[string]bool { 810 m := make(map[string]bool) 811 for _, r := range strings.Split(runners, ",") { 812 if name := strings.TrimSpace(r); name != "" { 813 m[name] = true 814 } 815 } 816 return m 817 } 818 819 func getRunnersList(opt *option, conf *project.Config) []string { 820 if len(opt.runners) > 0 { // if runners explicitly defined, use them 821 return strings.Split(opt.runners, ",") 822 } 823 824 if conf != nil { // if this is a Project run, and no explicitly provided runners 825 // if no runners explicitly provided 826 // get all runners from config 827 list := make([]string, 0, len(conf.Runner)) 828 for runner := range conf.Runner { 829 list = append(list, runner) 830 } 831 return list 832 } 833 834 // if this is simple run, get the single tool name 835 if name := toolName(opt); name != "" { 836 return []string{name} 837 } 838 839 return []string{} 840 }