diff --git a/README.md b/README.md index f48fbfb..4b3907a 100644 --- a/README.md +++ b/README.md @@ -448,6 +448,7 @@ echo "gruntwork-io/terragrunt gruntwork-io/terratest" | git-xargs \ | `--no-skip-ci` | By default, git-xargs will prepend \"[skip ci]\" to its commit messages to prevent large git-xargs jobs from creating expensive CI jobs excessively. If you pass the `--no-skip-ci` flag, then git-xargs will not prepend \"[skip ci]\". Default: false, meaning that \"[skip ci]\" will be prepended to commit messages. | Bool | No | | `--reviewers` | An optional slice of GitHub usernames, separated by commas, to request reviews from after a pull request is successfully opened. Default: empty slice, meaning that no reviewers will be requested. | String | No | | `--team-reviewers` | An optional slice of GitHub team names, separated by commas, to request reviews from after a pull request is successfully opened. Default: empty slice, meaning that no team reviewers will be requested. IMPORTANT: Please read and understand [the GitHub restrictions](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/requesting-a-pull-request-review) on this functionality before using it! Only certain GitHub organizations / payment plans support this functionality. | String | No | +| `--assignees` | An optional slice of GitHub usernames, separated by commas, to assign to the pull request after it is successfully opened. Default: empty slice, meaning that no assignees will be assigned. | String | No | | `--keep-cloned-repositories` | By default, git-xargs will delete the repositories it clones to your temporary file directory once it has completed processing that repo, to save space on your machine. If you wish to retain the local repositories, pass this flag. | Bool | No | ## Best practices, tips and tricks diff --git a/auth/auth.go b/auth/auth.go index 83b1be4..3d9f947 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -18,6 +18,11 @@ type githubPullRequestService interface { RequestReviewers(ctx context.Context, owner, repo string, number int, reviewers github.ReviewersRequest) (*github.PullRequest, *github.Response, error) } +// The go-github package satisfies this Issues service's interface in production +type githubIssuesService interface { + AddAssignees(ctx context.Context, owner, repo string, number int, assignees []string) (*github.Issue, *github.Response, error) +} + // The go-github package satisfies this Repositories service's interface in production type githubRepositoriesService interface { Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error) @@ -31,12 +36,14 @@ type githubRepositoriesService interface { // without actually making API calls to GitHub when running tests type GithubClient struct { PullRequests githubPullRequestService + Issues githubIssuesService Repositories githubRepositoriesService } func NewClient(client *github.Client) GithubClient { return GithubClient{ PullRequests: client.PullRequests, + Issues: client.Issues, Repositories: client.Repositories, } } diff --git a/cmd/git-xargs.go b/cmd/git-xargs.go index 2286ecc..b91d11b 100644 --- a/cmd/git-xargs.go +++ b/cmd/git-xargs.go @@ -34,6 +34,7 @@ func parseGitXargsConfig(c *cli.Context) (*config.GitXargsConfig, error) { config.PullRequestDescription = c.String("pull-request-description") config.Reviewers = c.StringSlice("reviewers") config.TeamReviewers = c.StringSlice("team-reviewers") + config.Assignees = c.StringSlice("assignees") config.ReposFile = c.String("repos") config.GithubOrg = c.String("github-org") config.RepoSlice = c.StringSlice("repo") diff --git a/common/common.go b/common/common.go index e731735..ccb5421 100644 --- a/common/common.go +++ b/common/common.go @@ -17,6 +17,7 @@ const ( PullRequestDescriptionFlagName = "pull-request-description" PullRequestReviewersFlagName = "reviewers" PullRequestTeamReviewersFlagName = "team-reviewers" + PullRequestAssigneesFlagName = "assignees" SecondsToWaitBetweenPrsFlagName = "seconds-between-prs" DefaultCommitMessage = "git-xargs programmatic commit" DefaultPullRequestTitle = "git-xargs programmatic pull request" @@ -92,6 +93,10 @@ var ( Name: PullRequestTeamReviewersFlagName, Usage: "A list of GitHub team names to request reviews from", } + GenericPullRequestAssigneesFlag = cli.StringSliceFlag{ + Name: PullRequestAssigneesFlagName, + Usage: "A list of GitHub usernames to request as the assignees", + } GenericSecondsToWaitFlag = cli.IntFlag{ Name: SecondsToWaitBetweenPrsFlagName, Usage: "The number of seconds to sleep between pull requests in order to respect GitHub API rate limits. Increase this number if you are being rate limited regularly. Defaults to 12 seconds.", diff --git a/config/config.go b/config/config.go index 34b7858..bd6b95c 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,7 @@ type GitXargsConfig struct { PullRequestDescription string Reviewers []string TeamReviewers []string + Assignees []string ReposFile string GithubOrg string RepoSlice []string @@ -59,6 +60,7 @@ func NewGitXargsConfig() *GitXargsConfig { PullRequestDescription: common.DefaultPullRequestDescription, Reviewers: []string{}, TeamReviewers: []string{}, + Assignees: []string{}, ReposFile: "", GithubOrg: "", RepoSlice: []string{}, @@ -93,3 +95,7 @@ func NewGitXargsTestConfig() *GitXargsConfig { func (c *GitXargsConfig) HasReviewers() bool { return len(c.Reviewers) > 0 || len(c.TeamReviewers) > 0 } + +func (c *GitXargsConfig) HasAssignees() bool { + return len(c.Assignees) > 0 +} diff --git a/main.go b/main.go index 7f4f656..686fb55 100644 --- a/main.go +++ b/main.go @@ -73,6 +73,7 @@ func setupApp() *cli.App { common.GenericPullRequestDescriptionFlag, common.GenericPullRequestReviewersFlag, common.GenericPullRequestTeamReviewersFlag, + common.GenericPullRequestAssigneesFlag, common.GenericSecondsToWaitFlag, common.GenericMaxPullRequestRetriesFlag, common.GenericSecondsToWaitWhenRateLimitedFlag, diff --git a/mocks/mocks.go b/mocks/mocks.go index 08a55dd..7eeaa3a 100644 --- a/mocks/mocks.go +++ b/mocks/mocks.go @@ -87,6 +87,16 @@ func (m mockGithubPullRequestService) RequestReviewers(ctx context.Context, owne return m.PullRequest, m.Response, nil } +// This mocks the Issue service in go-github that is used in production to call the associated GitHub endpoint +type mockgithubIssuesService struct { + Issue *github.Issue + Response *github.Response +} + +func (m mockgithubIssuesService) AddAssignees(ctx context.Context, owner, repo string, number int, assignees []string) (*github.Issue, *github.Response, error) { + return m.Issue, m.Response, nil +} + // This mocks the Repositories service in go-github that is used in production to call the associated GitHub endpoint type mockGithubRepositoriesService struct { Repository *github.Repository @@ -136,6 +146,10 @@ func ConfigureMockGithubClient() auth.GithubClient { }, Response: &github.Response{}, } + client.Issues = mockgithubIssuesService{ + Issue: &github.Issue{}, + Response: &github.Response{}, + } return client } diff --git a/repository/repo-operations.go b/repository/repo-operations.go index 15e0ed9..60c435f 100644 --- a/repository/repo-operations.go +++ b/repository/repo-operations.go @@ -597,6 +597,14 @@ func openPullRequest(config *config.GitXargsConfig, pr types.OpenPrRequest) erro } + if config.HasAssignees() { + assignees := config.Assignees + _, _, assigneeErr := config.GithubClient.Issues.AddAssignees(context.Background(), *pr.Repo.GetOwner().Login, pr.Repo.GetName(), githubPR.GetNumber(), assignees) + if assigneeErr != nil { + config.Stats.TrackSingle(stats.AddAssigneesErr, pr.Repo) + } + } + if config.Draft { config.Stats.TrackDraftPullRequest(pr.Repo.GetName(), githubPR.GetHTMLURL()) } else { diff --git a/stats/stats.go b/stats/stats.go index 777c5a9..a6d227e 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -78,6 +78,8 @@ const ( PRFailedAfterMaximumRetriesErr types.Event = "pr-failed-after-maximum-retries" // RequestReviewersErr denotes a repo whose follow up request to add reviewers to the opened pull request failed RequestReviewersErr types.Event = "request-reviewers-error" + // AddAssigneesErr denotes a repo whose follow up request to add assignees to the opened pull request failed + AddAssigneesErr types.Event = "add-assignees-error" ) var allEvents = []types.AnnotatedEvent{ @@ -112,6 +114,7 @@ var allEvents = []types.AnnotatedEvent{ {Event: PRFailedDueToRateLimitsErr, Description: "Repos whose initial Pull Request failed to be created due to GitHub rate limits"}, {Event: PRFailedAfterMaximumRetriesErr, Description: "Repos whose Pull Request failed to be created after the maximum number of retries"}, {Event: RequestReviewersErr, Description: "Repos whose request to add reviewers to the opened pull request failed"}, + {Event: AddAssigneesErr, Description: "Repos whose request to add assignees to the opened pull request failed"}, } // RunStats will be a stats-tracker class that keeps score of which repos were touched, which were considered for update, which had branches made, PRs made, which were missing workflows or contexts, or had out of date workflows syntax values, etc