Skip to content

Commit c4ea240

Browse files
authored
chore(tool/cmd/sycnewlibrary): add syncnewlibrary command (#4898)
The `syncnewlibrary` command is added to the tool directory. This command creates a new library configuration in the legacylibrarian `state.yaml` based on the library name and version found in `librarian.yaml`. The tool automates the process of syncing new library configuration to legacylibrarian `state.yaml` after a library is onboarded by librarian. Only release-related configurations are added since we only use legacylibrarian stage & tag. Fixes #4899
1 parent 0c4be5c commit c4ea240

File tree

6 files changed

+465
-0
lines changed

6 files changed

+465
-0
lines changed

tool/cmd/sycnewlibrary/main.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2026 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+
// The syncnewlibrary command creates a new library configuration to legacylibrarian state.yaml
16+
// based on the library name in librarian.yaml.
17+
package main
18+
19+
import (
20+
"errors"
21+
"flag"
22+
"log"
23+
"os"
24+
"path/filepath"
25+
"sort"
26+
27+
"github.com/googleapis/librarian/internal/config"
28+
"github.com/googleapis/librarian/internal/legacylibrarian/legacyconfig"
29+
"github.com/googleapis/librarian/internal/yaml"
30+
)
31+
32+
var (
33+
errRepoNotFound = errors.New("repo argument is required")
34+
)
35+
36+
func main() {
37+
if err := run(os.Args[1:]); err != nil {
38+
log.Fatal(err)
39+
}
40+
}
41+
42+
func run(args []string) error {
43+
flagSet := flag.NewFlagSet("syncnewlibrary", flag.ContinueOnError)
44+
if err := flagSet.Parse(args); err != nil {
45+
return err
46+
}
47+
if flagSet.NArg() < 1 {
48+
return errRepoNotFound
49+
}
50+
51+
repoPath := flagSet.Arg(0)
52+
abs, err := filepath.Abs(repoPath)
53+
if err != nil {
54+
return err
55+
}
56+
stateFile := filepath.Join(abs, legacyconfig.LibrarianDir, legacyconfig.LibrarianStateFile)
57+
state, err := yaml.Read[legacyconfig.LibrarianState](stateFile)
58+
if err != nil {
59+
return err
60+
}
61+
cfg, err := yaml.Read[config.Config](filepath.Join(abs, "librarian.yaml"))
62+
if err != nil {
63+
return err
64+
}
65+
state = syncNewLibrary(state, cfg)
66+
return yaml.Write(stateFile, state)
67+
}
68+
69+
func syncNewLibrary(state *legacyconfig.LibrarianState, cfg *config.Config) *legacyconfig.LibrarianState {
70+
for _, lib := range cfg.Libraries {
71+
legacyLib := state.LibraryByID(lib.Name)
72+
if legacyLib != nil {
73+
continue
74+
}
75+
state.Libraries = append(state.Libraries, createLegacyGoLibrary(lib.Name, lib.Version))
76+
}
77+
sort.Slice(state.Libraries, func(i, j int) bool {
78+
return state.Libraries[i].ID < state.Libraries[j].ID
79+
})
80+
return state
81+
}
82+
83+
func createLegacyGoLibrary(id, version string) *legacyconfig.LibraryState {
84+
return &legacyconfig.LibraryState{
85+
ID: id,
86+
Version: version,
87+
SourceRoots: []string{id},
88+
TagFormat: "{id}/v{version}",
89+
}
90+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Copyright 2026 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 main
16+
17+
import (
18+
"errors"
19+
"os"
20+
"path/filepath"
21+
"testing"
22+
23+
"github.com/google/go-cmp/cmp"
24+
"github.com/googleapis/librarian/internal/config"
25+
"github.com/googleapis/librarian/internal/legacylibrarian/legacyconfig"
26+
"github.com/googleapis/librarian/internal/yaml"
27+
)
28+
29+
func TestRun(t *testing.T) {
30+
for _, test := range []struct {
31+
name string
32+
repoPath string
33+
want *legacyconfig.LibrarianState
34+
}{
35+
{
36+
name: "success",
37+
repoPath: "testdata/sync-new-library",
38+
want: &legacyconfig.LibrarianState{
39+
Image: "test-image",
40+
Libraries: []*legacyconfig.LibraryState{
41+
{
42+
// Existing library in state.yaml
43+
ID: "accessapproval",
44+
Version: "1.9.0",
45+
APIs: []*legacyconfig.API{},
46+
SourceRoots: []string{"accessapproval", "internal/generated/snippets/accessapproval"},
47+
PreserveRegex: []string{},
48+
RemoveRegex: []string{},
49+
ReleaseExcludePaths: []string{"internal/generated/snippets/accessapproval/"},
50+
TagFormat: "{id}/v{version}",
51+
},
52+
{
53+
ID: "accesscontextmanager",
54+
Version: "1.9.7",
55+
APIs: []*legacyconfig.API{},
56+
SourceRoots: []string{"accesscontextmanager"},
57+
PreserveRegex: []string{},
58+
RemoveRegex: []string{},
59+
TagFormat: "{id}/v{version}",
60+
},
61+
{
62+
// Existing library in state.yaml
63+
ID: "advisorynotifications",
64+
Version: "1.5.6",
65+
APIs: []*legacyconfig.API{},
66+
SourceRoots: []string{"advisorynotifications", "internal/generated/snippets/advisorynotifications"},
67+
PreserveRegex: []string{},
68+
RemoveRegex: []string{},
69+
ReleaseExcludePaths: []string{"internal/generated/snippets/advisorynotifications/"},
70+
TagFormat: "{id}/v{version}",
71+
},
72+
{
73+
ID: "apigeeconnect",
74+
Version: "1.7.7",
75+
APIs: []*legacyconfig.API{},
76+
SourceRoots: []string{"apigeeconnect"},
77+
PreserveRegex: []string{},
78+
RemoveRegex: []string{},
79+
TagFormat: "{id}/v{version}",
80+
},
81+
},
82+
},
83+
},
84+
} {
85+
t.Run(test.name, func(t *testing.T) {
86+
statePath := filepath.Join(test.repoPath, ".librarian", "state.yaml")
87+
original, err := yaml.Read[legacyconfig.LibrarianState](statePath)
88+
if err != nil {
89+
t.Fatal(err)
90+
}
91+
t.Cleanup(func() {
92+
if err := yaml.Write(statePath, original); err != nil {
93+
t.Fatal(err)
94+
}
95+
})
96+
if err := run([]string{test.repoPath}); err != nil {
97+
t.Fatal(err)
98+
}
99+
got, err := yaml.Read[legacyconfig.LibrarianState](statePath)
100+
if err != nil {
101+
t.Fatal(err)
102+
}
103+
if diff := cmp.Diff(test.want, got); diff != "" {
104+
t.Errorf("mismatch (-want +got):\n%s", diff)
105+
}
106+
})
107+
}
108+
}
109+
110+
func TestRun_Error(t *testing.T) {
111+
for _, test := range []struct {
112+
name string
113+
repoPath string
114+
wantErr error
115+
}{
116+
{
117+
name: "no state.yaml",
118+
repoPath: "testdata/no-state",
119+
wantErr: os.ErrNotExist,
120+
},
121+
{
122+
name: "no librarian.yaml",
123+
repoPath: "testdata/no-librarian",
124+
wantErr: os.ErrNotExist,
125+
},
126+
} {
127+
t.Run(test.name, func(t *testing.T) {
128+
err := run([]string{test.repoPath})
129+
if !errors.Is(err, test.wantErr) {
130+
t.Errorf("got error %v, want %v", err, test.wantErr)
131+
}
132+
})
133+
}
134+
}
135+
136+
func TestSyncNewLibrary(t *testing.T) {
137+
for _, test := range []struct {
138+
name string
139+
state *legacyconfig.LibrarianState
140+
cfg *config.Config
141+
want *legacyconfig.LibrarianState
142+
}{
143+
{
144+
name: "sync new library",
145+
state: &legacyconfig.LibrarianState{
146+
Image: "test-image",
147+
Libraries: []*legacyconfig.LibraryState{
148+
{
149+
ID: "existing",
150+
Version: "1.0.0",
151+
SourceRoots: []string{"existing"},
152+
TagFormat: "{id}/v{version}",
153+
},
154+
},
155+
},
156+
cfg: &config.Config{
157+
Libraries: []*config.Library{
158+
{Name: "aiplatform", Version: "1.0.0"},
159+
{Name: "secretmanager", Version: "1.2.0"},
160+
},
161+
},
162+
want: &legacyconfig.LibrarianState{
163+
Image: "test-image",
164+
Libraries: []*legacyconfig.LibraryState{
165+
{
166+
ID: "aiplatform",
167+
Version: "1.0.0",
168+
SourceRoots: []string{"aiplatform"},
169+
TagFormat: "{id}/v{version}",
170+
},
171+
{
172+
ID: "existing",
173+
Version: "1.0.0",
174+
SourceRoots: []string{"existing"},
175+
TagFormat: "{id}/v{version}",
176+
},
177+
{
178+
ID: "secretmanager",
179+
Version: "1.2.0",
180+
SourceRoots: []string{"secretmanager"},
181+
TagFormat: "{id}/v{version}",
182+
},
183+
},
184+
},
185+
},
186+
{
187+
name: "no new library",
188+
state: &legacyconfig.LibrarianState{
189+
Image: "test-image",
190+
Libraries: []*legacyconfig.LibraryState{
191+
{
192+
ID: "existing",
193+
Version: "1.0.0",
194+
SourceRoots: []string{"existing"},
195+
TagFormat: "{id}/v{version}",
196+
},
197+
},
198+
},
199+
cfg: &config.Config{
200+
Libraries: []*config.Library{
201+
{Name: "existing", Version: "1.0.0"},
202+
},
203+
},
204+
want: &legacyconfig.LibrarianState{
205+
Image: "test-image",
206+
Libraries: []*legacyconfig.LibraryState{
207+
{
208+
ID: "existing",
209+
Version: "1.0.0",
210+
SourceRoots: []string{"existing"},
211+
TagFormat: "{id}/v{version}",
212+
},
213+
},
214+
},
215+
},
216+
} {
217+
t.Run(test.name, func(t *testing.T) {
218+
got := syncNewLibrary(test.state, test.cfg)
219+
if diff := cmp.Diff(test.want, got); diff != "" {
220+
t.Errorf("mismatch (-want +got):\n%s", diff)
221+
}
222+
})
223+
}
224+
}

tool/cmd/sycnewlibrary/testdata/no-librarian/.librarian/config.yaml

Whitespace-only changes.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
image: us-central1-docker.pkg.dev/cloud-sdk-librarian-prod/images-prod/librarian-go@sha256:19bb93e8f1f916c61b597db2bad65dc432f79baaabb210499d7d0e4ad1dffe29
2+
libraries:
3+
- id: accessapproval
4+
version: 1.9.0
5+
last_generated_commit: 6c3dce4a02401667e9dd6d28304fee3f98e20ff8
6+
apis:
7+
- path: google/cloud/accessapproval/v1
8+
service_config: accessapproval_v1.yaml
9+
source_roots:
10+
- accessapproval
11+
- internal/generated/snippets/accessapproval
12+
preserve_regex: []
13+
remove_regex:
14+
- ^internal/generated/snippets/accessapproval/
15+
- ^accessapproval/apiv1/[^/]*_client\.go$
16+
- ^accessapproval/apiv1/[^/]*_client_example_go123_test\.go$
17+
- ^accessapproval/apiv1/[^/]*_client_example_test\.go$
18+
- ^accessapproval/apiv1/auxiliary\.go$
19+
- ^accessapproval/apiv1/auxiliary_go123\.go$
20+
- ^accessapproval/apiv1/doc\.go$
21+
- ^accessapproval/apiv1/gapic_metadata\.json$
22+
- ^accessapproval/apiv1/helpers\.go$
23+
- ^accessapproval/apiv1/accessapprovalpb/.*$
24+
- ^accessapproval/apiv1/\.repo-metadata\.json$
25+
release_exclude_paths:
26+
- internal/generated/snippets/accessapproval/
27+
tag_format: '{id}/v{version}'
28+
- id: accesscontextmanager
29+
version: 1.10.0
30+
last_generated_commit: 6c3dce4a02401667e9dd6d28304fee3f98e20ff8
31+
apis:
32+
- path: google/identity/accesscontextmanager/v1
33+
service_config: accesscontextmanager_v1.yaml
34+
source_roots:
35+
- accesscontextmanager
36+
- internal/generated/snippets/accesscontextmanager
37+
preserve_regex: []
38+
remove_regex:
39+
- ^internal/generated/snippets/accesscontextmanager/
40+
- ^accesscontextmanager/apiv1/[^/]*_client\.go$
41+
- ^accesscontextmanager/apiv1/[^/]*_client_example_go123_test\.go$
42+
- ^accesscontextmanager/apiv1/[^/]*_client_example_test\.go$
43+
- ^accesscontextmanager/apiv1/auxiliary\.go$
44+
- ^accesscontextmanager/apiv1/auxiliary_go123\.go$
45+
- ^accesscontextmanager/apiv1/doc\.go$
46+
- ^accesscontextmanager/apiv1/gapic_metadata\.json$
47+
- ^accesscontextmanager/apiv1/helpers\.go$
48+
- ^accesscontextmanager/apiv1/accesscontextmanagerpb/.*$
49+
- ^accesscontextmanager/apiv1/\.repo-metadata\.json$
50+
release_exclude_paths:
51+
- internal/generated/snippets/accesscontextmanager/
52+
tag_format: '{id}/v{version}'
53+
- id: advisorynotifications
54+
version: 1.5.6
55+
last_generated_commit: 6c3dce4a02401667e9dd6d28304fee3f98e20ff8
56+
apis:
57+
- path: google/cloud/advisorynotifications/v1
58+
service_config: advisorynotifications_v1.yaml
59+
source_roots:
60+
- advisorynotifications
61+
- internal/generated/snippets/advisorynotifications
62+
preserve_regex: []
63+
remove_regex:
64+
- ^internal/generated/snippets/advisorynotifications/
65+
- ^advisorynotifications/apiv1/[^/]*_client\.go$
66+
- ^advisorynotifications/apiv1/[^/]*_client_example_go123_test\.go$
67+
- ^advisorynotifications/apiv1/[^/]*_client_example_test\.go$
68+
- ^advisorynotifications/apiv1/auxiliary\.go$
69+
- ^advisorynotifications/apiv1/auxiliary_go123\.go$
70+
- ^advisorynotifications/apiv1/doc\.go$
71+
- ^advisorynotifications/apiv1/gapic_metadata\.json$
72+
- ^advisorynotifications/apiv1/helpers\.go$
73+
- ^advisorynotifications/apiv1/advisorynotificationspb/.*$
74+
- ^advisorynotifications/apiv1/\.repo-metadata\.json$
75+
release_exclude_paths:
76+
- internal/generated/snippets/advisorynotifications/
77+
tag_format: '{id}/v{version}'

0 commit comments

Comments
 (0)