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