Skip to content

Commit 004401f

Browse files
authored
feat(internal/librarian): add skeleton for release tag-and-release (#1701)
Also added the one new flag we will need for this command. Updates: #1009
1 parent c8a858e commit 004401f

File tree

7 files changed

+191
-1
lines changed

7 files changed

+191
-1
lines changed

internal/config/config.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"os"
2222
"os/user"
23+
"regexp"
2324
"strings"
2425
)
2526

@@ -49,6 +50,11 @@ const (
4950
ReleaseInitRequest = "release-init-request.json"
5051
)
5152

53+
var (
54+
// pullRequestRegexp is regular expression that describes a uri of a pull request.
55+
pullRequestRegexp = regexp.MustCompile(`^https://github\.com/([a-zA-Z0-9-._]+)/([a-zA-Z0-9-._]+)/pull/([0-9]+)$`)
56+
)
57+
5258
// Config holds all configuration values parsed from flags or environment
5359
// variables. When adding members to this struct, please keep them in
5460
// alphabetical order.
@@ -130,6 +136,14 @@ type Config struct {
130136
// Requires the --library flag to be specified.
131137
LibraryVersion string
132138

139+
// PullRequest to target and operate one in the context of a release.
140+
//
141+
// The pull request should be in the format `https://github.com/{owner}/{repo}/pull/{number}`.
142+
// Setting this field for `tag-and-release` means librarian will only attempt
143+
// to process this exact pull request and not search for other pull requests
144+
// that may be ready for tagging and releasing.
145+
PullRequest string
146+
133147
// Push determines whether to push changes to GitHub. It is used in
134148
// all commands that create commits in a language repository:
135149
// configure and update-apis.
@@ -222,6 +236,13 @@ func (c *Config) IsValid() (bool, error) {
222236
return false, errors.New("specified library version without library id")
223237
}
224238

239+
if c.PullRequest != "" {
240+
matched := pullRequestRegexp.MatchString(c.PullRequest)
241+
if !matched {
242+
return false, errors.New("pull request URL is not valid")
243+
}
244+
}
245+
225246
if _, err := validateHostMount(c.HostMount, ""); err != nil {
226247
return false, err
227248
}

internal/config/config_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
//
99
// Unless required by applicable law or agreed to in writing, software
1010
// distributed under the License is distributed on an "AS IS" BASIS,
11-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// WITHOUT WARRANTIES, OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

@@ -156,6 +156,12 @@ func TestIsValid(t *testing.T) {
156156
Library: "library-id",
157157
},
158158
},
159+
{
160+
name: "Valid config - valid pull request",
161+
cfg: Config{
162+
PullRequest: "https://github.com/owner/repo/pull/123",
163+
},
164+
},
159165
{
160166
name: "Invalid config - Push true, token missing",
161167
cfg: Config{
@@ -202,6 +208,14 @@ func TestIsValid(t *testing.T) {
202208
wantErr: true,
203209
wantErrMsg: "unable to parse host mount",
204210
},
211+
{
212+
name: "Invalid config - invalid pull request url",
213+
cfg: Config{
214+
PullRequest: "https://github.com/owner/repo/issues/123",
215+
},
216+
wantErr: true,
217+
wantErrMsg: "pull request URL is not valid",
218+
},
205219
} {
206220
t.Run(test.name, func(t *testing.T) {
207221
gotValid, err := test.cfg.IsValid()

internal/librarian/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,7 @@ func addFlagRepo(fs *flag.FlagSet, cfg *config.Config) {
5858
func addFlagWorkRoot(fs *flag.FlagSet, cfg *config.Config) {
5959
fs.StringVar(&cfg.WorkRoot, "output", "", "Working directory root. When this is not specified, a working directory will be created in /tmp.")
6060
}
61+
62+
func addFlagPR(fs *flag.FlagSet, cfg *config.Config) {
63+
fs.StringVar(&cfg.PullRequest, "pr", "", "a pull request to operate on. It should be in the format of a uri https://github.com/{owner}/{repo}/pull/{number}. If not specified, will search for all merged pull requests with the label `release:pending` in the last 30 days.")
64+
}

internal/librarian/librarian.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func init() {
4040
CmdLibrarian.Init()
4141
CmdLibrarian.Commands = append(CmdLibrarian.Commands,
4242
cmdGenerate,
43+
cmdRelease,
4344
cmdVersion,
4445
)
4546
}

internal/librarian/release.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librarian
16+
17+
import (
18+
"github.com/googleapis/librarian/internal/cli"
19+
)
20+
21+
// cmdRelease is the command for the `release` subcommand.
22+
var cmdRelease = &cli.Command{
23+
Short: "release manages releases of libraries.",
24+
UsageLine: "librarian release <command> [arguments]",
25+
Long: "Manages releases of libraries.",
26+
}
27+
28+
func init() {
29+
cmdRelease.Init()
30+
CmdLibrarian.Commands = append(CmdLibrarian.Commands,
31+
cmdTagAndRelease,
32+
)
33+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librarian
16+
17+
import (
18+
"context"
19+
"fmt"
20+
21+
"github.com/googleapis/librarian/internal/cli"
22+
"github.com/googleapis/librarian/internal/config"
23+
)
24+
25+
// cmdTagAndRelease is the command for the `release tag-and-release` subcommand.
26+
var cmdTagAndRelease = &cli.Command{
27+
Short: "release tag-and-release tags and creates a GitHub release for a merged pull request.",
28+
UsageLine: "librarian release tag-and-release [arguments]",
29+
Long: "Tags and creates a GitHub release for a merged pull request.",
30+
Run: func(ctx context.Context, cfg *config.Config) error {
31+
runner, err := newTagAndReleaseRunner(cfg)
32+
if err != nil {
33+
return err
34+
}
35+
return runner.run(ctx)
36+
},
37+
}
38+
39+
func init() {
40+
cmdTagAndRelease.Init()
41+
fs := cmdTagAndRelease.Flags
42+
cfg := cmdGenerate.Config
43+
44+
addFlagRepo(fs, cfg)
45+
addFlagPR(fs, cfg)
46+
}
47+
48+
type tagAndReleaseRunner struct {
49+
cfg *config.Config
50+
}
51+
52+
func newTagAndReleaseRunner(cfg *config.Config) (*tagAndReleaseRunner, error) {
53+
if cfg.GitHubToken == "" {
54+
return nil, fmt.Errorf("`LIBRARIAN_GITHUB_TOKEN` must be set")
55+
}
56+
return &tagAndReleaseRunner{
57+
cfg: cfg,
58+
}, nil
59+
}
60+
61+
func (r *tagAndReleaseRunner) run(ctx context.Context) error {
62+
return nil
63+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package librarian
16+
17+
import (
18+
"testing"
19+
20+
"github.com/googleapis/librarian/internal/config"
21+
)
22+
23+
func TestNewTagAndReleaseRunner(t *testing.T) {
24+
testcases := []struct {
25+
name string
26+
cfg *config.Config
27+
wantErr bool
28+
}{
29+
{
30+
name: "valid config",
31+
cfg: &config.Config{
32+
GitHubToken: "some-token",
33+
},
34+
wantErr: false,
35+
},
36+
{
37+
name: "missing github token",
38+
cfg: &config.Config{},
39+
wantErr: true,
40+
},
41+
}
42+
for _, tc := range testcases {
43+
t.Run(tc.name, func(t *testing.T) {
44+
r, err := newTagAndReleaseRunner(tc.cfg)
45+
if (err != nil) != tc.wantErr {
46+
t.Errorf("newTagAndReleaseRunner() error = %v, wantErr %v", err, tc.wantErr)
47+
return
48+
}
49+
if !tc.wantErr && r == nil {
50+
t.Errorf("newTagAndReleaseRunner() got nil runner, want non-nil")
51+
}
52+
})
53+
}
54+
}

0 commit comments

Comments
 (0)