github.com/Mistwind/reviewdog@v0.0.0-20230317041057-48e69b6d9e86/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/reviewdog/reviewdog" 28 "github.com/reviewdog/reviewdog/cienv" 29 "github.com/reviewdog/reviewdog/commands" 30 "github.com/reviewdog/reviewdog/filter" 31 "github.com/reviewdog/reviewdog/parser" 32 "github.com/reviewdog/reviewdog/project" 33 bbservice "github.com/reviewdog/reviewdog/service/bitbucket" 34 gerritservice "github.com/reviewdog/reviewdog/service/gerrit" 35 githubservice "github.com/reviewdog/reviewdog/service/github" 36 "github.com/reviewdog/reviewdog/service/github/githubutils" 37 gitlabservice "github.com/reviewdog/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/reviewdog/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 gc, err := gitlabservice.NewGitLabPushCommitsCommenter(cli, build.Owner, build.Repo, build.SHA) 349 if err != nil { 350 return err 351 } 352 log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsCommenter ok\n") 353 354 cs = reviewdog.MultiCommentService(gc, cs) 355 ds, err = gitlabservice.NewGitLabPushCommitsDiff(cli, build.Owner, build.Repo, build.SHA, build.BeforeSHA) 356 if err != nil { 357 return err 358 } 359 log.Printf("reviewdog: [gitlab-push-commit-report] NewGitLabPushCommitsDiff ok\n") 360 361 case "gerrit-change-review": 362 b, cli, err := gerritBuildWithClient() 363 if err != nil { 364 return err 365 } 366 gc, err := gerritservice.NewChangeReviewCommenter(cli, b.GerritChangeID, b.GerritRevisionID) 367 if err != nil { 368 return err 369 } 370 cs = gc 371 372 d, err := gerritservice.NewChangeDiff(cli, b.Branch, b.GerritChangeID) 373 if err != nil { 374 return err 375 } 376 ds = d 377 case "bitbucket-code-report": 378 build, client, ct, err := bitbucketBuildWithClient(ctx) 379 if err != nil { 380 return err 381 } 382 ctx = ct 383 384 cs = bbservice.NewReportAnnotator(client, 385 build.Owner, build.Repo, build.SHA, getRunnersList(opt, projectConf)) 386 387 if !(opt.filterMode == filter.ModeDefault || opt.filterMode == filter.ModeNoFilter) { 388 // by default scan whole project with out diff (filter.ModeNoFilter) 389 // Bitbucket pipelines doesn't give an easy way to know 390 // which commit run pipeline before so we can compare between them 391 // however once PR is opened, Bitbucket Reports UI will do automatic 392 // filtering of annotations dividing them in two groups: 393 // - This pull request (10) 394 // - All (50) 395 log.Printf("reviewdog: [bitbucket-code-report] supports only with filter.ModeNoFilter for now") 396 } 397 opt.filterMode = filter.ModeNoFilter 398 ds = &reviewdog.EmptyDiff{} 399 case "local": 400 if opt.diffCmd == "" && opt.filterMode == filter.ModeNoFilter { 401 ds = &reviewdog.EmptyDiff{} 402 } else { 403 d, err := diffService(opt.diffCmd, opt.diffStrip) 404 if err != nil { 405 return err 406 } 407 ds = d 408 } 409 } 410 411 if isProject { 412 return project.Run(ctx, projectConf, buildRunnersMap(opt.runners), cs, ds, opt.tee, opt.filterMode, opt.failOnError) 413 } 414 415 p, err := newParserFromOpt(opt) 416 if err != nil { 417 return err 418 } 419 log.Printf("reviewdog: newParserFromOpt ok\n") 420 421 app := reviewdog.NewReviewdog(toolName(opt), p, cs, ds, opt.filterMode, opt.failOnError) 422 return app.Run(ctx, r) 423 } 424 425 func runList(w io.Writer) error { 426 tabw := tabwriter.NewWriter(w, 0, 8, 0, '\t', 0) 427 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjson", "Reviewdog Diagnostic JSON Format (JSON of DiagnosticResult message)", "https://github.com/reviewdog/reviewdog") 428 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "rdjsonl", "Reviewdog Diagnostic JSONL Format (JSONL of Diagnostic message)", "https://github.com/reviewdog/reviewdog") 429 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "diff", "Unified Diff Format", "https://en.wikipedia.org/wiki/Diff#Unified_format") 430 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", "checkstyle", "checkstyle XML format", "http://checkstyle.sourceforge.net/") 431 for _, f := range sortedFmts(fmts.DefinedFmts()) { 432 fmt.Fprintf(tabw, "%s\t%s\t- %s\n", f.Name, f.Description, f.URL) 433 } 434 return tabw.Flush() 435 } 436 437 type byFmtName []*fmts.Fmt 438 439 func (p byFmtName) Len() int { return len(p) } 440 func (p byFmtName) Less(i, j int) bool { return p[i].Name < p[j].Name } 441 func (p byFmtName) Swap(i, j int) { p[i], p[j] = p[j], p[i] } 442 443 func sortedFmts(fs fmts.Fmts) []*fmts.Fmt { 444 r := make([]*fmts.Fmt, 0, len(fs)) 445 for _, f := range fs { 446 r = append(r, f) 447 } 448 sort.Sort(byFmtName(r)) 449 return r 450 } 451 452 func diffService(s string, strip int) (reviewdog.DiffService, error) { 453 cmds, err := shellwords.Parse(s) 454 if err != nil { 455 return nil, err 456 } 457 if len(cmds) < 1 { 458 return nil, errors.New("diff command is empty") 459 } 460 cmd := exec.Command(cmds[0], cmds[1:]...) 461 d := reviewdog.NewDiffCmd(cmd, strip) 462 return d, nil 463 } 464 465 func newHTTPClient() *http.Client { 466 tr := &http.Transport{ 467 Proxy: http.ProxyFromEnvironment, 468 TLSClientConfig: &tls.Config{InsecureSkipVerify: insecureSkipVerify()}, 469 } 470 return &http.Client{Transport: tr} 471 } 472 473 func insecureSkipVerify() bool { 474 return os.Getenv("REVIEWDOG_INSECURE_SKIP_VERIFY") == "true" 475 } 476 477 func githubService(ctx context.Context, opt *option) (gs *githubservice.PullRequest, isPR bool, err error) { 478 token, err := nonEmptyEnv("REVIEWDOG_GITHUB_API_TOKEN") 479 if err != nil { 480 return nil, isPR, err 481 } 482 g, isPR, err := cienv.GetBuildInfo() 483 if err != nil { 484 return nil, isPR, err 485 } 486 487 client, err := githubClient(ctx, token) 488 if err != nil { 489 return nil, isPR, err 490 } 491 492 if !isPR { 493 if !opt.guessPullRequest { 494 return nil, false, nil 495 } 496 497 if g.Branch == "" && g.SHA == "" { 498 return nil, false, nil 499 } 500 501 prID, err := getPullRequestIDByBranchOrCommit(ctx, client, g) 502 if err != nil { 503 fmt.Fprintln(os.Stderr, err) 504 return nil, false, nil 505 } 506 g.PullRequest = prID 507 } 508 509 gs, err = githubservice.NewGitHubPullRequest(client, g.Owner, g.Repo, g.PullRequest, g.SHA) 510 if err != nil { 511 return nil, false, err 512 } 513 return gs, true, nil 514 } 515 516 func getPullRequestIDByBranchOrCommit(ctx context.Context, client *github.Client, info *cienv.BuildInfo) (int, error) { 517 options := &github.SearchOptions{ 518 Sort: "updated", 519 Order: "desc", 520 } 521 522 query := []string{ 523 "type:pr", 524 "state:open", 525 fmt.Sprintf("repo:%s/%s", info.Owner, info.Repo), 526 } 527 if info.Branch != "" { 528 query = append(query, fmt.Sprintf("head:%s", info.Branch)) 529 } 530 if info.SHA != "" { 531 query = append(query, info.SHA) 532 } 533 534 preparedQuery := strings.Join(query, " ") 535 pullRequests, _, err := client.Search.Issues(ctx, preparedQuery, options) 536 if err != nil { 537 return 0, err 538 } 539 540 if *pullRequests.Total == 0 { 541 return 0, fmt.Errorf("reviewdog: PullRequest not found, query: %s", preparedQuery) 542 } 543 544 return *pullRequests.Issues[0].Number, nil 545 } 546 547 func githubClient(ctx context.Context, token string) (*github.Client, error) { 548 ctx = context.WithValue(ctx, oauth2.HTTPClient, newHTTPClient()) 549 ts := oauth2.StaticTokenSource( 550 &oauth2.Token{AccessToken: token}, 551 ) 552 tc := oauth2.NewClient(ctx, ts) 553 client := github.NewClient(tc) 554 var err error 555 client.BaseURL, err = githubBaseURL() 556 return client, err 557 } 558 559 const defaultGitHubAPI = "https://api.github.com/" 560 561 func githubBaseURL() (*url.URL, error) { 562 if baseURL := os.Getenv("GITHUB_API"); baseURL != "" { 563 u, err := url.Parse(baseURL) 564 if err != nil { 565 return nil, fmt.Errorf("GitHub base URL from GITHUB_API is invalid: %v, %w", baseURL, err) 566 } 567 return u, nil 568 } 569 // get GitHub base URL from GitHub Actions' default environment variable GITHUB_API_URL 570 // ref: https://docs.github.com/en/actions/reference/environment-variables#default-environment-variables 571 if baseURL := os.Getenv("GITHUB_API_URL"); baseURL != "" { 572 u, err := url.Parse(baseURL + "/") 573 if err != nil { 574 return nil, fmt.Errorf("GitHub base URL from GITHUB_API_URL is invalid: %v, %w", baseURL, err) 575 } 576 return u, nil 577 } 578 u, err := url.Parse(defaultGitHubAPI) 579 if err != nil { 580 return nil, fmt.Errorf("GitHub base URL from reviewdog default is invalid: %v, %w", defaultGitHubAPI, err) 581 } 582 return u, nil 583 } 584 585 func gitlabBuildWithClient(reporter string) (*cienv.BuildInfo, *gitlab.Client, error) { 586 token, err := nonEmptyEnv("REVIEWDOG_GITLAB_API_TOKEN") 587 if err != nil { 588 return nil, nil, err 589 } 590 591 g, _, err := cienv.GetBuildInfo() 592 if err != nil { 593 return nil, nil, err 594 } 595 596 client, err := gitlabClient(token) 597 if err != nil { 598 return nil, nil, err 599 } 600 601 if reporter == "gitlab-mr-commit" || reporter == "gitlab-mr-discussion" { 602 if g.PullRequest == 0 { 603 prNr, err := fetchMergeRequestIDFromCommit(client, g.Owner+"/"+g.Repo, g.SHA) 604 if err != nil { 605 return nil, nil, err 606 } 607 if prNr != 0 { 608 g.PullRequest = prNr 609 } 610 } 611 } 612 613 return g, client, err 614 } 615 616 func gerritBuildWithClient() (*cienv.BuildInfo, *gerrit.Client, error) { 617 buildInfo, err := cienv.GetGerritBuildInfo() 618 if err != nil { 619 return nil, nil, err 620 } 621 622 gerritAddr := os.Getenv("GERRIT_ADDRESS") 623 if gerritAddr == "" { 624 return nil, nil, errors.New("cannot get gerrit host address from environment variable. Set GERRIT_ADDRESS ?") 625 } 626 627 username := os.Getenv("GERRIT_USERNAME") 628 password := os.Getenv("GERRIT_PASSWORD") 629 if username != "" && password != "" { 630 client := gerrit.NewClient(gerritAddr, gerrit.BasicAuth(username, password)) 631 return buildInfo, client, nil 632 } 633 634 if useGitCookiePath := os.Getenv("GERRIT_GIT_COOKIE_PATH"); useGitCookiePath != "" { 635 client := gerrit.NewClient(gerritAddr, gerrit.GitCookieFileAuth(useGitCookiePath)) 636 return buildInfo, client, nil 637 } 638 639 client := gerrit.NewClient(gerritAddr, gerrit.NoAuth) 640 return buildInfo, client, nil 641 } 642 643 func bitbucketBuildWithClient(ctx context.Context) (*cienv.BuildInfo, bbservice.APIClient, context.Context, error) { 644 build, _, err := cienv.GetBuildInfo() 645 if err != nil { 646 return nil, nil, ctx, err 647 } 648 649 bbUser := os.Getenv("BITBUCKET_USER") 650 bbPass := os.Getenv("BITBUCKET_PASSWORD") 651 bbAccessToken := os.Getenv("BITBUCKET_ACCESS_TOKEN") 652 bbServerURL := os.Getenv("BITBUCKET_SERVER_URL") 653 654 var client bbservice.APIClient 655 if bbServerURL != "" { 656 ctx, err = bbservice.BuildServerAPIContext(ctx, bbServerURL, bbUser, bbPass, bbAccessToken) 657 if err != nil { 658 return nil, nil, ctx, fmt.Errorf("failed to build context for Bitbucket API calls: %w", err) 659 } 660 client = bbservice.NewServerAPIClient() 661 } else { 662 ctx = bbservice.BuildCloudAPIContext(ctx, bbUser, bbPass, bbAccessToken) 663 client = bbservice.NewCloudAPIClient(cienv.IsInBitbucketPipeline(), cienv.IsInBitbucketPipe()) 664 } 665 666 return build, client, ctx, nil 667 } 668 669 func fetchMergeRequestIDFromCommit(cli *gitlab.Client, projectID, sha string) (id int, err error) { 670 // https://docs.gitlab.com/ce/api/merge_requests.html#list-project-merge-requests 671 opt := &gitlab.ListProjectMergeRequestsOptions{ 672 State: gitlab.String("opened"), 673 OrderBy: gitlab.String("updated_at"), 674 } 675 mrs, _, err := cli.MergeRequests.ListProjectMergeRequests(projectID, opt) 676 if err != nil { 677 return 0, err 678 } 679 for _, mr := range mrs { 680 if mr.SHA == sha { 681 return mr.IID, nil 682 } 683 } 684 return 0, nil 685 } 686 687 func gitlabClient(token string) (*gitlab.Client, error) { 688 baseURL, err := gitlabBaseURL() 689 if err != nil { 690 return nil, err 691 } 692 client, err := gitlab.NewClient(token, gitlab.WithHTTPClient(newHTTPClient()), gitlab.WithBaseURL(baseURL.String())) 693 if err != nil { 694 return nil, err 695 } 696 return client, nil 697 } 698 699 const defaultGitLabAPI = "https://gitlab.com/api/v4" 700 701 func gitlabBaseURL() (*url.URL, error) { 702 gitlabAPI := os.Getenv("GITLAB_API") 703 gitlabV4URL := os.Getenv("CI_API_V4_URL") 704 705 var baseURL string 706 if gitlabAPI != "" { 707 baseURL = gitlabAPI 708 } else if gitlabV4URL != "" { 709 baseURL = gitlabV4URL 710 } else { 711 baseURL = defaultGitLabAPI 712 } 713 714 u, err := url.Parse(baseURL) 715 if err != nil { 716 return nil, fmt.Errorf("GitLab base URL is invalid: %v, %w", baseURL, err) 717 } 718 return u, nil 719 } 720 721 func nonEmptyEnv(env string) (string, error) { 722 v := os.Getenv(env) 723 if v == "" { 724 return "", fmt.Errorf("environment variable $%v is not set", env) 725 } 726 return v, nil 727 } 728 729 type strslice []string 730 731 func (ss *strslice) String() string { 732 return fmt.Sprintf("%v", *ss) 733 } 734 735 func (ss *strslice) Set(value string) error { 736 *ss = append(*ss, value) 737 return nil 738 } 739 740 func projectConfig(path string) (*project.Config, error) { 741 b, err := readConf(path) 742 if err != nil { 743 return nil, fmt.Errorf("fail to open config: %w", err) 744 } 745 conf, err := project.Parse(b) 746 if err != nil { 747 return nil, fmt.Errorf("config is invalid: %w", err) 748 } 749 return conf, nil 750 } 751 752 func readConf(conf string) ([]byte, error) { 753 var conffiles []string 754 if conf != "" { 755 conffiles = []string{conf} 756 } else { 757 conffiles = []string{ 758 ".reviewdog.yaml", 759 ".reviewdog.yml", 760 "reviewdog.yaml", 761 "reviewdog.yml", 762 } 763 } 764 for _, f := range conffiles { 765 bytes, err := os.ReadFile(f) 766 if err == nil { 767 return bytes, nil 768 } 769 } 770 return nil, errors.New(".reviewdog.yml not found") 771 } 772 773 func newParserFromOpt(opt *option) (parser.Parser, error) { 774 p, err := parser.New(&parser.Option{ 775 FormatName: opt.f, 776 DiffStrip: opt.fDiffStrip, 777 Errorformat: opt.efms, 778 }) 779 if err != nil { 780 return nil, fmt.Errorf("fail to create parser. use either -f or -efm: %w", err) 781 } 782 return p, err 783 } 784 785 func toolName(opt *option) string { 786 name := opt.name 787 if name == "" && opt.f != "" { 788 name = opt.f 789 } 790 return name 791 } 792 793 func buildRunnersMap(runners string) map[string]bool { 794 m := make(map[string]bool) 795 for _, r := range strings.Split(runners, ",") { 796 if name := strings.TrimSpace(r); name != "" { 797 m[name] = true 798 } 799 } 800 return m 801 } 802 803 func getRunnersList(opt *option, conf *project.Config) []string { 804 if len(opt.runners) > 0 { // if runners explicitly defined, use them 805 return strings.Split(opt.runners, ",") 806 } 807 808 if conf != nil { // if this is a Project run, and no explicitly provided runners 809 // if no runners explicitly provided 810 // get all runners from config 811 list := make([]string, 0, len(conf.Runner)) 812 for runner := range conf.Runner { 813 list = append(list, runner) 814 } 815 return list 816 } 817 818 // if this is simple run, get the single tool name 819 if name := toolName(opt); name != "" { 820 return []string{name} 821 } 822 823 return []string{} 824 }