Skip to content
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,8 @@ Next, to push and raise PRs against changed repos, run:

Use `turbolift create-prs --sleep 30s` to, for example, force a 30s pause between creation of each PR. This can be helpful in reducing load on shared infrastructure.

By default Turbolift applies a `turbolift` label to each PR it creates. Opt out with `turbolift create-prs --no-apply-labels`.

> Important: if raising many PRs, you may generate load on shared infrastructure such as CI. It is *highly* recommended that you:
> * slow the rate of PR creation by making Turbolift sleep in between PRs
> * create PRs in batches, for example by commenting out repositories in `repos.txt`
Expand Down
8 changes: 8 additions & 0 deletions cmd/create_prs/create_prs.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ var (
repoFile string
prDescriptionFile string
sleep time.Duration
noApplyLabels bool
)

func NewCreatePRsCmd() *cobra.Command {
Expand All @@ -55,6 +56,7 @@ func NewCreatePRsCmd() *cobra.Command {

cmd.Flags().DurationVar(&sleep, "sleep", 0, "Fixed sleep in between PR creations (to spread load on CI infrastructure)")
cmd.Flags().BoolVar(&isDraft, "draft", false, "Creates the Pull Request as Draft PR")
cmd.Flags().BoolVar(&noApplyLabels, "no-apply-labels", false, "Do not apply the default turbolift label to created PRs")
cmd.Flags().StringVar(&repoFile, "repos", "repos.txt", "A file containing a list of repositories to clone.")
cmd.Flags().StringVar(&prDescriptionFile, "description", "README.md", "A file containing the title and description for the PRs.")

Expand Down Expand Up @@ -116,11 +118,17 @@ func run(c *cobra.Command, _ []string) {
createPrActivity = logger.StartActivity("Creating PR in %s", repo.FullRepoName)
}

labels := []string{github.TurboliftLabel}
if noApplyLabels {
labels = nil
}

pullRequest := github.PullRequest{
Title: dir.PrTitle,
Body: dir.PrBody,
UpstreamRepo: repo.FullRepoName,
IsDraft: isDraft,
Labels: labels,
}

didCreate, err := gh.CreatePullRequest(createPrActivity.Writer(), repoDirPath, pullRequest)
Expand Down
48 changes: 38 additions & 10 deletions cmd/create_prs/create_prs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,8 @@ func TestItLogsCreatePrErrorsButContinuesToTryAll(t *testing.T) {
assert.Contains(t, out, "2 errored")

fakeGitHub.AssertCalledWith(t, [][]string{
{"create_pull_request", "work/org/repo1", "PR title"},
{"create_pull_request", "work/org/repo2", "PR title"},
{"create_pull_request", "work/org/repo1", "PR title", "turbolift"},
{"create_pull_request", "work/org/repo2", "PR title", "turbolift"},
})
}

Expand All @@ -150,8 +150,8 @@ func TestItLogsCreatePrSkippedButContinuesToTryAll(t *testing.T) {
assert.Contains(t, out, "0 OK, 2 skipped")

fakeGitHub.AssertCalledWith(t, [][]string{
{"create_pull_request", "work/org/repo1", "PR title"},
{"create_pull_request", "work/org/repo2", "PR title"},
{"create_pull_request", "work/org/repo1", "PR title", "turbolift"},
{"create_pull_request", "work/org/repo2", "PR title", "turbolift"},
})
}

Expand All @@ -169,8 +169,8 @@ func TestItLogsCreatePrsSucceeds(t *testing.T) {
assert.Contains(t, out, "2 OK, 0 skipped")

fakeGitHub.AssertCalledWith(t, [][]string{
{"create_pull_request", "work/org/repo1", "PR title"},
{"create_pull_request", "work/org/repo2", "PR title"},
{"create_pull_request", "work/org/repo1", "PR title", "turbolift"},
{"create_pull_request", "work/org/repo2", "PR title", "turbolift"},
})
}

Expand All @@ -190,8 +190,8 @@ func TestItLogsCreateDraftPr(t *testing.T) {
assert.Contains(t, out, "2 OK, 0 skipped")

fakeGitHub.AssertCalledWith(t, [][]string{
{"create_pull_request", "work/org/repo1", "PR title"},
{"create_pull_request", "work/org/repo2", "PR title"},
{"create_pull_request", "work/org/repo1", "PR title", "turbolift"},
{"create_pull_request", "work/org/repo2", "PR title", "turbolift"},
})
}

Expand All @@ -215,8 +215,27 @@ func TestItCreatesPrsFromAlternativeDescriptionFile(t *testing.T) {
assert.Contains(t, out, "2 OK, 0 skipped")

fakeGitHub.AssertCalledWith(t, [][]string{
{"create_pull_request", "work/org/repo1", "custom PR title"},
{"create_pull_request", "work/org/repo2", "custom PR title"},
{"create_pull_request", "work/org/repo1", "custom PR title", "turbolift"},
{"create_pull_request", "work/org/repo2", "custom PR title", "turbolift"},
})
}

func TestItCanSkipApplyingLabels(t *testing.T) {
fakeGitHub := github.NewAlwaysSucceedsFakeGitHub()
gh = fakeGitHub
fakeGit := git.NewAlwaysSucceedsFakeGit()
g = fakeGit

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")

out, err := runCommandNoLabels()
assert.NoError(t, err)
assert.Contains(t, out, "turbolift create-prs completed")
assert.Contains(t, out, "2 OK, 0 skipped")

fakeGitHub.AssertCalledWith(t, [][]string{
{"create_pull_request", "work/org/repo1", "PR title", ""},
{"create_pull_request", "work/org/repo2", "PR title", ""},
})
}

Expand All @@ -228,6 +247,15 @@ func runCommand() (string, error) {
return outBuffer.String(), err
}

func runCommandNoLabels() (string, error) {
cmd := NewCreatePRsCmd()
cmd.SetArgs([]string{"--no-apply-labels"})
outBuffer := bytes.NewBufferString("")
cmd.SetOut(outBuffer)
err := cmd.Execute()
return outBuffer.String(), err
}

func runCommandWithAlternativeDescriptionFile(fileName string) (string, error) {
cmd := NewCreatePRsCmd()
prDescriptionFile = fileName
Expand Down
4 changes: 3 additions & 1 deletion internal/github/fake_github.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package github
import (
"errors"
"io"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand All @@ -42,7 +43,8 @@ type FakeGitHub struct {
}

func (f *FakeGitHub) CreatePullRequest(_ io.Writer, workingDir string, metadata PullRequest) (didCreate bool, err error) {
args := []string{"create_pull_request", workingDir, metadata.Title}
labelList := strings.Join(metadata.Labels, ",")
args := []string{"create_pull_request", workingDir, metadata.Title, labelList}
f.calls = append(f.calls, args)
return f.handler(CreatePullRequest, args)
}
Expand Down
7 changes: 7 additions & 0 deletions internal/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,14 @@ import (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funny, github doesn't show any syntax highlighting on this one?

var execInstance executor.Executor = executor.NewRealExecutor()

const TurboliftLabel = "turbolift"

type PullRequest struct {
Title string
Body string
UpstreamRepo string
IsDraft bool
Labels []string
ReviewDecision string
}

Expand Down Expand Up @@ -60,6 +63,10 @@ func (r *RealGitHub) CreatePullRequest(output io.Writer, workingDir string, pr P
pr.UpstreamRepo,
}

for _, label := range pr.Labels {
gh_args = append(gh_args, "--label", label)
}

if pr.IsDraft {
gh_args = append(gh_args, "--draft")
}
Expand Down
32 changes: 29 additions & 3 deletions internal/github/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func TestItReturnsErrorOnFailedCreatePr(t *testing.T) {
assert.False(t, didCreatePr)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1"},
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1", "--label", "turbolift"},
})
}

Expand All @@ -99,7 +99,7 @@ func TestItReturnsFalseAndNilErrorOnNoOpCreatePr(t *testing.T) {
assert.False(t, didCreatePr)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1"},
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1", "--label", "turbolift"},
})
}

Expand All @@ -112,7 +112,7 @@ func TestItSuccessfulCreatesADraftPr(t *testing.T) {
assert.True(t, didCreatePr)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1", "--draft"},
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1", "--label", "turbolift", "--draft"},
})
}

