Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"os"
"os/user"
"regexp"
"strings"
)

Expand Down Expand Up @@ -49,6 +50,11 @@ const (
ReleaseInitRequest = "release-init-request.json"
)

var (
// pullRequestRegexp is regular expression that describes a uri of a pull request.
pullRequestRegexp = regexp.MustCompile(`^https://github\.com/([a-zA-Z0-9-._]+)/([a-zA-Z0-9-._]+)/pull/([0-9]+)$`)
)

// Config holds all configuration values parsed from flags or environment
// variables. When adding members to this struct, please keep them in
// alphabetical order.
Expand Down Expand Up @@ -130,6 +136,14 @@ type Config struct {
// Requires the --library flag to be specified.
LibraryVersion string

// PullRequest to target and operate one in the context of a release.
//
// The pull request should be in the format `https://github.com/{owner}/{repo}/pull/{number}`.
// Setting this field for `tag-and-release` means librarian will only attempt
// to process this exact pull request and not search for other pull requests
// that may be ready for tagging and releasing.
PullRequest string

// Push determines whether to push changes to GitHub. It is used in
// all commands that create commits in a language repository:
// configure and update-apis.
Expand Down Expand Up @@ -222,6 +236,13 @@ func (c *Config) IsValid() (bool, error) {
return false, errors.New("specified library version without library id")
}

if c.PullRequest != "" {
matched := pullRequestRegexp.MatchString(c.PullRequest)
if !matched {
return false, errors.New("pull request URL is not valid")
}
}

if _, err := validateHostMount(c.HostMount, ""); err != nil {
return false, err
}
Expand Down
16 changes: 15 additions & 1 deletion internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// WITHOUT WARRANTIES, OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

Expand Down Expand Up @@ -156,6 +156,12 @@ func TestIsValid(t *testing.T) {
Library: "library-id",
},
},
{
name: "Valid config - valid pull request",
cfg: Config{
PullRequest: "https://github.com/owner/repo/pull/123",
},
},
{
name: "Invalid config - Push true, token missing",
cfg: Config{
Expand Down Expand Up @@ -202,6 +208,14 @@ func TestIsValid(t *testing.T) {
wantErr: true,
wantErrMsg: "unable to parse host mount",
},
{
name: "Invalid config - invalid pull request url",
cfg: Config{
PullRequest: "https://github.com/owner/repo/issues/123",
},
wantErr: true,
wantErrMsg: "pull request URL is not valid",
},
} {
t.Run(test.name, func(t *testing.T) {
gotValid, err := test.cfg.IsValid()
Expand Down
4 changes: 4 additions & 0 deletions internal/librarian/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,7 @@ func addFlagRepo(fs *flag.FlagSet, cfg *config.Config) {
func addFlagWorkRoot(fs *flag.FlagSet, cfg *config.Config) {
fs.StringVar(&cfg.WorkRoot, "output", "", "Working directory root. When this is not specified, a working directory will be created in /tmp.")
}

func addFlagPR(fs *flag.FlagSet, cfg *config.Config) {
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.")
}
1 change: 1 addition & 0 deletions internal/librarian/librarian.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func init() {
CmdLibrarian.Init()
CmdLibrarian.Commands = append(CmdLibrarian.Commands,
cmdGenerate,
cmdRelease,
cmdVersion,
)
}
Expand Down
33 changes: 33 additions & 0 deletions internal/librarian/release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package librarian

import (
"github.com/googleapis/librarian/internal/cli"
)

// cmdRelease is the command for the `release` subcommand.
var cmdRelease = &cli.Command{
Short: "release manages releases of libraries.",
UsageLine: "librarian release <command> [arguments]",
Long: "Manages releases of libraries.",
}

func init() {
cmdRelease.Init()
CmdLibrarian.Commands = append(CmdLibrarian.Commands,
cmdTagAndRelease,
)
}
63 changes: 63 additions & 0 deletions internal/librarian/tag_and_release.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package librarian

import (
"context"
"fmt"

"github.com/googleapis/librarian/internal/cli"
"github.com/googleapis/librarian/internal/config"
)

// cmdTagAndRelease is the command for the `release tag-and-release` subcommand.
var cmdTagAndRelease = &cli.Command{
Short: "release tag-and-release tags and creates a GitHub release for a merged pull request.",
UsageLine: "librarian release tag-and-release [arguments]",
Long: "Tags and creates a GitHub release for a merged pull request.",
Run: func(ctx context.Context, cfg *config.Config) error {
runner, err := newTagAndReleaseRunner(cfg)
if err != nil {
return err
}
return runner.run(ctx)
},
}

func init() {
cmdTagAndRelease.Init()
fs := cmdTagAndRelease.Flags
cfg := cmdGenerate.Config

addFlagRepo(fs, cfg)
addFlagPR(fs, cfg)
}

type tagAndReleaseRunner struct {
cfg *config.Config
}

func newTagAndReleaseRunner(cfg *config.Config) (*tagAndReleaseRunner, error) {
if cfg.GitHubToken == "" {
return nil, fmt.Errorf("`LIBRARIAN_GITHUB_TOKEN` must be set")
}
return &tagAndReleaseRunner{
cfg: cfg,
}, nil
}

func (r *tagAndReleaseRunner) run(ctx context.Context) error {
return nil
}
54 changes: 54 additions & 0 deletions internal/librarian/tag_and_release_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package librarian

import (
"testing"

"github.com/googleapis/librarian/internal/config"
)

func TestNewTagAndReleaseRunner(t *testing.T) {
testcases := []struct {
name string
cfg *config.Config
wantErr bool
}{
{
name: "valid config",
cfg: &config.Config{
GitHubToken: "some-token",
},
wantErr: false,
},
{
name: "missing github token",
cfg: &config.Config{},
wantErr: true,
},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
r, err := newTagAndReleaseRunner(tc.cfg)
if (err != nil) != tc.wantErr {
t.Errorf("newTagAndReleaseRunner() error = %v, wantErr %v", err, tc.wantErr)
return
}
if !tc.wantErr && r == nil {
t.Errorf("newTagAndReleaseRunner() got nil runner, want non-nil")
}
})
}
}