Skip to content

Commit 6444b5f

Browse files
authored
feat(internal/librarian): add logic to get pull requests (#1754)
Updates: #1009
1 parent 00b91cd commit 6444b5f

File tree

10 files changed

+325
-35
lines changed

10 files changed

+325
-35
lines changed

internal/config/config.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,12 @@ type Config struct {
103103
// the tool is executing.
104104
CI string
105105

106+
// CommandName is the name of the command being executed.
107+
//
108+
// commandName is populated automatically after flag parsing. No user setup is
109+
// expected.
110+
CommandName string
111+
106112
// GitHubToken is the access token to use for all operations involving
107113
// GitHub.
108114
//
@@ -214,18 +220,12 @@ type Config struct {
214220
//
215221
// WorkRoot is used by all librarian commands.
216222
WorkRoot string
217-
218-
// commandName is the name of the command being executed.
219-
//
220-
// commandName is populated automatically after flag parsing. No user setup is
221-
// expected.
222-
commandName string
223223
}
224224

225225
// New returns a new Config populated with environment variables.
226226
func New(cmdName string) *Config {
227227
return &Config{
228-
commandName: cmdName,
228+
CommandName: cmdName,
229229
GitHubToken: os.Getenv("LIBRARIAN_GITHUB_TOKEN"),
230230
}
231231
}
@@ -244,7 +244,7 @@ func (c *Config) setupUser() error {
244244
}
245245

246246
func (c *Config) createWorkRoot() error {
247-
if c.commandName == versionCmdName {
247+
if c.CommandName == versionCmdName {
248248
return nil
249249
}
250250
if c.WorkRoot != "" {
@@ -272,7 +272,7 @@ func (c *Config) createWorkRoot() error {
272272
}
273273

274274
func (c *Config) deriveRepo() error {
275-
if c.commandName == versionCmdName {
275+
if c.CommandName == versionCmdName {
276276
return nil
277277
}
278278
if c.Repo != "" {
@@ -313,7 +313,7 @@ func (c *Config) IsValid() (bool, error) {
313313
return false, err
314314
}
315315

316-
if c.commandName != versionCmdName && c.Repo == "" {
316+
if c.CommandName != versionCmdName && c.Repo == "" {
317317
return false, errors.New("language repository not specified or detected")
318318
}
319319

internal/config/config_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"time"
2626

2727
"github.com/google/go-cmp/cmp"
28-
"github.com/google/go-cmp/cmp/cmpopts"
2928
)
3029

3130
func TestNew(t *testing.T) {
@@ -42,13 +41,15 @@ func TestNew(t *testing.T) {
4241
},
4342
want: Config{
4443
GitHubToken: "gh_token",
44+
CommandName: "test",
4545
},
4646
},
4747
{
4848
name: "No environment variables set",
4949
envVars: map[string]string{},
5050
want: Config{
5151
GitHubToken: "",
52+
CommandName: "test",
5253
},
5354
},
5455
{
@@ -58,6 +59,7 @@ func TestNew(t *testing.T) {
5859
},
5960
want: Config{
6061
GitHubToken: "gh_token",
62+
CommandName: "test",
6163
},
6264
},
6365
} {
@@ -68,7 +70,7 @@ func TestNew(t *testing.T) {
6870

6971
got := New("test")
7072

71-
if diff := cmp.Diff(&test.want, got, cmpopts.IgnoreUnexported(Config{})); diff != "" {
73+
if diff := cmp.Diff(&test.want, got); diff != "" {
7274
t.Errorf("New() mismatch (-want +got):\n%s", diff)
7375
}
7476
})
@@ -280,7 +282,7 @@ func TestCreateWorkRoot(t *testing.T) {
280282
{
281283
name: "version command",
282284
config: &Config{
283-
commandName: "version",
285+
CommandName: "version",
284286
WorkRoot: "/some/path",
285287
},
286288
setup: func(t *testing.T) (string, func()) {
@@ -376,7 +378,7 @@ func TestDeriveRepo(t *testing.T) {
376378
name: "version command",
377379
config: &Config{
378380
Repo: "/some/path",
379-
commandName: "version",
381+
CommandName: "version",
380382
},
381383
wantRepoPath: "/some/path",
382384
},

internal/github/github.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ type RepositoryCommit = github.RepositoryCommit
3737
// PullRequestReview is a type alias for the go-github type.
3838
type PullRequestReview = github.PullRequestReview
3939

40+
// RepositoryRelease is a type alias for the go-github type.
41+
type RepositoryRelease = github.RepositoryRelease
42+
4043
// MergeMethodRebase is a constant alias for the go-github constant.
4144
const MergeMethodRebase = github.MergeMethodRebase
4245

internal/librarian/command.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,16 +46,21 @@ func newCommandRunner(cfg *config.Config) (*commandRunner, error) {
4646
if cfg.APISource == "" {
4747
cfg.APISource = "https://github.com/googleapis/googleapis"
4848
}
49-
sourceRepo, err := cloneOrOpenRepo(cfg.WorkRoot, cfg.APISource, cfg.CI)
49+
languageRepo, err := cloneOrOpenRepo(cfg.WorkRoot, cfg.Repo, cfg.CI)
5050
if err != nil {
5151
return nil, err
5252
}
5353

54-
languageRepo, err := cloneOrOpenRepo(cfg.WorkRoot, cfg.Repo, cfg.CI)
55-
if err != nil {
56-
return nil, err
54+
var sourceRepo gitrepo.Repository
55+
var sourceRepoDir string
56+
if cfg.CommandName != tagAndReleaseCmdName {
57+
sourceRepo, err = cloneOrOpenRepo(cfg.WorkRoot, cfg.APISource, cfg.CI)
58+
if err != nil {
59+
return nil, err
60+
}
61+
sourceRepoDir = sourceRepo.GetDir()
5762
}
58-
state, err := loadRepoState(languageRepo, sourceRepo.GetDir())
63+
state, err := loadRepoState(languageRepo, sourceRepoDir)
5964
if err != nil {
6065
return nil, err
6166
}

internal/librarian/command_test.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,8 @@ func TestCommandUsage(t *testing.T) {
4040
if parts[0] != "librarian" {
4141
t.Errorf("invalid usage text: %q (the first word should be `librarian`)", c.UsageLine)
4242
}
43-
// The second word should always be the command name.
44-
if parts[1] != c.Name() {
45-
t.Errorf("invalid usage text: %q (second word should be command name %q)", c.UsageLine, c.Name())
43+
if !strings.Contains(c.UsageLine, c.Name()) {
44+
t.Errorf("invalid usage text: %q (should contain command name %q)", c.UsageLine, c.Name())
4645
}
4746
})
4847
}

internal/librarian/init.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import (
2626

2727
// cmdInit is the command for the `release init` subcommand.
2828
var cmdInit = &cli.Command{
29-
Short: "release init initiates a release by creating a release pull request.",
29+
Short: "init initiates a release by creating a release pull request.",
3030
UsageLine: "librarian release init [arguments]",
3131
Long: `The release init command is the primary entry point for initiating a release.
3232
It orchestrates the process of parsing commits, determining new versions, generating

internal/librarian/librarian.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@ type GitHubClient interface {
8181
GetRawContent(ctx context.Context, path, ref string) ([]byte, error)
8282
CreatePullRequest(ctx context.Context, repo *github.Repository, remoteBranch, title, body string) (*github.PullRequestMetadata, error)
8383
AddLabelsToIssue(ctx context.Context, repo *github.Repository, number int, labels []string) error
84+
GetLabels(ctx context.Context, number int) ([]string, error)
85+
ReplaceLabels(ctx context.Context, number int, labels []string) error
86+
SearchPullRequests(ctx context.Context, query string) ([]*github.PullRequest, error)
87+
GetPullRequest(ctx context.Context, number int) (*github.PullRequest, error)
88+
CreateRelease(ctx context.Context, tagName, name, body, commitish string) (*github.RepositoryRelease, error)
89+
CreateIssueComment(ctx context.Context, number int, comment string) error
8490
}
8591

8692
// ContainerClient is an abstraction over the Docker client.

internal/librarian/mocks_test.go

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,24 @@ import (
3131
// mockGitHubClient is a mock implementation of the GitHubClient interface for testing.
3232
type mockGitHubClient struct {
3333
GitHubClient
34-
rawContent []byte
35-
rawErr error
36-
createPullRequestCalls int
37-
addLabelsToIssuesCalls int
38-
createPullRequestErr error
39-
addLabelsToIssuesErr error
40-
createdPR *github.PullRequestMetadata
34+
rawContent []byte
35+
rawErr error
36+
createPullRequestCalls int
37+
addLabelsToIssuesCalls int
38+
getLabelsCalls int
39+
replaceLabelsCalls int
40+
searchPullRequestsCalls int
41+
getPullRequestCalls int
42+
createPullRequestErr error
43+
addLabelsToIssuesErr error
44+
getLabelsErr error
45+
replaceLabelsErr error
46+
searchPullRequestsErr error
47+
getPullRequestErr error
48+
createdPR *github.PullRequestMetadata
49+
labels []string
50+
pullRequests []*github.PullRequest
51+
pullRequest *github.PullRequest
4152
}
4253

4354
func (m *mockGitHubClient) GetRawContent(ctx context.Context, path, ref string) ([]byte, error) {
@@ -57,6 +68,26 @@ func (m *mockGitHubClient) AddLabelsToIssue(ctx context.Context, repo *github.Re
5768
return m.addLabelsToIssuesErr
5869
}
5970

71+
func (m *mockGitHubClient) GetLabels(ctx context.Context, number int) ([]string, error) {
72+
m.getLabelsCalls++
73+
return m.labels, m.getLabelsErr
74+
}
75+
76+
func (m *mockGitHubClient) ReplaceLabels(ctx context.Context, number int, labels []string) error {
77+
m.replaceLabelsCalls++
78+
return m.replaceLabelsErr
79+
}
80+
81+
func (m *mockGitHubClient) SearchPullRequests(ctx context.Context, query string) ([]*github.PullRequest, error) {
82+
m.searchPullRequestsCalls++
83+
return m.pullRequests, m.searchPullRequestsErr
84+
}
85+
86+
func (m *mockGitHubClient) GetPullRequest(ctx context.Context, number int) (*github.PullRequest, error) {
87+
m.getPullRequestCalls++
88+
return m.pullRequest, m.getPullRequestErr
89+
}
90+
6091
// mockContainerClient is a mock implementation of the ContainerClient interface for testing.
6192
type mockContainerClient struct {
6293
ContainerClient

internal/librarian/tag_and_release.go

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,23 @@ package librarian
1616

1717
import (
1818
"context"
19+
"errors"
1920
"fmt"
2021
"log/slog"
2122
"regexp"
23+
"strconv"
2224
"strings"
25+
"time"
2326

2427
"github.com/googleapis/librarian/internal/cli"
2528
"github.com/googleapis/librarian/internal/config"
29+
"github.com/googleapis/librarian/internal/github"
30+
"github.com/googleapis/librarian/internal/gitrepo"
31+
)
32+
33+
const (
34+
pullRequestSegments = 5
35+
tagAndReleaseCmdName = "tag-and-release"
2636
)
2737

2838
var (
@@ -32,7 +42,7 @@ var (
3242

3343
// cmdTagAndRelease is the command for the `release tag-and-release` subcommand.
3444
var cmdTagAndRelease = &cli.Command{
35-
Short: "release tag-and-release tags and creates a GitHub release for a merged pull request.",
45+
Short: "tag-and-release tags and creates a GitHub release for a merged pull request.",
3646
UsageLine: "librarian release tag-and-release [arguments]",
3747
Long: "Tags and creates a GitHub release for a merged pull request.",
3848
Run: func(ctx context.Context, cfg *config.Config) error {
@@ -54,19 +64,92 @@ func init() {
5464
}
5565

5666
type tagAndReleaseRunner struct {
57-
cfg *config.Config
67+
cfg *config.Config
68+
ghClient GitHubClient
69+
repo gitrepo.Repository
70+
state *config.LibrarianState
5871
}
5972

6073
func newTagAndReleaseRunner(cfg *config.Config) (*tagAndReleaseRunner, error) {
74+
runner, err := newCommandRunner(cfg)
75+
if err != nil {
76+
return nil, err
77+
}
6178
if cfg.GitHubToken == "" {
6279
return nil, fmt.Errorf("`LIBRARIAN_GITHUB_TOKEN` must be set")
6380
}
6481
return &tagAndReleaseRunner{
65-
cfg: cfg,
82+
cfg: cfg,
83+
repo: runner.repo,
84+
state: runner.state,
85+
ghClient: runner.ghClient,
6686
}, nil
6787
}
6888

6989
func (r *tagAndReleaseRunner) run(ctx context.Context) error {
90+
slog.Info("running tag-and-release command")
91+
prs, err := r.determinePullRequestsToProcess(ctx)
92+
if err != nil {
93+
return err
94+
}
95+
if len(prs) == 0 {
96+
slog.Info("no pull requests to process, exiting")
97+
return nil
98+
}
99+
100+
var hadErrors bool
101+
for _, p := range prs {
102+
if err := r.processPullRequest(ctx, p); err != nil {
103+
slog.Error("failed to process pull request", "pr", p.GetNumber(), "error", err)
104+
hadErrors = true
105+
continue
106+
}
107+
slog.Info("processed pull request", "pr", p.GetNumber())
108+
}
109+
slog.Info("finished processing all pull requests")
110+
111+
if hadErrors {
112+
return errors.New("failed to process some pull requests")
113+
}
114+
return nil
115+
}
116+
117+
func (r *tagAndReleaseRunner) determinePullRequestsToProcess(ctx context.Context) ([]*github.PullRequest, error) {
118+
slog.Info("determining pull requests to process")
119+
if r.cfg.PullRequest != "" {
120+
slog.Info("processing a single pull request", "pr", r.cfg.PullRequest)
121+
ss := strings.Split(r.cfg.PullRequest, "/")
122+
if len(ss) != pullRequestSegments {
123+
return nil, fmt.Errorf("invalid pull request format: %s", r.cfg.PullRequest)
124+
}
125+
prNum, err := strconv.Atoi(ss[pullRequestSegments-1])
126+
if err != nil {
127+
return nil, fmt.Errorf("invalid pull request number: %s", ss[pullRequestSegments-1])
128+
}
129+
pr, err := r.ghClient.GetPullRequest(ctx, prNum)
130+
if err != nil {
131+
return nil, fmt.Errorf("failed to get pull request %d: %w", prNum, err)
132+
}
133+
return []*github.PullRequest{pr}, nil
134+
}
135+
136+
slog.Info("searching for pull requests to tag and release")
137+
thirtyDaysAgo := time.Now().Add(-30 * 24 * time.Hour).Format(time.RFC3339)
138+
query := fmt.Sprintf("label:release:pending merged:>=%s", thirtyDaysAgo)
139+
prs, err := r.ghClient.SearchPullRequests(ctx, query)
140+
if err != nil {
141+
return nil, fmt.Errorf("failed to search pull requests: %w", err)
142+
}
143+
return prs, nil
144+
}
145+
146+
func (r *tagAndReleaseRunner) processPullRequest(_ context.Context, p *github.PullRequest) error {
147+
slog.Info("processing pull request", "pr", p.GetNumber())
148+
// hack to make CI happy until we impl
149+
// TODO(https://github.com/googleapis/librarian/issues/1009)
150+
if p.GetNumber() != 0 {
151+
return fmt.Errorf("skipping pull request %d", p.GetNumber())
152+
}
70153
return nil
71154
}
72155

0 commit comments

Comments
 (0)