Expand All @@ -124,6 +124,19 @@ func TestItReturnsTrueAndNilErrorOnSuccessfulCreatePr(t *testing.T) {
assert.NoError(t, err)
assert.True(t, didCreatePr)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1", "--label", "turbolift"},
})
}

func TestItCreatesPrWithoutLabelsWhenNoneProvided(t *testing.T) {
fakeExecutor := executor.NewAlwaysSucceedsFakeExecutor()
execInstance = fakeExecutor

didCreatePr, _, err := runCreatePrWithoutLabelsAndCaptureOutput()
assert.NoError(t, err)
assert.True(t, didCreatePr)

fakeExecutor.AssertCalledWith(t, [][]string{
{"work/org/repo1", "gh", "pr", "create", "--title", "some title", "--body", "some body", "--repo", "org/repo1"},
})
Expand Down Expand Up @@ -197,6 +210,7 @@ func runCreatePrAndCaptureOutput() (bool, string, error) {
Title: "some title",
Body: "some body",
UpstreamRepo: "org/repo1",
Labels: []string{TurboliftLabel},
})

return didCreatePr, sb.String(), err
Expand All @@ -209,6 +223,18 @@ func runCreateDraftPrAndCaptureOutput() (bool, string, error) {
Body: "some body",
UpstreamRepo: "org/repo1",
IsDraft: true,
Labels: []string{TurboliftLabel},
})

return didCreatePr, sb.String(), err
}

func runCreatePrWithoutLabelsAndCaptureOutput() (bool, string, error) {
sb := strings.Builder{}
didCreatePr, err := NewRealGitHub().CreatePullRequest(&sb, "work/org/repo1", PullRequest{
Title: "some title",
Body: "some body",
UpstreamRepo: "org/repo1",
})

return didCreatePr, sb.String(), err
Expand Down
Loading