Skip to content

Commit 4603c4f

Browse files
authored
feat(internal/librarian): add release command (#3024)
The librarian release command is added, which supports releasing a single library or all libraries in a workspace. librarian release <library>: bump version for one library librarian release --all: bump versions for all libraries A testhelper language implementation is added to support testing the release command without depending on external tools like cargo. The testhelper sets all versions to "1.2.3" for predictable test assertions. The --execute flag is added but not yet implemented. By default, the release command operates in dry-run mode, and modifies the release artifacts to show planned changes, without pushing any git tags. For #2966
1 parent e1a2178 commit 4603c4f

File tree

8 files changed

+386
-55
lines changed

8 files changed

+386
-55
lines changed

internal/language/internal/rust/release.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
package rust
1717

1818
import (
19-
"context"
2019
"fmt"
2120
"io/fs"
2221
"os"
@@ -36,14 +35,32 @@ type cargoManifest struct {
3635
Package *cargoPackage `toml:"package"`
3736
}
3837

39-
// BumpVersions bumps versions for all Cargo.toml files and updates
40-
// librarian.yaml. If name is non-empty, only bumps the version for that
41-
// library.
42-
func BumpVersions(ctx context.Context, cfg *config.Config, name string) (*config.Config, error) {
38+
// ReleaseAll bumps versions for all Cargo.toml files and updates librarian.yaml.
39+
func ReleaseAll(cfg *config.Config) (*config.Config, error) {
40+
return release(cfg, "")
41+
}
42+
43+
// ReleaseLibrary bumps the version for a specific library and updates librarian.yaml.
44+
func ReleaseLibrary(cfg *config.Config, name string) (*config.Config, error) {
45+
return release(cfg, name)
46+
}
47+
48+
func release(cfg *config.Config, name string) (*config.Config, error) {
4349
if cfg.Versions == nil {
4450
cfg.Versions = make(map[string]string)
4551
}
4652

53+
shouldRelease := func(pkgName string) bool {
54+
// If name is the empty string, release everything.
55+
if name == "" {
56+
return true
57+
}
58+
if name == pkgName {
59+
return true
60+
}
61+
return false
62+
}
63+
4764
var found bool
4865
err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
4966
if err != nil {
@@ -64,7 +81,7 @@ func BumpVersions(ctx context.Context, cfg *config.Config, name string) (*config
6481
if manifest.Package == nil {
6582
return nil
6683
}
67-
if name != "" && manifest.Package.Name != name {
84+
if !shouldRelease(manifest.Package.Name) {
6885
return nil
6986
}
7087

@@ -82,7 +99,6 @@ func BumpVersions(ctx context.Context, cfg *config.Config, name string) (*config
8299
if err != nil {
83100
return nil, err
84101
}
85-
86102
if name != "" && !found {
87103
return nil, fmt.Errorf("library %q not found", name)
88104
}

internal/language/internal/rust/release_test.go

Lines changed: 57 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,53 +18,78 @@ import (
1818
"fmt"
1919
"os"
2020
"path/filepath"
21+
"strings"
2122
"testing"
2223

2324
"github.com/google/go-cmp/cmp"
2425
cmdtest "github.com/googleapis/librarian/internal/command"
2526
"github.com/googleapis/librarian/internal/config"
2627
)
2728

28-
func TestBumpVersions(t *testing.T) {
29-
cmdtest.RequireCommand(t, "cargo")
30-
cmdtest.RequireCommand(t, "taplo")
31-
tmpDir := t.TempDir()
32-
t.Chdir(tmpDir)
29+
const (
30+
storageDir = "src/storage"
31+
storageCargo = "src/storage/Cargo.toml"
32+
storageName = "google-cloud-storage"
33+
storageInitial = "1.0.0"
34+
storageReleased = "1.1.0"
35+
36+
secretmanagerDir = "src/secretmanager"
37+
secretmanagerCargo = "src/secretmanager/Cargo.toml"
38+
secretmanagerName = "google-cloud-secretmanager-v1"
39+
secretmanagerInitial = "1.5.3"
40+
secretmanagerReleased = "1.6.0"
41+
)
3342

34-
createCrate(t, "src/storage", "google-cloud-storage", "1.0.0")
35-
createCrate(t, "src/secretmanager", "google-cloud-secretmanager-v1", "1.5.3")
43+
func TestReleaseAll(t *testing.T) {
44+
cfg := setupRelease(t)
45+
got, err := ReleaseAll(cfg)
46+
if err != nil {
47+
t.Fatal(err)
48+
}
3649

37-
cfg := &config.Config{
38-
Version: "v1",
39-
Language: "rust",
40-
Versions: map[string]string{
41-
"google-cloud-storage": "1.0.0",
42-
"google-cloud-secretmanager-v1": "1.5.3",
43-
},
50+
checkCargoVersion(t, storageCargo, storageReleased)
51+
checkCargoVersion(t, secretmanagerCargo, secretmanagerReleased)
52+
want := map[string]string{
53+
storageName: storageReleased,
54+
secretmanagerName: secretmanagerReleased,
4455
}
45-
configPath := "librarian.yaml"
46-
if err := cfg.Write(configPath); err != nil {
47-
t.Fatal(err)
56+
if diff := cmp.Diff(want, got.Versions); diff != "" {
57+
t.Errorf("mismatch (-want +got):\n%s", diff)
4858
}
59+
}
4960

50-
updatedCfg, err := BumpVersions(t.Context(), cfg, "")
61+
func TestReleaseOne(t *testing.T) {
62+
cfg := setupRelease(t)
63+
got, err := ReleaseLibrary(cfg, storageName)
5164
if err != nil {
5265
t.Fatal(err)
5366
}
5467

55-
if err := updatedCfg.Write(configPath); err != nil {
56-
t.Fatal(err)
68+
checkCargoVersion(t, storageCargo, storageReleased)
69+
checkCargoVersion(t, secretmanagerCargo, secretmanagerInitial)
70+
want := map[string]string{
71+
storageName: storageReleased,
72+
secretmanagerName: secretmanagerInitial,
73+
}
74+
if diff := cmp.Diff(want, got.Versions); diff != "" {
75+
t.Errorf("mismatch (-want +got):\n%s", diff)
5776
}
77+
}
5878

59-
checkCargoVersion(t, "src/storage/Cargo.toml", "1.1.0")
60-
checkCargoVersion(t, "src/secretmanager/Cargo.toml", "1.6.0")
79+
func setupRelease(t *testing.T) *config.Config {
80+
t.Helper()
81+
cmdtest.RequireCommand(t, "cargo")
82+
cmdtest.RequireCommand(t, "taplo")
83+
tmpDir := t.TempDir()
84+
t.Chdir(tmpDir)
6185

62-
wantVersions := map[string]string{
63-
"google-cloud-storage": "1.1.0",
64-
"google-cloud-secretmanager-v1": "1.6.0",
65-
}
66-
if diff := cmp.Diff(wantVersions, updatedCfg.Versions); diff != "" {
67-
t.Errorf("versions mismatch (-want +got):\n%s", diff)
86+
createCrate(t, storageDir, storageName, storageInitial)
87+
createCrate(t, secretmanagerDir, secretmanagerName, secretmanagerInitial)
88+
return &config.Config{
89+
Versions: map[string]string{
90+
storageName: storageInitial,
91+
secretmanagerName: secretmanagerInitial,
92+
},
6893
}
6994
}
7095

@@ -91,22 +116,9 @@ func checkCargoVersion(t *testing.T, path, wantVersion string) {
91116
if err != nil {
92117
t.Fatal(err)
93118
}
94-
95-
want := fmt.Sprintf(`version = "%s"`, wantVersion)
96-
if !contains(string(contents), want) {
97-
t.Errorf("%s does not contain %q\nGot:\n%s", path, want, string(contents))
98-
}
99-
}
100-
101-
func contains(s, substr string) bool {
102-
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && containsInner(s, substr))
103-
}
104-
105-
func containsInner(s, substr string) bool {
106-
for i := 0; i <= len(s)-len(substr); i++ {
107-
if s[i:i+len(substr)] == substr {
108-
return true
109-
}
119+
wantLine := fmt.Sprintf(`version = "%s"`, wantVersion)
120+
got := string(contents)
121+
if !strings.Contains(got, wantLine) {
122+
t.Errorf("%s version mismatch:\nwant line: %q\ngot:\n%s", path, wantLine, got)
110123
}
111-
return false
112124
}

internal/language/release.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
// http://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 language
16+
17+
import (
18+
"fmt"
19+
20+
"github.com/googleapis/librarian/internal/config"
21+
"github.com/googleapis/librarian/internal/language/internal/rust"
22+
)
23+
24+
// ReleaseAll bumps versions for all libraries and updates librarian.yaml and
25+
// other release artifacts for the language.
26+
func ReleaseAll(cfg *config.Config) (*config.Config, error) {
27+
switch cfg.Language {
28+
case "testhelper":
29+
return testReleaseAll(cfg)
30+
case "rust":
31+
return rust.ReleaseAll(cfg)
32+
default:
33+
return nil, fmt.Errorf("language not supported for release --all: %q", cfg.Language)
34+
}
35+
}
36+
37+
// ReleaseLibrary bumps versions for one library and updates librarian.yaml and
38+
// other release artifacts for the language.
39+
func ReleaseLibrary(cfg *config.Config, name string) (*config.Config, error) {
40+
switch cfg.Language {
41+
case "testhelper":
42+
return testReleaseLibrary(cfg, name)
43+
case "rust":
44+
return rust.ReleaseLibrary(cfg, name)
45+
default:
46+
return nil, fmt.Errorf("language not supported for release --all: %q", cfg.Language)
47+
}
48+
}

internal/language/release_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
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+
// http://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 language
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
"github.com/googleapis/librarian/internal/config"
22+
)
23+
24+
func TestReleaseAll(t *testing.T) {
25+
cfg := &config.Config{
26+
Language: "testhelper",
27+
Versions: map[string]string{
28+
"lib1": "0.1.0",
29+
"lib2": "0.2.0",
30+
},
31+
}
32+
cfg, err := ReleaseAll(cfg)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
want := map[string]string{
37+
"lib1": TestReleaseVersion,
38+
"lib2": TestReleaseVersion,
39+
}
40+
if diff := cmp.Diff(want, cfg.Versions); diff != "" {
41+
t.Errorf("mismatch (-want +got):\n%s", diff)
42+
}
43+
}
44+
45+
func TestReleaseLibrary(t *testing.T) {
46+
cfg := &config.Config{
47+
Language: "testhelper",
48+
Versions: map[string]string{
49+
"lib1": "0.1.0",
50+
"lib2": "0.2.0",
51+
},
52+
}
53+
cfg, err := ReleaseLibrary(cfg, "lib1")
54+
if err != nil {
55+
t.Fatal(err)
56+
}
57+
want := map[string]string{
58+
"lib1": TestReleaseVersion,
59+
"lib2": "0.2.0",
60+
}
61+
if diff := cmp.Diff(want, cfg.Versions); diff != "" {
62+
t.Errorf("mismatch (-want +got):\n%s", diff)
63+
}
64+
}

internal/language/testhelper.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// Package language provides language implementations for testing the librarian
16-
// CLI logic, without calling any language-specific implementation or tooling.
1715
package language
1816

1917
import (
@@ -24,11 +22,32 @@ import (
2422
"github.com/googleapis/librarian/internal/config"
2523
)
2624

25+
// TestReleaseVersion is the version that libraries are always released at
26+
// when using the testhelper language implementation.
27+
const TestReleaseVersion = "1.2.3"
28+
29+
func testReleaseAll(cfg *config.Config) (*config.Config, error) {
30+
if cfg.Versions == nil {
31+
cfg.Versions = make(map[string]string)
32+
}
33+
for k := range cfg.Versions {
34+
cfg.Versions[k] = TestReleaseVersion
35+
}
36+
return cfg, nil
37+
}
38+
39+
func testReleaseLibrary(cfg *config.Config, name string) (*config.Config, error) {
40+
if cfg.Versions == nil {
41+
cfg.Versions = make(map[string]string)
42+
}
43+
cfg.Versions[name] = TestReleaseVersion
44+
return cfg, nil
45+
}
46+
2747
func testGenerate(library *config.Library) error {
2848
if err := os.MkdirAll(library.Output, 0755); err != nil {
2949
return err
3050
}
31-
3251
content := fmt.Sprintf("# %s\n\nGenerated library\n", library.Name)
3352
readmePath := filepath.Join(library.Output, "README.md")
3453
return os.WriteFile(readmePath, []byte(content), 0644)

internal/librarian/librarian.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func Run(ctx context.Context, args ...string) error {
3535
Commands: []*cli.Command{
3636
versionCommand(),
3737
generateCommand(),
38+
releaseCommand(),
3839
},
3940
}
4041
return cmd.Run(ctx, args)

0 commit comments

Comments
 (0)