From 9af477c387dbf5c9c199910f7703487660d81904 Mon Sep 17 00:00:00 2001 From: "Stephen Lewis (Burrows)" Date: Wed, 4 Dec 2024 11:46:47 -0800 Subject: [PATCH] Converted issue-labeler tool to use cobra (#12473) --- .ci/magician/go.mod | 2 +- .ci/magician/go.sum | 6 +- .../cmd/backfill_issue_labels.go | 70 +++++++++++++++++++ tools/issue-labeler/cmd/compute_new_labels.go | 58 +++++++++++++++ tools/issue-labeler/cmd/root.go | 44 ++++++++++++ tools/issue-labeler/go.mod | 7 +- tools/issue-labeler/go.sum | 9 +++ tools/issue-labeler/labeler/backfill.go | 35 ++++++---- tools/issue-labeler/labeler/labels.go | 2 +- tools/issue-labeler/main.go | 38 +--------- 10 files changed, 216 insertions(+), 55 deletions(-) create mode 100644 tools/issue-labeler/cmd/backfill_issue_labels.go create mode 100644 tools/issue-labeler/cmd/compute_new_labels.go create mode 100644 tools/issue-labeler/cmd/root.go diff --git a/.ci/magician/go.mod b/.ci/magician/go.mod index e3443c66892b..61ce18c80238 100644 --- a/.ci/magician/go.mod +++ b/.ci/magician/go.mod @@ -7,7 +7,7 @@ replace github.com/GoogleCloudPlatform/magic-modules/tools/issue-labeler => ../. require ( github.com/GoogleCloudPlatform/magic-modules/tools/issue-labeler v0.0.0-00010101000000-000000000000 github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 // indirect golang.org/x/exp v0.0.0-20230810033253-352e893a4cad google.golang.org/api v0.114.0 diff --git a/.ci/magician/go.sum b/.ci/magician/go.sum index 041a2fb36447..ec29a418f5c0 100644 --- a/.ci/magician/go.sum +++ b/.ci/magician/go.sum @@ -10,7 +10,7 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -77,8 +77,8 @@ github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1: github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/tools/issue-labeler/cmd/backfill_issue_labels.go b/tools/issue-labeler/cmd/backfill_issue_labels.go new file mode 100644 index 000000000000..c353b29e579c --- /dev/null +++ b/tools/issue-labeler/cmd/backfill_issue_labels.go @@ -0,0 +1,70 @@ +/* +* Copyright 2024 Google LLC. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. + */ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/magic-modules/tools/issue-labeler/labeler" +) + +var ( + // used for flags + backfillSince string + backfillDryRun bool +) + +var backfillIssueLabels = &cobra.Command{ + Use: "backfill-issue-labels [--dry-run] [--since=1973-01-01]", + Short: "Backfills labels on old issues", + Long: "Backfills labels on old issues", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + // For now actual usage is handled inside UpdateIssues. This is just a new quick check. + _, ok := os.LookupEnv("GITHUB_TOKEN") + if !ok { + return fmt.Errorf("did not provide GITHUB_TOKEN environment variable") + } + return execBackfillIssueLabels() + }, +} + +func execBackfillIssueLabels() error { + regexpLabels, err := labeler.BuildRegexLabels(labeler.EnrolledTeamsYaml) + if err != nil { + return fmt.Errorf("building regex labels: %w", err) + } + repository := "hashicorp/terraform-provider-google" + issues, err := labeler.GetIssues(repository, backfillSince) + if err != nil { + return fmt.Errorf("getting github issues: %w", err) + } + issueUpdates := labeler.ComputeIssueUpdates(issues, regexpLabels) + err = labeler.UpdateIssues(repository, issueUpdates, backfillDryRun) + if err != nil { + return fmt.Errorf("updating github issues: %w", err) + } + return nil +} + +func init() { + rootCmd.AddCommand(backfillIssueLabels) + backfillIssueLabels.Flags().BoolVar(&backfillDryRun, "dry-run", false, "Only log write actions instead of updating issues") + backfillIssueLabels.Flags().StringVar(&backfillSince, "since", "1973-01-01", "Only apply labels to issues filed after given date") +} diff --git a/tools/issue-labeler/cmd/compute_new_labels.go b/tools/issue-labeler/cmd/compute_new_labels.go new file mode 100644 index 000000000000..22b5326fae95 --- /dev/null +++ b/tools/issue-labeler/cmd/compute_new_labels.go @@ -0,0 +1,58 @@ +/* +* Copyright 2024 Google LLC. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. + */ +package cmd + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/spf13/cobra" + + "github.com/GoogleCloudPlatform/magic-modules/tools/issue-labeler/labeler" +) + +var computeNewLabels = &cobra.Command{ + Use: "compute-new-labels", + Short: "Computes labels that should be added to an issue based on its body", + Long: "Computes labels that should be added to an issue based on its body", + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + return execComputeNewLabels() + }, +} + +func execComputeNewLabels() error { + regexpLabels, err := labeler.BuildRegexLabels(labeler.EnrolledTeamsYaml) + if err != nil { + return fmt.Errorf("building regex labels: %w", err) + } + issueBody := os.Getenv("ISSUE_BODY") + affectedResources := labeler.ExtractAffectedResources(issueBody) + labels := labeler.ComputeLabels(affectedResources, regexpLabels) + + if len(labels) > 0 { + labels = append(labels, "forward/review") + sort.Strings(labels) + fmt.Println(`["` + strings.Join(labels, `", "`) + `"]`) + } + return nil +} + +func init() { + rootCmd.AddCommand(computeNewLabels) +} diff --git a/tools/issue-labeler/cmd/root.go b/tools/issue-labeler/cmd/root.go new file mode 100644 index 000000000000..fb8d576b6392 --- /dev/null +++ b/tools/issue-labeler/cmd/root.go @@ -0,0 +1,44 @@ +/* +* Copyright 2024 Google LLC. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. + */ +package cmd + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// rootCmd represents the base command when called without any subcommands + +var rootCmd = &cobra.Command{ + Use: "issue-labeler", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + fmt.Printf("Inside rootCmd PersistentPreRun with args: %v\n", args) + }, + Short: "Tool for interacting with issue labels (specifically for services)", + Long: `Tool for interacting with issue labels (specifically for services)`, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + err := rootCmd.Execute() + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/tools/issue-labeler/go.mod b/tools/issue-labeler/go.mod index bdd9a278d9a1..7c7ed5edc1f2 100644 --- a/tools/issue-labeler/go.mod +++ b/tools/issue-labeler/go.mod @@ -8,4 +8,9 @@ require ( gopkg.in/yaml.v2 v2.4.0 ) -require gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect +require ( + 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 a308adc5aa29..40345351b651 100644 --- a/tools/issue-labeler/go.sum +++ b/tools/issue-labeler/go.sum @@ -1,10 +1,18 @@ +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/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= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -12,3 +20,4 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/issue-labeler/labeler/backfill.go b/tools/issue-labeler/labeler/backfill.go index 89eed2d58077..21a58df4baa6 100644 --- a/tools/issue-labeler/labeler/backfill.go +++ b/tools/issue-labeler/labeler/backfill.go @@ -3,6 +3,7 @@ package labeler import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -37,7 +38,7 @@ type IssueUpdateBody struct { Labels []string `json:"labels"` } -func GetIssues(repository, since string) []Issue { +func GetIssues(repository, since string) ([]Issue, error) { client := &http.Client{} done := false page := 1 @@ -46,19 +47,19 @@ func GetIssues(repository, since string) []Issue { 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 { - glog.Exitf("Error creating request: %v", err) + 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 { - glog.Exitf("Error listing issues: %v", err) + return nil, fmt.Errorf("listing issues: %v", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - glog.Exitf("Error reading response body: %v", err) + return nil, fmt.Errorf("reading response body: %v", err) } var newIssues []Issue json.Unmarshal(body, &newIssues) @@ -66,7 +67,7 @@ func GetIssues(repository, since string) []Issue { var err ErrorResponse json.Unmarshal(body, &err) if err.Message == "Bad credentials" { - glog.Exitf("Error from API: Bad credentials") + return nil, errors.New("Error from API: Bad credentials") } glog.Infof("API returned message: %s", err.Message) done = true @@ -75,7 +76,7 @@ func GetIssues(repository, since string) []Issue { page++ } } - return issues + return issues, nil } func ComputeIssueUpdates(issues []Issue, regexpLabels []RegexpLabel) []IssueUpdate { @@ -134,52 +135,60 @@ func ComputeIssueUpdates(issues []Issue, regexpLabels []RegexpLabel) []IssueUpda return issueUpdates } -func UpdateIssues(repository string, issueUpdates []IssueUpdate, dryRun bool) { +func UpdateIssues(repository string, issueUpdates []IssueUpdate, dryRun bool) error { client := &http.Client{} + 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 { - glog.Errorf("Error marshalling json: %v", err) - continue + 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 { - glog.Errorf("Error creating request: %v", err) - continue + return fmt.Errorf("creating request: %w", err) } 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) + + // Pretty-print the body for debugging b, err := json.MarshalIndent(updateBody, "", " ") if err != nil { - glog.Errorf("Error marshalling json: %v", err) - continue + return fmt.Errorf("Error marshalling json: %w", err) } 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) } + if failed > 0 { + return fmt.Errorf("failed to update %d / %d issues", failed, len(issueUpdates)) + } + return nil } diff --git a/tools/issue-labeler/labeler/labels.go b/tools/issue-labeler/labeler/labels.go index d702b081dd65..1862d6c44676 100644 --- a/tools/issue-labeler/labeler/labels.go +++ b/tools/issue-labeler/labeler/labels.go @@ -34,7 +34,7 @@ func BuildRegexLabels(teamsYaml []byte) ([]RegexpLabel, error) { enrolledTeams := make(map[string]LabelData) regexpLabels := []RegexpLabel{} if err := yaml.Unmarshal(teamsYaml, &enrolledTeams); err != nil { - return regexpLabels, fmt.Errorf("Error unmarshalling enrolled teams yaml: %w", err) + return regexpLabels, fmt.Errorf("unmarshalling enrolled teams yaml: %w", err) } for label, data := range enrolledTeams { diff --git a/tools/issue-labeler/main.go b/tools/issue-labeler/main.go index d83ff58b8250..d22535daccfd 100644 --- a/tools/issue-labeler/main.go +++ b/tools/issue-labeler/main.go @@ -1,41 +1,7 @@ package main -import ( - "flag" - "fmt" - "os" - "sort" - "strings" - - "github.com/GoogleCloudPlatform/magic-modules/tools/issue-labeler/labeler" - "github.com/golang/glog" -) - -var flagBackfillDate = flag.String("backfill-date", "", "run in backfill mode to apply labels to issues filed after given date") -var flagDryRun = flag.Bool("backfill-dry-run", false, "when combined with backfill-date, perform a dry run of backfill mode") +import "github.com/GoogleCloudPlatform/magic-modules/tools/issue-labeler/cmd" func main() { - flag.Parse() - - regexpLabels, err := labeler.BuildRegexLabels(labeler.EnrolledTeamsYaml) - if err != nil { - glog.Exitf("Error building regex labels: %v", err) - } - - if *flagBackfillDate == "" { - issueBody := os.Getenv("ISSUE_BODY") - affectedResources := labeler.ExtractAffectedResources(issueBody) - labels := labeler.ComputeLabels(affectedResources, regexpLabels) - - if len(labels) > 0 { - labels = append(labels, "forward/review") - sort.Strings(labels) - fmt.Println(`["` + strings.Join(labels, `", "`) + `"]`) - } - } else { - repository := "hashicorp/terraform-provider-google" - issues := labeler.GetIssues(repository, *flagBackfillDate) - issueUpdates := labeler.ComputeIssueUpdates(issues, regexpLabels) - labeler.UpdateIssues(repository, issueUpdates, *flagDryRun) - } + cmd.Execute() }