diff --git a/.ci/magician/go.mod b/.ci/magician/go.mod index 61ce18c80238..0ab0e7e857a4 100644 --- a/.ci/magician/go.mod +++ b/.ci/magician/go.mod @@ -22,8 +22,7 @@ require ( ) require ( - cloud.google.com/go/compute v1.19.1 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/golang/glog v1.1.1 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect @@ -36,7 +35,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect go.opencensus.io v0.24.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.7.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/appengine v1.6.7 // indirect diff --git a/.ci/magician/go.sum b/.ci/magician/go.sum index ec29a418f5c0..e2cf9c686669 100644 --- a/.ci/magician/go.sum +++ b/.ci/magician/go.sum @@ -1,9 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -cloud.google.com/go/compute v1.19.1 h1:am86mquDUgjGNWxiGn+5PGLbmgiWXlE/yNWpIpNvuXY= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= @@ -109,8 +107,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.7.0 h1:qe6s0zUXlPX80/dITx3440hWZ7GwMwgDDyrSGTPJG/g= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 3efa48f2f59c..ebbbca8699ac 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -19,6 +19,7 @@ permissions: read-all jobs: analysis: + if: github.repository == 'GoogleCloudPlatform/magic-modules' name: Scorecard analysis runs-on: ubuntu-22.04 permissions: diff --git a/.github/workflows/teamcity-services-diff-check-weekly.yml b/.github/workflows/teamcity-services-diff-check-weekly.yml index 877c98844bfb..f36999c7392b 100644 --- a/.github/workflows/teamcity-services-diff-check-weekly.yml +++ b/.github/workflows/teamcity-services-diff-check-weekly.yml @@ -12,38 +12,39 @@ on: jobs: teamcity-services-diff-check: - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: ubuntu-22.04 - steps: - - name: Checkout Repository - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 - - name: TeamCity Google Provider Generate - uses: ./.github/actions/build-downstream - with: - repo: 'terraform-provider-google' - token: '$GITHUB_TOKEN' - # The path where GA/Beta providers are generated is grabbed from the OUTPUT_PATH that's set in build_downstream.yaml - # export OUTPUT_PATH=$GOPATH/src/github.com/$UPSTREAM_OWNER/$GH_REPO - # OUTPUT_PATH changes after each generate (GA/beta) - - name: Set GOOGLE_REPO_PATH to path where GA provider was generated - run: echo "GOOGLE_REPO_PATH=${{ env.OUTPUT_PATH}}" >> $GITHUB_ENV - - name: TeamCity Google Beta Provider Generate - uses: ./.github/actions/build-downstream - with: - repo: 'terraform-provider-google-beta' - token: '$GITHUB_TOKEN' - - name: Set GOOGLE_BETA_REPO_PATH to path where beta provider was generated - run: echo "GOOGLE_BETA_REPO_PATH=${{ env.OUTPUT_PATH}}" >> $GITHUB_ENV - - name: Check that new services have been added to the TeamCity configuration code - run: | - # Create lists of service packages in providers. Need to cd into repos where go.mod is to do this command. - cd ${{env.GOOGLE_REPO_PATH}} - go list -f '{{.Name}}' ${{env.GOOGLE_REPO_PATH}}/google/services/... > $GITHUB_WORKSPACE/provider_services_ga.txt - cd ${{env.GOOGLE_BETA_REPO_PATH}} - go list -f '{{.Name}}' ${{env.GOOGLE_BETA_REPO_PATH}}/google-beta/services/... > $GITHUB_WORKSPACE/provider_services_beta.txt + if: github.repository == 'GoogleCloudPlatform/magic-modules' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: ubuntu-22.04 + steps: + - name: Checkout Repository + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.2 + - name: TeamCity Google Provider Generate + uses: ./.github/actions/build-downstream + with: + repo: 'terraform-provider-google' + token: '$GITHUB_TOKEN' + # The path where GA/Beta providers are generated is grabbed from the OUTPUT_PATH that's set in build_downstream.yaml + # export OUTPUT_PATH=$GOPATH/src/github.com/$UPSTREAM_OWNER/$GH_REPO + # OUTPUT_PATH changes after each generate (GA/beta) + - name: Set GOOGLE_REPO_PATH to path where GA provider was generated + run: echo "GOOGLE_REPO_PATH=${{ env.OUTPUT_PATH}}" >> $GITHUB_ENV + - name: TeamCity Google Beta Provider Generate + uses: ./.github/actions/build-downstream + with: + repo: 'terraform-provider-google-beta' + token: '$GITHUB_TOKEN' + - name: Set GOOGLE_BETA_REPO_PATH to path where beta provider was generated + run: echo "GOOGLE_BETA_REPO_PATH=${{ env.OUTPUT_PATH}}" >> $GITHUB_ENV + - name: Check that new services have been added to the TeamCity configuration code + run: | + # Create lists of service packages in providers. Need to cd into repos where go.mod is to do this command. + cd ${{env.GOOGLE_REPO_PATH}} + go list -f '{{.Name}}' ${{env.GOOGLE_REPO_PATH}}/google/services/... > $GITHUB_WORKSPACE/provider_services_ga.txt + cd ${{env.GOOGLE_BETA_REPO_PATH}} + go list -f '{{.Name}}' ${{env.GOOGLE_BETA_REPO_PATH}}/google-beta/services/... > $GITHUB_WORKSPACE/provider_services_beta.txt - # Run tool to compare service packages in the providers vs those listed in TeamCity config files - cd $GITHUB_WORKSPACE - go run ./tools/teamcity-diff-check/main.go -version=ga -provider_services=./provider_services_ga.txt -teamcity_services=./mmv1/third_party/terraform/.teamcity/components/inputs/services_ga.kt - go run ./tools/teamcity-diff-check/main.go -version=beta -provider_services=./provider_services_beta.txt -teamcity_services=./mmv1/third_party/terraform/.teamcity/components/inputs/services_beta.kt + # Run tool to compare service packages in the providers vs those listed in TeamCity config files + cd $GITHUB_WORKSPACE + go run ./tools/teamcity-diff-check/main.go -version=ga -provider_services=./provider_services_ga.txt -teamcity_services=./mmv1/third_party/terraform/.teamcity/components/inputs/services_ga.kt + go run ./tools/teamcity-diff-check/main.go -version=beta -provider_services=./provider_services_beta.txt -teamcity_services=./mmv1/third_party/terraform/.teamcity/components/inputs/services_beta.kt diff --git a/tools/issue-labeler/constants/const.go b/tools/issue-labeler/constants/const.go new file mode 100644 index 000000000000..74acf320dca6 --- /dev/null +++ b/tools/issue-labeler/constants/const.go @@ -0,0 +1,3 @@ +package constants + +const GITHUB_YELLOW = "fbca04" diff --git a/tools/issue-labeler/go.mod b/tools/issue-labeler/go.mod index 7c7ed5edc1f2..98c26dfa69fe 100644 --- a/tools/issue-labeler/go.mod +++ b/tools/issue-labeler/go.mod @@ -4,13 +4,16 @@ go 1.23 require ( github.com/golang/glog v1.1.1 + github.com/google/go-github/v61 v61.0.0 + github.com/spf13/cobra v1.8.1 golang.org/x/exp v0.0.0-20230810033253-352e893a4cad + golang.org/x/oauth2 v0.24.0 gopkg.in/yaml.v2 v2.4.0 ) require ( + github.com/google/go-querystring v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.8.1 // indirect github.com/spf13/pflag v1.0.5 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/tools/issue-labeler/go.sum b/tools/issue-labeler/go.sum index 40345351b651..6bf2d02dc507 100644 --- a/tools/issue-labeler/go.sum +++ b/tools/issue-labeler/go.sum @@ -1,6 +1,13 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= github.com/golang/glog v1.1.1/go.mod h1:zR+okUeTbrL6EL3xHUDxZuEtGv04p5shwip1+mL/rLQ= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v61 v61.0.0 h1:VwQCBwhyE9JclCI+22/7mLB1PuU9eowCXKY5pNlu1go= +github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= @@ -15,6 +22,9 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad h1:g0bG7Z4uG+OgH2QDODnjp6ggkk1bJDsINcuWmJN1iJU= golang.org/x/exp v0.0.0-20230810033253-352e893a4cad/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/tools/issue-labeler/labeler/backfill.go b/tools/issue-labeler/labeler/backfill.go index 21a58df4baa6..e96e1ab4df53 100644 --- a/tools/issue-labeler/labeler/backfill.go +++ b/tools/issue-labeler/labeler/backfill.go @@ -1,95 +1,87 @@ package labeler import ( - "bytes" - "encoding/json" - "errors" + "context" "fmt" - "io" - "net/http" - "os" "sort" + "time" "github.com/golang/glog" + "github.com/google/go-github/v61/github" ) -type ErrorResponse struct { - Message string -} - -type Issue struct { - Number uint64 - Body string - Labels []Label - PullRequest map[string]any `json:"pull_request"` -} - type Label struct { Name string } type IssueUpdate struct { - Number uint64 + Number int Labels []string OldLabels []string } -type IssueUpdateBody struct { - Labels []string `json:"labels"` -} +func GetIssues(repository, since string) ([]*github.Issue, error) { + client := newGitHubClient() + owner, repo, err := splitRepository(repository) + if err != nil { + return nil, fmt.Errorf("invalid repository format: %w", err) + } -func GetIssues(repository, since string) ([]Issue, error) { - client := &http.Client{} - done := false - page := 1 - var issues []Issue - for !done { - url := fmt.Sprintf("https://api.github.com/repos/%s/issues?since=%s&per_page=100&page=%d", repository, since, page) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return nil, fmt.Errorf("creating request: %w", err) - } - req.Header.Add("Accept", "application/vnd.github+json") - req.Header.Add("Authorization", "Bearer "+os.Getenv("GITHUB_TOKEN")) - req.Header.Add("X-GitHub-Api-Version", "2022-11-28") - resp, err := client.Do(req) - if err != nil { - return nil, fmt.Errorf("listing issues: %v", err) - } - defer resp.Body.Close() - body, err := io.ReadAll(resp.Body) + sinceTime, err := time.Parse(time.RFC3339, since) + if err != nil { + return nil, fmt.Errorf("invalid since time format: %w", err) + } + + opt := &github.IssueListByRepoOptions{ + Since: sinceTime, + State: "all", + Sort: "updated", + Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: 100, + }, + } + + var allIssues []*github.Issue + ctx := context.Background() + + for { + issues, resp, err := client.Issues.ListByRepo(ctx, owner, repo, opt) if err != nil { - return nil, fmt.Errorf("reading response body: %v", err) + return nil, fmt.Errorf("listing issues: %w", err) } - var newIssues []Issue - json.Unmarshal(body, &newIssues) - if len(newIssues) == 0 { - var err ErrorResponse - json.Unmarshal(body, &err) - if err.Message == "Bad credentials" { - return nil, errors.New("Error from API: Bad credentials") + + // Convert github.Issue to our Issue type + for _, issue := range issues { + labels := make([]Label, len(issue.Labels)) + for i, l := range issue.Labels { + labels[i] = Label{Name: *l.Name} } - glog.Infof("API returned message: %s", err.Message) - done = true - } else { - issues = append(issues, newIssues...) - page++ + + allIssues = append(allIssues, issue) } + + if resp.NextPage == 0 { + break + } + opt.Page = resp.NextPage } - return issues, nil + + return allIssues, nil } -func ComputeIssueUpdates(issues []Issue, regexpLabels []RegexpLabel) []IssueUpdate { +// ComputeIssueUpdates remains the same as it doesn't interact with GitHub API +func ComputeIssueUpdates(issues []*github.Issue, regexpLabels []RegexpLabel) []IssueUpdate { var issueUpdates []IssueUpdate for _, issue := range issues { - if len(issue.PullRequest) > 0 { + if !issue.IsPullRequest() { continue } desired := make(map[string]struct{}) for _, existing := range issue.Labels { - desired[existing.Name] = struct{}{} + desired[*existing.Name] = struct{}{} } _, terraform := desired["service/terraform"] @@ -112,7 +104,7 @@ func ComputeIssueUpdates(issues []Issue, regexpLabels []RegexpLabel) []IssueUpda issueUpdate.OldLabels = append(issueUpdate.OldLabels, label) } - affectedResources := ExtractAffectedResources(issue.Body) + affectedResources := ExtractAffectedResources(*issue.Body) for _, needed := range ComputeLabels(affectedResources, regexpLabels) { desired[needed] = struct{}{} } @@ -126,8 +118,7 @@ func ComputeIssueUpdates(issues []Issue, regexpLabels []RegexpLabel) []IssueUpda } sort.Strings(issueUpdate.Labels) - issueUpdate.Number = issue.Number - + issueUpdate.Number = *issue.Number issueUpdates = append(issueUpdates, issueUpdate) } } @@ -136,57 +127,35 @@ func ComputeIssueUpdates(issues []Issue, regexpLabels []RegexpLabel) []IssueUpda } func UpdateIssues(repository string, issueUpdates []IssueUpdate, dryRun bool) error { - client := &http.Client{} + client := newGitHubClient() + owner, repo, err := splitRepository(repository) + if err != nil { + return fmt.Errorf("invalid repository format: %w", err) + } + + ctx := context.Background() failed := 0 - for _, issueUpdate := range issueUpdates { - url := fmt.Sprintf("https://api.github.com/repos/%s/issues/%d", repository, issueUpdate.Number) - updateBody := IssueUpdateBody{Labels: issueUpdate.Labels} - body, err := json.Marshal(updateBody) - if err != nil { - return fmt.Errorf("marshalling json: %w", err) - } - buf := bytes.NewReader(body) - req, err := http.NewRequest("PATCH", url, buf) - req.Header.Add("Authorization", "Bearer "+os.Getenv("GITHUB_TOKEN")) - req.Header.Add("X-GitHub-Api-Version", "2022-11-28") - if err != nil { - return fmt.Errorf("creating request: %w", err) + + for _, update := range issueUpdates { + fmt.Printf("Existing labels: %v\n", update.OldLabels) + fmt.Printf("New labels: %v\n", update.Labels) + fmt.Printf("Updating issue: https://github.com/%s/issues/%d\n", repository, update.Number) + if dryRun { + continue } - fmt.Printf("Existing labels: %v\n", issueUpdate.OldLabels) - fmt.Printf("New labels: %v\n", issueUpdate.Labels) - fmt.Printf("%s %s (https://github.com/%s/issues/%d)\n", req.Method, req.URL, repository, issueUpdate.Number) + _, _, err := client.Issues.Edit(ctx, owner, repo, int(update.Number), &github.IssueRequest{ + Labels: &update.Labels, + }) - // Pretty-print the body for debugging - b, err := json.MarshalIndent(updateBody, "", " ") if err != nil { - return fmt.Errorf("Error marshalling json: %w", err) + glog.Errorf("Error updating issue %d: %v", update.Number, err) + failed++ + continue } - fmt.Println(string(b)) - - if !dryRun { - resp, err := client.Do(req) - if err != nil { - glog.Errorf("Error updating issue: %v", err) - failed += 1 - continue - } - body, err := io.ReadAll(resp.Body) - if err != nil { - glog.Errorf("Error reading response body: %v", err) - failed += 1 - continue - } - var errResp ErrorResponse - json.Unmarshal(body, &errResp) - if errResp.Message != "" { - fmt.Printf("API error: %s", errResp.Message) - failed += 1 - continue - } - } - fmt.Printf("GitHub Issue %s %d updated successfully", repository, issueUpdate.Number) + fmt.Printf("GitHub Issue %s %d updated successfully\n", repository, update.Number) } + if failed > 0 { return fmt.Errorf("failed to update %d / %d issues", failed, len(issueUpdates)) } diff --git a/tools/issue-labeler/labeler/backfill_test.go b/tools/issue-labeler/labeler/backfill_test.go index 36ffa8defee3..96e5f9d1dda2 100644 --- a/tools/issue-labeler/labeler/backfill_test.go +++ b/tools/issue-labeler/labeler/backfill_test.go @@ -2,13 +2,44 @@ package labeler import ( "fmt" - "reflect" "regexp" + "strconv" "strings" "testing" + + "github.com/google/go-github/v61/github" ) +// TestIssue represents a simplified issue structure for testing +type TestIssue struct { + Number int + Body string + Labels []string +} + +// Convert TestIssue to github.Issue +func (i TestIssue) toGithubIssue() *github.Issue { + var labels []*github.Label + for _, l := range i.Labels { + name := l + label := github.Label{Name: &name} + labels = append(labels, &label) + } + + number := i.Number + body := i.Body + pullRequestURLstr := "https://api.github.com/repos/owner/repo/pulls/" + strconv.Itoa(number) + prLinks := &github.PullRequestLinks{URL: &pullRequestURLstr} + + return &github.Issue{ + Number: &number, + Body: &body, + Labels: labels, + PullRequestLinks: prLinks, + } +} + func testIssueBodyWithResources(resources []string) string { return fmt.Sprintf(` ### New or Affected Resource(s): @@ -34,47 +65,42 @@ func TestComputeIssueUpdates(t *testing.T) { Label: "service/service2-subteam2", }, } + cases := map[string]struct { - issues []Issue + issues []TestIssue regexpLabels []RegexpLabel expectedIssueUpdates []IssueUpdate }{ "no issues -> no updates": { - issues: []Issue{}, + issues: []TestIssue{}, regexpLabels: defaultRegexpLabels, expectedIssueUpdates: []IssueUpdate{}, }, "exempt labels -> no updates": { - issues: []Issue{ + issues: []TestIssue{ { - Number: 1, - Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), - Labels: []Label{{Name: "service/terraform"}}, - PullRequest: map[string]any{}, + Number: 1, + Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), + Labels: []string{"service/terraform"}, }, { - Number: 2, - Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), - Labels: []Label{{Name: "forward/exempt"}}, - PullRequest: map[string]any{}, + Number: 2, + Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), + Labels: []string{"forward/exempt"}, }, }, regexpLabels: defaultRegexpLabels, expectedIssueUpdates: []IssueUpdate{}, }, "add resource & review labels": { - issues: []Issue{ + issues: []TestIssue{ { - Number: 1, - Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), - Labels: []Label{}, - PullRequest: map[string]any{}, + Number: 1, + Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), }, { - Number: 2, - Body: testIssueBodyWithResources([]string{"google_service2_resource1"}), - Labels: []Label{}, - PullRequest: map[string]any{}, + Number: 2, + Body: testIssueBodyWithResources([]string{"google_service2_resource1"}), }, }, regexpLabels: defaultRegexpLabels, @@ -90,36 +116,32 @@ func TestComputeIssueUpdates(t *testing.T) { }, }, "don't update issues if all service labels are already present": { - issues: []Issue{ + issues: []TestIssue{ { - Number: 1, - Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), - Labels: []Label{{Name: "service/service1"}}, - PullRequest: map[string]any{}, + Number: 1, + Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), + Labels: []string{"service/service1"}, }, { - Number: 2, - Body: testIssueBodyWithResources([]string{"google_service2_resource1"}), - Labels: []Label{{Name: "service/service2-subteam1"}}, - PullRequest: map[string]any{}, + Number: 2, + Body: testIssueBodyWithResources([]string{"google_service2_resource1"}), + Labels: []string{"service/service2-subteam1"}, }, }, regexpLabels: defaultRegexpLabels, expectedIssueUpdates: []IssueUpdate{}, }, "add missing service labels": { - issues: []Issue{ + issues: []TestIssue{ { - Number: 1, - Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), - Labels: []Label{{Name: "service/service2-subteam1"}}, - PullRequest: map[string]any{}, + Number: 1, + Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), + Labels: []string{"service/service2-subteam1"}, }, { - Number: 2, - Body: testIssueBodyWithResources([]string{"google_service2_resource2"}), - Labels: []Label{{Name: "service/service1"}}, - PullRequest: map[string]any{}, + Number: 2, + Body: testIssueBodyWithResources([]string{"google_service2_resource2"}), + Labels: []string{"service/service1"}, }, }, regexpLabels: defaultRegexpLabels, @@ -137,12 +159,11 @@ func TestComputeIssueUpdates(t *testing.T) { }, }, "don't add missing service labels if already linked": { - issues: []Issue{ + issues: []TestIssue{ { - Number: 1, - Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), - Labels: []Label{{Name: "service/service2-subteam1"}, {Name: "forward/linked"}}, - PullRequest: map[string]any{}, + Number: 1, + Body: testIssueBodyWithResources([]string{"google_service1_resource1"}), + Labels: []string{"service/service2-subteam1", "forward/linked"}, }, }, regexpLabels: defaultRegexpLabels, @@ -154,11 +175,66 @@ func TestComputeIssueUpdates(t *testing.T) { tc := tc t.Run(tn, func(t *testing.T) { t.Parallel() - issueUpdates := ComputeIssueUpdates(tc.issues, tc.regexpLabels) - // reflect.DeepEqual treats nil & empty slices as not equal so ignore diffs if both slices are empty. - if (len(issueUpdates) > 0 || len(tc.expectedIssueUpdates) > 0) && !reflect.DeepEqual(issueUpdates, tc.expectedIssueUpdates) { + + // Convert TestIssues to github.Issues + var githubIssues []*github.Issue + for _, issue := range tc.issues { + githubIssues = append(githubIssues, issue.toGithubIssue()) + } + + issueUpdates := ComputeIssueUpdates(githubIssues, tc.regexpLabels) + if !issueUpdatesEqual(issueUpdates, tc.expectedIssueUpdates) { t.Errorf("Expected %v, got %v", tc.expectedIssueUpdates, issueUpdates) } }) } } + +func TestSplitRepository(t *testing.T) { + tests := []struct { + name string + repository string + wantOwner string + wantRepo string + wantErr bool + }{ + { + name: "valid repository", + repository: "owner/repo", + wantOwner: "owner", + wantRepo: "repo", + wantErr: false, + }, + { + name: "invalid repository", + repository: "invalid-format", + wantOwner: "", + wantRepo: "", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + owner, repo, err := splitRepository(tt.repository) + if (err != nil) != tt.wantErr { + t.Errorf("splitRepository() error = %v, wantErr %v", err, tt.wantErr) + return + } + if owner != tt.wantOwner { + t.Errorf("splitRepository() owner = %v, want %v", owner, tt.wantOwner) + } + if repo != tt.wantRepo { + t.Errorf("splitRepository() repo = %v, want %v", repo, tt.wantRepo) + } + }) + } +} + +// Helper function to compare issue updates while handling nil/empty slice equality +func issueUpdatesEqual(a, b []IssueUpdate) bool { + if len(a) == 0 && len(b) == 0 { + return true + } + return reflect.DeepEqual(a, b) +} diff --git a/tools/issue-labeler/labeler/github.go b/tools/issue-labeler/labeler/github.go new file mode 100644 index 000000000000..bd9b98ae2f2b --- /dev/null +++ b/tools/issue-labeler/labeler/github.go @@ -0,0 +1,33 @@ +package labeler + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/google/go-github/v61/github" + "golang.org/x/oauth2" +) + +func newGitHubClient() *github.Client { + ctx := context.Background() + ts := oauth2.StaticTokenSource( + &oauth2.Token{AccessToken: os.Getenv("GITHUB_TOKEN")}, + ) + tc := oauth2.NewClient(ctx, ts) + return github.NewClient(tc) +} + +// Helper functions +func splitRepository(repository string) (string, string, error) { + var owner, repo string + or := strings.Split(repository, "/") + if len(or) != 2 { + return "", "", fmt.Errorf("unexpected repository format %s", repository) + } + + owner = or[0] + repo = or[1] + return owner, repo, nil +}