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