From 45c759eb59fe75dc83f069b8eadbb8c1ad4d1fd9 Mon Sep 17 00:00:00 2001 From: Kevin Minehart Date: Thu, 24 Nov 2022 08:17:12 -0600 Subject: [PATCH] CI: Make the downstream enterprise test a check instead of comments (#59071) --- go.mod | 1 + go.sum | 3 + pkg/build/cmd/argcount_wrapper.go | 17 ++- pkg/build/cmd/enterprisecheck.go | 121 ++++++++++++++++++++++ pkg/build/cmd/enterprisecheck_test.go | 69 +++++++++++++ pkg/build/cmd/flags.go | 6 ++ pkg/build/cmd/main.go | 44 ++++++-- pkg/build/env/lookup.go | 18 ++++ pkg/build/env/lookup_test.go | 43 ++++++++ pkg/build/git/git.go | 143 ++++++++++++++++++++++++++ pkg/build/git/git_checks_test.go | 55 ++++++++++ pkg/build/git/git_issues_test.go | 134 ++++++++++++++++++++++++ pkg/build/git/git_test.go | 25 +++++ pkg/build/stringutil/contains.go | 10 ++ 14 files changed, 681 insertions(+), 8 deletions(-) create mode 100644 pkg/build/cmd/enterprisecheck.go create mode 100644 pkg/build/cmd/enterprisecheck_test.go create mode 100644 pkg/build/env/lookup.go create mode 100644 pkg/build/env/lookup_test.go create mode 100644 pkg/build/git/git.go create mode 100644 pkg/build/git/git_checks_test.go create mode 100644 pkg/build/git/git_issues_test.go create mode 100644 pkg/build/git/git_test.go create mode 100644 pkg/build/stringutil/contains.go diff --git a/go.mod b/go.mod index 8f29add2ffd..c04c7037488 100644 --- a/go.mod +++ b/go.mod @@ -284,6 +284,7 @@ require ( github.com/drone/envsubst v1.0.3 // indirect github.com/drone/runner-go v1.12.0 // indirect github.com/go-ozzo/ozzo-validation/v4 v4.3.0 // indirect + github.com/google/go-github/v31 v31.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa // indirect github.com/googleapis/go-type-adapters v1.0.0 // indirect diff --git a/go.sum b/go.sum index d41b9baea0d..c55c5495966 100644 --- a/go.sum +++ b/go.sum @@ -1240,6 +1240,8 @@ github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-github/v31 v31.0.0 h1:JJUxlP9lFK+ziXKimTCprajMApV1ecWD4NB6CCb0plo= +github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM= github.com/google/go-github/v45 v45.2.0 h1:5oRLszbrkvxDDqBCNj2hjDZMKmvexaZ1xw/FCD+K3FI= github.com/google/go-github/v45 v45.2.0/go.mod h1:FObaZJEDSTa/WGCzZ2Z3eoCDXWJKMenWWTrd8jrta28= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= @@ -3303,6 +3305,7 @@ google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6r google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0 h1:NMB9J4cCxs9xEm+1Z9QiO3eFvn7EnQj3Eo3hN6ugVlg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= diff --git a/pkg/build/cmd/argcount_wrapper.go b/pkg/build/cmd/argcount_wrapper.go index d3a3cfd67f6..690695cd350 100644 --- a/pkg/build/cmd/argcount_wrapper.go +++ b/pkg/build/cmd/argcount_wrapper.go @@ -2,7 +2,22 @@ package main import "github.com/urfave/cli/v2" -func ArgCountWrapper(max int, action cli.ActionFunc) cli.ActionFunc { +// ArgCountWrapper will cause the action to fail if there were not exactly `num` args provided. +func ArgCountWrapper(num int, action cli.ActionFunc) cli.ActionFunc { + return func(ctx *cli.Context) error { + if ctx.NArg() != num { + if err := cli.ShowSubcommandHelp(ctx); err != nil { + return cli.Exit(err.Error(), 1) + } + return cli.Exit("", 1) + } + + return action(ctx) + } +} + +// ArgCountWrapper will cause the action to fail if there were more than `num` args provided. +func MaxArgCountWrapper(max int, action cli.ActionFunc) cli.ActionFunc { return func(ctx *cli.Context) error { if ctx.NArg() > max { if err := cli.ShowSubcommandHelp(ctx); err != nil { diff --git a/pkg/build/cmd/enterprisecheck.go b/pkg/build/cmd/enterprisecheck.go new file mode 100644 index 00000000000..732ae728442 --- /dev/null +++ b/pkg/build/cmd/enterprisecheck.go @@ -0,0 +1,121 @@ +package main + +import ( + "os" + "strconv" + + "github.com/grafana/grafana/pkg/build/env" + "github.com/grafana/grafana/pkg/build/git" + "github.com/urfave/cli/v2" +) + +// checkOpts are options used to create a new GitHub check for the enterprise downstream test. +type checkOpts struct { + SHA string + URL string + Branch string + PR int +} + +func getCheckOpts(args []string) (*checkOpts, error) { + sha, ok := env.Lookup("SOURCE_COMMIT", args) + if !ok { + return nil, cli.Exit(`missing environment variable "SOURCE_COMMIT"`, 1) + } + + url, ok := env.Lookup("DRONE_BUILD_LINK", args) + if !ok { + return nil, cli.Exit(`missing environment variable "DRONE_BUILD_LINK"`, 1) + } + + branch, ok := env.Lookup("DRONE_SOURCE_BRANCH", args) + if !ok { + return nil, cli.Exit("Unable to retrieve build source branch", 1) + } + + prStr, ok := env.Lookup("OSS_PULL_REQUEST", args) + if !ok { + matches := git.PRCheckRegexp().FindStringSubmatch(branch) + if matches == nil || len(matches) <= 1 { + return nil, cli.Exit("Unable to retrieve PR number", 1) + } + + prStr = matches[1] + } + + pr, err := strconv.Atoi(prStr) + if err != nil { + return nil, err + } + + return &checkOpts{ + Branch: branch, + PR: pr, + SHA: sha, + URL: url, + }, nil +} + +// EnterpriseCheckBegin creates the GitHub check and signals the beginning of the downstream build / test process +func EnterpriseCheckBegin(c *cli.Context) error { + var ( + ctx = c.Context + client = git.NewGitHubClient(ctx, c.String("github-token")) + ) + + opts, err := getCheckOpts(os.Environ()) + if err != nil { + return err + } + + if _, err = git.CreateEnterpriseStatus(ctx, client.Repositories, opts.SHA, opts.URL, "pending"); err != nil { + return err + } + + return nil +} + +func EnterpriseCheckSuccess(c *cli.Context) error { + return completeEnterpriseCheck(c, true) +} + +func EnterpriseCheckFail(c *cli.Context) error { + return completeEnterpriseCheck(c, false) +} + +func completeEnterpriseCheck(c *cli.Context, success bool) error { + var ( + ctx = c.Context + client = git.NewGitHubClient(ctx, c.String("github-token")) + ) + + // Update the pull request labels + opts, err := getCheckOpts(os.Environ()) + if err != nil { + return err + } + + status := "failure" + if success { + status = "success" + } + + // Update the GitHub check... + if _, err := git.CreateEnterpriseStatus(ctx, client.Repositories, opts.SHA, opts.URL, status); err != nil { + return err + } + + // Delete branch if needed + if git.PRCheckRegexp().MatchString(opts.Branch) { + if err := git.DeleteEnterpriseBranch(ctx, client.Git, opts.Branch); err != nil { + return nil + } + } + + label := "enterprise-failed" + if success { + label = "enterprise-ok" + } + + return git.AddLabelToPR(ctx, client.Issues, opts.PR, label) +} diff --git a/pkg/build/cmd/enterprisecheck_test.go b/pkg/build/cmd/enterprisecheck_test.go new file mode 100644 index 00000000000..0eeb5bd5741 --- /dev/null +++ b/pkg/build/cmd/enterprisecheck_test.go @@ -0,0 +1,69 @@ +package main + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetCheckOpts(t *testing.T) { + t.Run("it should return the checkOpts if the correct environment variables are set", func(t *testing.T) { + args := []string{ + "SOURCE_COMMIT=1234", + "DRONE_SOURCE_BRANCH=test", + "DRONE_BUILD_LINK=http://example.com", + "OSS_PULL_REQUEST=1", + } + + opts, err := getCheckOpts(args) + require.NoError(t, err) + require.Equal(t, opts.SHA, "1234") + require.Equal(t, opts.URL, "http://example.com") + }) + t.Run("it should return an error if SOURCE_COMMIT is not set", func(t *testing.T) { + args := []string{ + "DRONE_BUILD_LINK=http://example.com", + "DRONE_SOURCE_BRANCH=test", + "DRONE_BUILD_LINK=http://example.com", + "OSS_PULL_REQUEST=1", + } + + opts, err := getCheckOpts(args) + require.Nil(t, opts) + require.Error(t, err) + }) + t.Run("it should return an error if DRONE_BUILD_LINK is not set", func(t *testing.T) { + args := []string{ + "SOURCE_COMMIT=1234", + "DRONE_SOURCE_BRANCH=test", + "OSS_PULL_REQUEST=1", + } + + opts, err := getCheckOpts(args) + require.Nil(t, opts) + require.Error(t, err) + }) + t.Run("it should return an error if OSS_PULL_REQUEST is not set", func(t *testing.T) { + args := []string{ + "SOURCE_COMMIT=1234", + "DRONE_SOURCE_BRANCH=test", + "DRONE_BUILD_LINK=http://example.com", + } + + opts, err := getCheckOpts(args) + require.Nil(t, opts) + require.Error(t, err) + }) + t.Run("it should return an error if OSS_PULL_REQUEST is not an integer", func(t *testing.T) { + args := []string{ + "SOURCE_COMMIT=1234", + "DRONE_SOURCE_BRANCH=test", + "DRONE_BUILD_LINK=http://example.com", + "OSS_PULL_REQUEST=http://example.com", + } + + opts, err := getCheckOpts(args) + require.Nil(t, opts) + require.Error(t, err) + }) +} diff --git a/pkg/build/cmd/flags.go b/pkg/build/cmd/flags.go index 7aa6d98d026..e92ce0022d2 100644 --- a/pkg/build/cmd/flags.go +++ b/pkg/build/cmd/flags.go @@ -46,4 +46,10 @@ var ( Usage: "Google Cloud Platform key file", Required: true, } + gitHubTokenFlag = cli.StringFlag{ + Name: "github-token", + Value: "", + EnvVars: []string{"GITHUB_TOKEN"}, + Usage: "GitHub token", + } ) diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index 658cceb9011..d8863710085 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -16,7 +16,7 @@ func main() { Name: "build-backend", Usage: "Build one or more variants of back-end binaries", ArgsUsage: "[version]", - Action: ArgCountWrapper(1, BuildBackend), + Action: MaxArgCountWrapper(1, BuildBackend), Flags: []cli.Flag{ &jobsFlag, &variantsFlag, @@ -67,7 +67,7 @@ func main() { Name: "build-frontend", Usage: "Build front-end artifacts", ArgsUsage: "[version]", - Action: ArgCountWrapper(1, BuildFrontend), + Action: MaxArgCountWrapper(1, BuildFrontend), Flags: []cli.Flag{ &jobsFlag, &editionFlag, @@ -77,7 +77,7 @@ func main() { { Name: "build-docker", Usage: "Build Grafana Docker images", - Action: ArgCountWrapper(1, BuildDocker), + Action: MaxArgCountWrapper(1, BuildDocker), Flags: []cli.Flag{ &jobsFlag, &editionFlag, @@ -112,7 +112,7 @@ func main() { { Name: "build-plugins", Usage: "Build internal plug-ins", - Action: ArgCountWrapper(1, BuildInternalPlugins), + Action: MaxArgCountWrapper(1, BuildInternalPlugins), Flags: []cli.Flag{ &jobsFlag, &editionFlag, @@ -125,7 +125,7 @@ func main() { Name: "publish-metrics", Usage: "Publish a set of metrics from stdin", ArgsUsage: "", - Action: ArgCountWrapper(1, PublishMetrics), + Action: MaxArgCountWrapper(1, PublishMetrics), }, { Name: "verify-drone", @@ -141,7 +141,7 @@ func main() { Name: "package", Usage: "Package one or more Grafana variants", ArgsUsage: "[version]", - Action: ArgCountWrapper(1, Package), + Action: MaxArgCountWrapper(1, Package), Flags: []cli.Flag{ &jobsFlag, &variantsFlag, @@ -182,7 +182,7 @@ func main() { Name: "fetch", Usage: "Fetch Grafana Docker images", ArgsUsage: "[version]", - Action: ArgCountWrapper(1, FetchImages), + Action: MaxArgCountWrapper(1, FetchImages), Flags: []cli.Flag{ &editionFlag, }, @@ -277,6 +277,36 @@ func main() { }, }, }, + { + Name: "enterprise-check", + Usage: "Commands for testing against Grafana Enterprise", + Subcommands: cli.Commands{ + { + Name: "begin", + Usage: "Creates the GitHub check in a pull request and begins the tests", + Action: EnterpriseCheckBegin, + Flags: []cli.Flag{ + &gitHubTokenFlag, + }, + }, + { + Name: "success", + Usage: "Updates the GitHub check in a pull request to show a successful build and updates the pull request labels", + Action: EnterpriseCheckSuccess, + Flags: []cli.Flag{ + &gitHubTokenFlag, + }, + }, + { + Name: "fail", + Usage: "Updates the GitHub check in a pull request to show a failed build and updates the pull request labels", + Action: EnterpriseCheckFail, + Flags: []cli.Flag{ + &gitHubTokenFlag, + }, + }, + }, + }, } if err := app.Run(os.Args); err != nil { diff --git a/pkg/build/env/lookup.go b/pkg/build/env/lookup.go new file mode 100644 index 00000000000..993b7259e14 --- /dev/null +++ b/pkg/build/env/lookup.go @@ -0,0 +1,18 @@ +package env + +import ( + "strings" +) + +// Lookup is the equivalent of os.LookupEnv, only you are able to provide the list of environment variables. +// To use this as os.LookupEnv would be used, simply call +// `env.Lookup("ENVIRONMENT_VARIABLE", os.Environ())` +func Lookup(name string, vars []string) (string, bool) { + for _, v := range vars { + if strings.HasPrefix(v, name) { + return strings.TrimPrefix(v, name+"="), true + } + } + + return "", false +} diff --git a/pkg/build/env/lookup_test.go b/pkg/build/env/lookup_test.go new file mode 100644 index 00000000000..cebfb4fac29 --- /dev/null +++ b/pkg/build/env/lookup_test.go @@ -0,0 +1,43 @@ +package env_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/build/env" + + "github.com/stretchr/testify/require" +) + +func TestLookup(t *testing.T) { + values := []string{"ENV_1=a", "ENV_2=b", "ENV_3=c", "ENV_4_TEST="} + + { + v, ok := env.Lookup("ENV_1", values) + require.Equal(t, v, "a") + require.True(t, ok) + } + + { + v, ok := env.Lookup("ENV_2", values) + require.Equal(t, v, "b") + require.True(t, ok) + } + + { + v, ok := env.Lookup("ENV_3", values) + require.Equal(t, v, "c") + require.True(t, ok) + } + + { + v, ok := env.Lookup("ENV_4_TEST", values) + require.Equal(t, v, "") + require.True(t, ok) + } + + { + v, ok := env.Lookup("NOT_THERE", values) + require.Equal(t, v, "") + require.False(t, ok) + } +} diff --git a/pkg/build/git/git.go b/pkg/build/git/git.go new file mode 100644 index 00000000000..63fa5147f7c --- /dev/null +++ b/pkg/build/git/git.go @@ -0,0 +1,143 @@ +package git + +import ( + "context" + "errors" + "fmt" + "net/http" + "regexp" + + "github.com/google/go-github/v45/github" + "github.com/grafana/grafana/pkg/build/stringutil" + "golang.org/x/oauth2" +) + +const ( + MainBranch = "main" + HomeDir = "." + RepoOwner = "grafana" + OSSRepo = "grafana" + EnterpriseRepo = "grafana-enterprise" + EnterpriseCheckName = "Grafana Enterprise" + EnterpriseCheckDescription = "Downstream tests to ensure that your changes are compatible with Grafana Enterprise" +) + +var EnterpriseCheckLabels = []string{"enterprise-ok", "enterprise-failed", "enterprise-override"} + +var ( + ErrorNoDroneBuildLink = errors.New("no drone build link") +) + +type GitService interface { + DeleteRef(ctx context.Context, owner string, repo string, ref string) (*github.Response, error) +} + +type LabelsService interface { + ListLabelsByIssue(ctx context.Context, owner string, repo string, number int, opts *github.ListOptions) ([]*github.Label, *github.Response, error) + RemoveLabelForIssue(ctx context.Context, owner string, repo string, number int, label string) (*github.Response, error) + AddLabelsToIssue(ctx context.Context, owner string, repo string, number int, labels []string) ([]*github.Label, *github.Response, error) +} + +type CommentService interface { + CreateComment(ctx context.Context, owner string, repo string, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error) +} + +type StatusesService interface { + CreateStatus(ctx context.Context, owner, repo, ref string, status *github.RepoStatus) (*github.RepoStatus, *github.Response, error) +} + +// NewGitHubClient creates a new Client using the provided GitHub token if not empty. +func NewGitHubClient(ctx context.Context, token string) *github.Client { + var tc *http.Client + if token != "" { + ts := oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token, + }) + tc = oauth2.NewClient(ctx, ts) + } + + return github.NewClient(tc) +} + +func PRCheckRegexp() *regexp.Regexp { + reBranch, err := regexp.Compile(`^pr-check-([0-9]+)\/(.+)$`) + if err != nil { + panic(fmt.Sprintf("Failed to compile regexp: %s", err)) + } + + return reBranch +} + +func AddLabelToPR(ctx context.Context, client LabelsService, prID int, newLabel string) error { + // Check existing labels + labels, _, err := client.ListLabelsByIssue(ctx, RepoOwner, OSSRepo, prID, nil) + if err != nil { + return err + } + + duplicate := false + for _, label := range labels { + if *label.Name == newLabel { + duplicate = true + continue + } + + // Delete existing "enterprise-xx" labels + if stringutil.Contains(EnterpriseCheckLabels, *label.Name) { + _, err := client.RemoveLabelForIssue(ctx, RepoOwner, OSSRepo, prID, *label.Name) + if err != nil { + return err + } + } + } + + if duplicate { + return nil + } + + _, _, err = client.AddLabelsToIssue(ctx, RepoOwner, OSSRepo, prID, []string{newLabel}) + if err != nil { + return err + } + + return nil +} + +func DeleteEnterpriseBranch(ctx context.Context, client GitService, branchName string) error { + ref := "heads/" + branchName + _, err := client.DeleteRef(ctx, RepoOwner, EnterpriseRepo, ref) + if err != nil { + return err + } + + return nil +} + +// CreateEnterpriseStatus sets the status on a commit for the enterprise build check. +func CreateEnterpriseStatus(ctx context.Context, client StatusesService, sha, link, status string) (*github.RepoStatus, error) { + check, _, err := client.CreateStatus(ctx, RepoOwner, OSSRepo, sha, &github.RepoStatus{ + Context: github.String(EnterpriseCheckName), + Description: github.String(EnterpriseCheckDescription), + TargetURL: github.String(link), + State: github.String(status), + }) + + if err != nil { + return nil, err + } + + return check, nil +} + +func CreateEnterpriseBuildFailedComment(ctx context.Context, client CommentService, link string, prID int) error { + body := fmt.Sprintf("Drone build failed: %s", link) + + _, _, err := client.CreateComment(ctx, RepoOwner, OSSRepo, prID, &github.IssueComment{ + Body: &body, + }) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/build/git/git_checks_test.go b/pkg/build/git/git_checks_test.go new file mode 100644 index 00000000000..ed3d34a8d28 --- /dev/null +++ b/pkg/build/git/git_checks_test.go @@ -0,0 +1,55 @@ +package git_test + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-github/v45/github" + "github.com/grafana/grafana/pkg/build/git" + "github.com/stretchr/testify/require" +) + +type TestChecksService struct { + CreateCheckRunError error +} + +func (s *TestChecksService) CreateStatus(ctx context.Context, owner, repo, ref string, status *github.RepoStatus) (*github.RepoStatus, *github.Response, error) { + if s.CreateCheckRunError != nil { + return nil, nil, s.CreateCheckRunError + } + + return &github.RepoStatus{ + ID: github.Int64(1), + URL: status.URL, + }, nil, nil +} + +func TestCreateEnterpriseRepoStatus(t *testing.T) { + t.Run("It should create a repo status", func(t *testing.T) { + var ( + ctx = context.Background() + client = &TestChecksService{} + link = "http://example.com" + sha = "1234" + ) + + _, err := git.CreateEnterpriseStatus(ctx, client, link, sha, "success") + + require.NoError(t, err) + }) + t.Run("It should return an error if GitHub fails to create the status", func(t *testing.T) { + var ( + ctx = context.Background() + createCheckError = errors.New("create check run error") + client = &TestChecksService{ + CreateCheckRunError: createCheckError, + } + link = "http://example.com" + sha = "1234" + ) + + _, err := git.CreateEnterpriseStatus(ctx, client, link, sha, "success") + require.ErrorIs(t, err, createCheckError) + }) +} diff --git a/pkg/build/git/git_issues_test.go b/pkg/build/git/git_issues_test.go new file mode 100644 index 00000000000..4eaa3bb6169 --- /dev/null +++ b/pkg/build/git/git_issues_test.go @@ -0,0 +1,134 @@ +package git_test + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-github/v45/github" + "github.com/grafana/grafana/pkg/build/git" + "github.com/stretchr/testify/require" +) + +type TestLabelsService struct { + Labels []*github.Label + ListLabelsError error + RemoveLabelError error + AddLabelsError error +} + +func (s *TestLabelsService) ListLabelsByIssue(ctx context.Context, owner string, repo string, number int, opts *github.ListOptions) ([]*github.Label, *github.Response, error) { + if s.ListLabelsError != nil { + return nil, nil, s.ListLabelsError + } + + labels := s.Labels + if labels == nil { + labels = []*github.Label{} + } + + return labels, nil, nil +} + +func (s *TestLabelsService) RemoveLabelForIssue(ctx context.Context, owner string, repo string, number int, label string) (*github.Response, error) { + if s.RemoveLabelError != nil { + return nil, s.RemoveLabelError + } + + return &github.Response{}, nil +} + +func (s *TestLabelsService) AddLabelsToIssue(ctx context.Context, owner string, repo string, number int, labels []string) ([]*github.Label, *github.Response, error) { + if s.AddLabelsError != nil { + return nil, nil, s.AddLabelsError + } + + l := make([]*github.Label, len(labels)) + for i, v := range labels { + l[i] = &github.Label{ + Name: github.String(v), + } + } + + return l, nil, nil +} + +func TestAddLabelToPR(t *testing.T) { + t.Run("It should add a label to a pull request", func(t *testing.T) { + var ( + ctx = context.Background() + client = &TestLabelsService{} + pr = 20 + label = "test-label" + ) + + require.NoError(t, git.AddLabelToPR(ctx, client, pr, label)) + }) + t.Run("It should not return an error if the label already exists", func(t *testing.T) { + var ( + ctx = context.Background() + client = &TestLabelsService{ + Labels: []*github.Label{ + { + Name: github.String("test-label"), + }, + }, + } + pr = 20 + label = "test-label" + ) + + require.NoError(t, git.AddLabelToPR(ctx, client, pr, label)) + }) + + t.Run("It should return an error if GitHub returns an error when listing labels", func(t *testing.T) { + var ( + ctx = context.Background() + listLabelsError = errors.New("list labels error") + client = &TestLabelsService{ + ListLabelsError: listLabelsError, + Labels: []*github.Label{}, + } + pr = 20 + label = "test-label" + ) + + require.ErrorIs(t, git.AddLabelToPR(ctx, client, pr, label), listLabelsError) + }) + + t.Run("It should not return an error if there are existing enterprise-check labels.", func(t *testing.T) { + var ( + ctx = context.Background() + client = &TestLabelsService{ + Labels: []*github.Label{ + { + Name: github.String("enterprise-failed"), + }, + }, + } + pr = 20 + label = "test-label" + ) + + require.NoError(t, git.AddLabelToPR(ctx, client, pr, label)) + }) + + t.Run("It should return an error if GitHub returns an error when removing existing enterprise-check labels", func(t *testing.T) { + var ( + ctx = context.Background() + removeLabelError = errors.New("remove label error") + client = &TestLabelsService{ + RemoveLabelError: removeLabelError, + Labels: []*github.Label{ + { + Name: github.String("enterprise-failed"), + }, + }, + } + pr = 20 + label = "test-label" + ) + + require.ErrorIs(t, git.AddLabelToPR(ctx, client, pr, label), removeLabelError) + }) +} diff --git a/pkg/build/git/git_test.go b/pkg/build/git/git_test.go new file mode 100644 index 00000000000..ba629ca669f --- /dev/null +++ b/pkg/build/git/git_test.go @@ -0,0 +1,25 @@ +package git_test + +import ( + "testing" + + "github.com/grafana/grafana/pkg/build/git" + "github.com/stretchr/testify/assert" +) + +func TestPRCheckRegexp(t *testing.T) { + var ( + shouldMatch = []string{"pr-check-1/branch-name", "pr-check-111/branch/name", "pr-check-102930122/branch-name"} + shouldNotMatch = []string{"pr-check-a/branch", "km/test", "test", "pr-check", "pr-check/test", "price"} + ) + + regex := git.PRCheckRegexp() + + for _, v := range shouldMatch { + assert.Truef(t, regex.MatchString(v), "regex should match %s", v) + } + + for _, v := range shouldNotMatch { + assert.False(t, regex.MatchString(v), "regex should not match %s", v) + } +} diff --git a/pkg/build/stringutil/contains.go b/pkg/build/stringutil/contains.go new file mode 100644 index 00000000000..b53efe70759 --- /dev/null +++ b/pkg/build/stringutil/contains.go @@ -0,0 +1,10 @@ +package stringutil + +func Contains(arr []string, s string) bool { + for _, e := range arr { + if e == s { + return true + } + } + return false +}