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