Repository: lindell/multi-gitter
Pull request #289 (2022-10-08 08:46:18)

fix(github): add retry mechanism to all requests


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5402cb3..4d274c6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,30 @@
 # Changelog
 
+## [0.43.0](https://www.github.com/lindell/multi-gitter/compare/v0.42.4...v0.43.0) (2022-10-18)
+
+
+### Features
+
+* added option to add labels to creates pull requests ([#292](https://www.github.com/lindell/multi-gitter/issues/292)) ([fccf678](https://www.github.com/lindell/multi-gitter/commit/fccf678c384ff01cd0247c35860acf0d257e41a7))
+
+### [0.42.4](https://www.github.com/lindell/multi-gitter/compare/v0.42.3...v0.42.4) (2022-10-17)
+
+
+### Bug Fixes
+
+* **github:** added missing closed pr status ([#290](https://www.github.com/lindell/multi-gitter/issues/290)) ([9e3644a](https://www.github.com/lindell/multi-gitter/commit/9e3644a899197470985707112238f9990df99564))
+* better error message when the same feature and base branch is used ([#281](https://www.github.com/lindell/multi-gitter/issues/281)) ([ff98e8f](https://www.github.com/lindell/multi-gitter/commit/ff98e8fff43a3ccc14feb2f24f6c54c134dbc21c))
+* retry when encounting the GitHub rate limit ([#280](https://www.github.com/lindell/multi-gitter/issues/280)) ([008a26a](https://www.github.com/lindell/multi-gitter/commit/008a26ae0182c8e69cc0679a0d7ee776415c1d62))
+
+
+### Dependencies
+
+* update golang.org/x/oauth2 digest to f213421 ([dfd1837](https://www.github.com/lindell/multi-gitter/commit/dfd18376c44f4b9a36628142fc52277b0aa48ec0))
+* update module github.com/spf13/viper to v1.13.0 ([099a9c9](https://www.github.com/lindell/multi-gitter/commit/099a9c9f3e5341b207ec298356319303df67a31f))
+* update module github.com/xanzy/go-gitlab to v0.73.1 ([badc233](https://www.github.com/lindell/multi-gitter/commit/badc23353c3f611031fa70483b6892cf8f2e0b99))
+* update module go to 1.19 ([2fdbcde](https://www.github.com/lindell/multi-gitter/commit/2fdbcde5f6160ece5b8fac38d65761278b9f0f6a))
+* update module go-github to v47 ([#286](https://www.github.com/lindell/multi-gitter/issues/286)) ([68fb1e2](https://www.github.com/lindell/multi-gitter/commit/68fb1e28af23bbdd8c77323e572a3c43d06a4445))
+
 ### [0.42.3](https://www.github.com/lindell/multi-gitter/compare/v0.42.2...v0.42.3) (2022-08-12)
 
 
diff --git a/README.md b/README.md
index 66aaa7b..505bee2 100755
--- a/README.md
+++ b/README.md
@@ -164,6 +164,10 @@ insecure: false
 # Take manual decision before committing any change. Requires git to be installed.
 interactive: false
 
+# Labels to be added to any created pull request.
+labels:
+  - example
+
 # The file where all logs should be printed to. "-" means stdout.
 log-file: "-"
 
@@ -562,6 +566,7 @@ Flags:
       --include-subgroups          Include GitLab subgroups when using the --group flag.
       --insecure                   Insecure controls whether a client verifies the server certificate chain and host name. Used only for Bitbucket server.
   -i, --interactive                Take manual decision before committing any change. Requires git to be installed.
+      --labels strings             Labels to be added to any created pull request.
       --log-file string            The file where all logs should be printed to. "-" means stdout. (default "-")
       --log-format string          The formating of the logs. Available values: text, json, json-pretty. (default "text")
   -L, --log-level string           The level of logging that should be made. Available values: trace, debug, info, error. (default "info")
diff --git a/cmd/cmd-run.go b/cmd/cmd-run.go
index 142b5a8..c37d406 100755
--- a/cmd/cmd-run.go
+++ b/cmd/cmd-run.go
@@ -54,6 +54,7 @@ Available values:
 	_ = cmd.RegisterFlagCompletionFunc("conflict-strategy", func(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {
 		return []string{"skip", "replace"}, cobra.ShellCompDirectiveNoFileComp
 	})
+	cmd.Flags().StringSliceP("labels", "", nil, "Labels to be added to any created pull request.")
 	cmd.Flags().StringP("author-name", "", "", "Name of the committer. If not set, the global git config setting will be used.")
 	cmd.Flags().StringP("author-email", "", "", "Email of the committer. If not set, the global git config setting will be used.")
 	configureGit(cmd)
@@ -89,6 +90,7 @@ func run(cmd *cobra.Command, args []string) error {
 	strOutput, _ := flag.GetString("output")
 	assignees, _ := flag.GetStringSlice("assignees")
 	draft, _ := flag.GetBool("draft")
+	labels, _ := flag.GetStringSlice("labels")
 
 	if concurrent < 1 {
 		return errors.New("concurrent runs can't be less than one")
@@ -192,6 +194,7 @@ func run(cmd *cobra.Command, args []string) error {
 		Assignees:        assignees,
 		ConflictStrategy: conflictStrategy,
 		Draft:            draft,
+		Labels:           labels,
 
 		Concurrent: concurrent,
 
diff --git a/internal/git/cmdgit/git.go b/internal/git/cmdgit/git.go
index 2655d0a..dd92bdc 100644
--- a/internal/git/cmdgit/git.go
+++ b/internal/git/cmdgit/git.go
@@ -2,6 +2,7 @@ package cmdgit
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"os/exec"
 	"regexp"
@@ -45,14 +46,14 @@ func (g *Git) run(cmd *exec.Cmd) (string, error) {
 }
 
 // Clone a repository
-func (g *Git) Clone(url string, baseName string) error {
+func (g *Git) Clone(ctx context.Context, url string, baseName string) error {
 	args := []string{"clone", url, "--branch", baseName, "--single-branch"}
 	if g.FetchDepth > 0 {
 		args = append(args, "--depth", fmt.Sprint(g.FetchDepth))
 	}
 	args = append(args, g.Directory)
 
-	cmd := exec.Command("git", args...)
+	cmd := exec.CommandContext(ctx, "git", args...)
 	_, err := g.run(cmd)
 	return err
 }
@@ -129,14 +130,14 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
 }
 
 // Push the committed changes to the remote
-func (g *Git) Push(remoteName string, force bool) error {
+func (g *Git) Push(ctx context.Context, remoteName string, force bool) error {
 	args := []string{"push", "--no-verify", remoteName}
 	if force {
 		args = append(args, "--force")
 	}
 	args = append(args, "HEAD")
 
-	cmd := exec.Command("git", args...)
+	cmd := exec.CommandContext(ctx, "git", args...)
 	_, err := g.run(cmd)
 	return err
 }
diff --git a/internal/git/gogit/git.go b/internal/git/gogit/git.go
index 5bbb9c8..39aecbf 100755
--- a/internal/git/gogit/git.go
+++ b/internal/git/gogit/git.go
@@ -2,6 +2,7 @@ package gogit
 
 import (
 	"bytes"
+	"context"
 	"time"
 
 	"github.com/go-git/go-git/v5/config"
@@ -24,8 +25,8 @@ type Git struct {
 }
 
 // Clone a repository
-func (g *Git) Clone(url string, baseName string) error {
-	r, err := git.PlainClone(g.Directory, false, &git.CloneOptions{
+func (g *Git) Clone(ctx context.Context, url string, baseName string) error {
+	r, err := git.PlainCloneContext(ctx, g.Directory, false, &git.CloneOptions{
 		URL:           url,
 		RemoteName:    "origin",
 		Depth:         g.FetchDepth,
@@ -202,8 +203,8 @@ func (g *Git) BranchExist(remoteName, branchName string) (bool, error) {
 }
 
 // Push the committed changes to the remote
-func (g *Git) Push(remoteName string, force bool) error {
-	return g.repo.Push(&git.PushOptions{
+func (g *Git) Push(ctx context.Context, remoteName string, force bool) error {
+	return g.repo.PushContext(ctx, &git.PushOptions{
 		RemoteName: remoteName,
 		Force:      force,
 	})
diff --git a/internal/multigitter/print.go b/internal/multigitter/print.go
index 196245b..9735479 100755
--- a/internal/multigitter/print.go
+++ b/internal/multigitter/print.go
@@ -77,14 +77,14 @@ func (r Printer) runSingleRepo(ctx context.Context, repo scm.Repository) error {
 
 	sourceController := r.CreateGit(tmpDir)
 
-	err = sourceController.Clone(repo.CloneURL(), repo.DefaultBranch())
+	err = sourceController.Clone(ctx, repo.CloneURL(), repo.DefaultBranch())
 	if err != nil {
 		return err
 	}
 
 	// Run the command that might or might not change the content of the repo
 	// If the command return a non zero exit code, abort.
-	cmd := exec.Command(r.ScriptPath, r.Arguments...)
+	cmd := exec.CommandContext(ctx, r.ScriptPath, r.Arguments...)
 	cmd.Dir = tmpDir
 	cmd.Env = append(os.Environ(),
 		fmt.Sprintf("REPOSITORY=%s", repo.FullName()),
diff --git a/internal/multigitter/run.go b/internal/multigitter/run.go
index 923c0ce..5c27162 100755
--- a/internal/multigitter/run.go
+++ b/internal/multigitter/run.go
@@ -63,6 +63,8 @@ type Runner struct {
 
 	Draft bool // If set, creates Pull Requests as draft
 
+	Labels []string // Labels to be added to the pull request
+
 	Interactive bool // If set, interactive mode is activated and the user will be asked to verify every change
 
 	CreateGit func(dir string) Git
@@ -214,7 +216,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 		return nil, errors.Errorf("both the feature branch and base branch was named %s, if you intended to push directly into the base branch, please use the `skip-pr` option", baseBranch)
 	}
 
-	err = sourceController.Clone(repo.CloneURL(), baseBranch)
+	err = sourceController.Clone(ctx, repo.CloneURL(), baseBranch)
 	if err != nil {
 		return nil, err
 	}
@@ -229,7 +231,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 
 	// Run the command that might or might not change the content of the repo
 	// If the command return a non zero exit code, abort.
-	cmd := exec.Command(r.ScriptPath, r.Arguments...)
+	cmd := exec.CommandContext(ctx, r.ScriptPath, r.Arguments...)
 	cmd.Dir = tmpDir
 	cmd.Env = append(os.Environ(),
 		fmt.Sprintf("REPOSITORY=%s", repo.FullName()),
@@ -301,7 +303,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 
 	log.Info("Pushing changes to remote")
 	forcePush := featureBranchExist && r.ConflictStrategy == ConflictStrategyReplace
-	err = sourceController.Push(remoteName, forcePush)
+	err = sourceController.Push(ctx, remoteName, forcePush)
 	if err != nil {
 		return nil, errors.Wrap(err, "could not push changes")
 	}
@@ -334,6 +336,7 @@ func (r *Runner) runSingleRepo(ctx context.Context, repo scm.Repository) (scm.Pu
 			Reviewers: getReviewers(r.Reviewers, r.MaxReviewers),
 			Assignees: r.Assignees,
 			Draft:     r.Draft,
+			Labels:    r.Labels,
 		})
 		if err != nil {
 			return nil, err
diff --git a/internal/multigitter/shared.go b/internal/multigitter/shared.go
index cd030fa..d05b5f7 100644
--- a/internal/multigitter/shared.go
+++ b/internal/multigitter/shared.go
@@ -1,6 +1,7 @@
 package multigitter
 
 import (
+	"context"
 	"fmt"
 	"syscall"
 
@@ -24,12 +25,12 @@ func transformExecError(err error) error {
 
 // Git is a git implementation
 type Git interface {
-	Clone(url string, baseName string) error
+	Clone(ctx context.Context, url string, baseName string) error
 	ChangeBranch(branchName string) error
 	Changes() (bool, error)
 	Commit(commitAuthor *git.CommitAuthor, commitMessage string) error
 	BranchExist(remoteName, branchName string) (bool, error)
-	Push(remoteName string, force bool) error
+	Push(ctx context.Context, remoteName string, force bool) error
 	AddRemote(name, url string) error
 }
 
diff --git a/internal/scm/gitea/gitea.go b/internal/scm/gitea/gitea.go
index 488287f..40e8045 100644
--- a/internal/scm/gitea/gitea.go
+++ b/internal/scm/gitea/gitea.go
@@ -216,12 +216,18 @@ func (g *Gitea) CreatePullRequest(ctx context.Context, repo scm.Repository, prRe
 		prTitle = "WIP: " + prTitle // See https://docs.gitea.io/en-us/pull-request/
 	}
 
+	labels, err := g.getLabelsFromStrings(ctx, r, newPR.Labels)
+	if err != nil {
+		return nil, errors.WithMessage(err, "could not map labels")
+	}
+
 	pr, _, err := g.giteaClient(ctx).CreatePullRequest(r.ownerName, r.name, gitea.CreatePullRequestOption{
 		Head:      head,
 		Base:      newPR.Base,
 		Title:     prTitle,
 		Body:      newPR.Body,
 		Assignees: newPR.Assignees,
+		Labels:    labels,
 	})
 	if err != nil {
 		return nil, errors.Wrap(err, "could not create pull request")
@@ -245,6 +251,33 @@ func (g *Gitea) CreatePullRequest(ctx context.Context, repo scm.Repository, prRe
 	}, nil
 }
 
+func (g *Gitea) getLabelsFromStrings(ctx context.Context, repo repository, labelNames []string) ([]int64, error) {
+	if len(labelNames) == 0 {
+		return nil, nil
+	}
+
+	labels, _, err := g.giteaClient(ctx).ListRepoLabels(repo.ownerName, repo.name, gitea.ListLabelsOptions{})
+	if err != nil {
+		return nil, err
+	}
+
+	// Create a map for quick lookup
+	labelMap := map[string]int64{}
+	for _, label := range labels {
+		labelMap[label.Name] = label.ID
+	}
+
+	// Get the ids of all labels, if the label-name does not exist, simply skip it
+	var ret []int64
+	for _, name := range labelNames {
+		if id, ok := labelMap[name]; ok {
+			ret = append(ret, id)
+		}
+	}
+
+	return ret, nil
+}
+
 // GetPullRequests gets all pull requests of with a specific branch
 func (g *Gitea) GetPullRequests(ctx context.Context, branchName string) ([]scm.PullRequest, error) {
 	repos, err := g.getRepositories(ctx)
diff --git a/internal/scm/github/github.go b/internal/scm/github/github.go
index ce89b1b..cd54b94 100755
--- a/internal/scm/github/github.go
+++ b/internal/scm/github/github.go
@@ -214,11 +214,13 @@ func (g *Github) getOrganizationRepositories(ctx context.Context, orgName string
 	var repos []*github.Repository
 	i := 1
 	for {
-		rr, _, err := g.ghClient.Repositories.ListByOrg(ctx, orgName, &github.RepositoryListByOrgOptions{
-			ListOptions: github.ListOptions{
-				Page:    i,
-				PerPage: 100,
-			},
+		rr, _, err := retry(ctx, func() ([]*github.Repository, *github.Response, error) {
+			return g.ghClient.Repositories.ListByOrg(ctx, orgName, &github.RepositoryListByOrgOptions{
+				ListOptions: github.ListOptions{
+					Page:    i,
+					PerPage: 100,
+				},
+			})
 		})
 		if err != nil {
 			return nil, err
@@ -237,11 +239,13 @@ func (g *Github) getUserRepositories(ctx context.Context, user string) ([]*githu
 	var repos []*github.Repository
 	i := 1
 	for {
-		rr, _, err := g.ghClient.Repositories.List(ctx, user, &github.RepositoryListOptions{
-			ListOptions: github.ListOptions{
-				Page:    i,
-				PerPage: 100,
-			},
+		rr, _, err := retry(ctx, func() ([]*github.Repository, *github.Response, error) {
+			return g.ghClient.Repositories.List(ctx, user, &github.RepositoryListOptions{
+				ListOptions: github.ListOptions{
+					Page:    i,
+					PerPage: 100,
+				},
+			})
 		})
 		if err != nil {
 			return nil, err
@@ -257,7 +261,10 @@ func (g *Github) getUserRepositories(ctx context.Context, user string) ([]*githu
 }
 
 func (g *Github) getRepository(ctx context.Context, repoRef RepositoryReference) (*github.Repository, error) {
-	repo, _, err := g.ghClient.Repositories.Get(ctx, repoRef.OwnerName, repoRef.Name)
+	repo, _, err := retry(ctx, func() (*github.Repository, *github.Response, error) {
+		return g.ghClient.Repositories.Get(ctx, repoRef.OwnerName, repoRef.Name)
+	})
+
 	if err != nil {
 		return nil, err
 	}
@@ -285,6 +292,10 @@ func (g *Github) CreatePullRequest(ctx context.Context, repo scm.Repository, prR
 		return nil, err
 	}
 
+	if err := g.addLabels(ctx, r, newPR, pr); err != nil {
+		return nil, err
+	}
+
 	return convertPullRequest(pr), nil
 }
 
@@ -325,6 +336,16 @@ func (g *Github) addAssignees(ctx context.Context, repo repository, newPR scm.Ne
 	return err
 }
 
+func (g *Github) addLabels(ctx context.Context, repo repository, newPR scm.NewPullRequest, createdPR *github.PullRequest) error {
+	if len(newPR.Labels) == 0 {
+		return nil
+	}
+	_, _, err := retry(ctx, func() ([]*github.Label, *github.Response, error) {
+		return g.ghClient.Issues.AddLabelsToIssue(ctx, repo.ownerName, repo.name, createdPR.GetNumber(), newPR.Labels)
+	})
+	return err
+}
+
 // GetPullRequests gets all pull requests of with a specific branch
 func (g *Github) GetPullRequests(ctx context.Context, branchName string) ([]scm.PullRequest, error) {
 	repos, err := g.getRepositories(ctx)
@@ -438,7 +459,9 @@ func (g *Github) loggedInUser(ctx context.Context) (string, error) {
 		return g.user, nil
 	}
 
-	user, _, err := g.ghClient.Users.Get(ctx, "")
+	user, _, err := retry(ctx, func() (*github.User, *github.Response, error) {
+		return g.ghClient.Users.Get(ctx, "")
+	})
 	if err != nil {
 		return "", err
 	}
@@ -472,12 +495,14 @@ func (g *Github) GetOpenPullRequest(ctx context.Context, repo scm.Repository, br
 		return nil, err
 	}
 
-	prs, _, err := g.ghClient.PullRequests.List(ctx, headOwner, r.name, &github.PullRequestListOptions{
-		Head:  fmt.Sprintf("%s:%s", headOwner, branchName),
-		State: "open",
-		ListOptions: github.ListOptions{
-			PerPage: 1,
-		},
+	prs, _, err := retry(ctx, func() ([]*github.PullRequest, *github.Response, error) {
+		return g.ghClient.PullRequests.List(ctx, headOwner, r.name, &github.PullRequestListOptions{
+			Head:  fmt.Sprintf("%s:%s", headOwner, branchName),
+			State: "open",
+			ListOptions: github.ListOptions{
+				PerPage: 1,
+			},
+		})
 	})
 	if err != nil {
 		return nil, err
@@ -496,7 +521,9 @@ func (g *Github) MergePullRequest(ctx context.Context, pullReq scm.PullRequest)
 	defer g.modUnlock()
 
 	// We need to fetch the repo again since no AllowXMerge is present in listings of repositories
-	repo, _, err := g.ghClient.Repositories.Get(ctx, pr.ownerName, pr.repoName)
+	repo, _, err := retry(ctx, func() (*github.Repository, *github.Response, error) {
+		return g.ghClient.Repositories.Get(ctx, pr.ownerName, pr.repoName)
+	})
 	if err != nil {
 		return err
 	}
@@ -535,14 +562,18 @@ func (g *Github) ClosePullRequest(ctx context.Context, pullReq scm.PullRequest)
 	g.modLock()
 	defer g.modUnlock()
 
-	_, _, err := g.ghClient.PullRequests.Edit(ctx, pr.ownerName, pr.repoName, pr.number, &github.PullRequest{
-		State: &[]string{"closed"}[0],
+	_, _, err := retry(ctx, func() (*github.PullRequest, *github.Response, error) {
+		return g.ghClient.PullRequests.Edit(ctx, pr.ownerName, pr.repoName, pr.number, &github.PullRequest{
+			State: &[]string{"closed"}[0],
+		})
 	})
 	if err != nil {
 		return err
 	}
 
-	_, err = g.ghClient.Git.DeleteRef(ctx, pr.prOwnerName, pr.prRepoName, fmt.Sprintf("heads/%s", pr.branchName))
+	_, err = retryWithoutReturn(ctx, func() (*github.Response, error) {
+		return g.ghClient.Git.DeleteRef(ctx, pr.prOwnerName, pr.prRepoName, fmt.Sprintf("heads/%s", pr.branchName))
+	})
 	return err
 }
 
@@ -568,7 +599,9 @@ func (g *Github) ForkRepository(ctx context.Context, repo scm.Repository, newOwn
 		var err error
 		var repo *github.Repository
 		for i := 0; i < 10; i++ {
-			repo, _, err = g.ghClient.Repositories.Get(ctx, createdRepo.GetOwner().GetLogin(), createdRepo.GetName())
+			repo, _, err = retry(ctx, func() (*github.Repository, *github.Response, error) {
+				return g.ghClient.Repositories.Get(ctx, createdRepo.GetOwner().GetLogin(), createdRepo.GetName())
+			})
 			if err != nil {
 				time.Sleep(time.Second * 3)
 				continue
@@ -585,7 +618,9 @@ func (g *Github) ForkRepository(ctx context.Context, repo scm.Repository, newOwn
 
 // GetAutocompleteOrganizations gets organizations for autocompletion
 func (g *Github) GetAutocompleteOrganizations(ctx context.Context, _ string) ([]string, error) {
-	orgs, _, err := g.ghClient.Organizations.List(ctx, "", nil)
+	orgs, _, err := retry(ctx, func() ([]*github.Organization, *github.Response, error) {
+		return g.ghClient.Organizations.List(ctx, "", nil)
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -600,7 +635,9 @@ func (g *Github) GetAutocompleteOrganizations(ctx context.Context, _ string) ([]
 
 // GetAutocompleteUsers gets users for autocompletion
 func (g *Github) GetAutocompleteUsers(ctx context.Context, str string) ([]string, error) {
-	users, _, err := g.ghClient.Search.Users(ctx, str, nil)
+	users, _, err := retry(ctx, func() (*github.UsersSearchResult, *github.Response, error) {
+		return g.ghClient.Search.Users(ctx, str, nil)
+	})
 	if err != nil {
 		return nil, err
 	}
@@ -628,7 +665,9 @@ func (g *Github) GetAutocompleteRepositories(ctx context.Context, str string) ([
 		q = fmt.Sprintf("%s in:name", str)
 	}
 
-	repos, _, err := g.ghClient.Search.Repositories(ctx, q, nil)
+	repos, _, err := retry(ctx, func() (*github.RepositoriesSearchResult, *github.Response, error) {
+		return g.ghClient.Search.Repositories(ctx, q, nil)
+	})
 	if err != nil {
 		return nil, err
 	}
diff --git a/internal/scm/github/pullrequest.go b/internal/scm/github/pullrequest.go
index 8a5dbc5..5df8b02 100644
--- a/internal/scm/github/pullrequest.go
+++ b/internal/scm/github/pullrequest.go
@@ -26,6 +26,8 @@ func convertGraphQLPullRequest(pr graphqlPR) pullRequest {
 
 	if pr.Merged {
 		status = scm.PullRequestStatusMerged
+	} else if pr.Closed {
+		status = scm.PullRequestStatusClosed
 	} else if combinedStatus == nil {
 		status = scm.PullRequestStatusSuccess
 	} else {
diff --git a/internal/scm/gitlab/gitlab.go b/internal/scm/gitlab/gitlab.go
index fe0165f..4bba751 100644
--- a/internal/scm/gitlab/gitlab.go
+++ b/internal/scm/gitlab/gitlab.go
@@ -226,6 +226,7 @@ func (g *Gitlab) CreatePullRequest(ctx context.Context, repo scm.Repository, prR
 		prTitle = "Draft: " + prTitle // See https://docs.gitlab.com/ee/user/project/merge_requests/drafts.html#mark-merge-requests-as-drafts
 	}
 
+	labels := gitlab.Labels(newPR.Labels)
 	removeSourceBranch := true
 	mr, _, err := g.glClient.MergeRequests.CreateMergeRequest(prR.pid, &gitlab.CreateMergeRequestOptions{
 		Title:              &prTitle,
@@ -237,6 +238,7 @@ func (g *Gitlab) CreatePullRequest(ctx context.Context, repo scm.Repository, prR
 		RemoveSourceBranch: &removeSourceBranch,
 		Squash:             &r.shouldSquash,
 		AssigneeIDs:        &assigneesIDs,
+		Labels:             &labels,
 	})
 	if err != nil {
 		return nil, err
diff --git a/internal/scm/pullrequest.go b/internal/scm/pullrequest.go
index 285bf4b..68f7567 100755
--- a/internal/scm/pullrequest.go
+++ b/internal/scm/pullrequest.go
@@ -15,6 +15,7 @@ type NewPullRequest struct {
 	Reviewers []string // The username of all reviewers
 	Assignees []string
 	Draft     bool
+	Labels    []string
 }
 
 // PullRequestStatus is the status of a pull request, including statuses of the last commit
diff --git a/version.txt b/version.txt
index ae738d3..8298bb0 100644
--- a/version.txt
+++ b/version.txt
@@ -1,1 +1,1 @@
-0.42.3
+0.43.0


Test patch:
diff --git a/tests/table_test.go b/tests/table_test.go
index 37866d7..69ce932 100644
--- a/tests/table_test.go
+++ b/tests/table_test.go
@@ -879,6 +879,30 @@ Repositories with a successful run:
 			},
 		},
 
+		{
+			name: "labels",
+			vcCreate: func(t *testing.T) *vcmock.VersionController {
+				return &vcmock.VersionController{
+					Repositories: []vcmock.Repository{
+						createRepo(t, "owner", "should-change", "i like apples"),
+					},
+				}
+			},
+			args: []string{
+				"run",
+				"--author-name", "Test Author",
+				"--author-email", "test@example.com",
+				"-B", "custom-branch-name",
+				"-m", "custom message",
+				"--labels", "label1,label2",
+				changerBinaryPath,
+			},
+			verify: func(t *testing.T, vcMock *vcmock.VersionController, runData runData) {
+				require.Len(t, vcMock.PullRequests, 1)
+				assert.Equal(t, []string{"label1", "label2"}, vcMock.PullRequests[0].Labels)
+			},
+		},
+
 		{
 			name: "remove files",
 			vcCreate: func(t *testing.T) *vcmock.VersionController {