Skip to content

Commit 5fce72a

Browse files
chore: refactor to remove service config from librarian yaml if it can find it in googleapis (#3370)
Update logic in RunTidy, to only remove the service config entry from librarian.yaml if it can find it in googleapis repo. (Safer implementation). This includes a refactor to make FetchSource available outside of generate command. [Generated librarian yaml file](https://paste.googleplex.com/5634686921605120) Fixes #3358 --------- Signed-off-by: ldetmer <[email protected]> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 781b724 commit 5fce72a

File tree

8 files changed

+130
-74
lines changed

8 files changed

+130
-74
lines changed

devtools/cmd/migrate-librarian/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ func run(ctx context.Context, args []string) error {
135135
return fmt.Errorf("failed to write config: %w", err)
136136
}
137137

138-
if err := librarian.RunTidy(); err != nil {
138+
if err := librarian.RunTidy(ctx); err != nil {
139139
return errTidyFailed
140140
}
141141

devtools/cmd/migrate-sidekick/main.go

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

1818
import (
19+
"context"
1920
"errors"
2021
"flag"
2122
"fmt"
@@ -75,13 +76,13 @@ func readCargoConfig(dir string) (*rustrelease.Cargo, error) {
7576
}
7677

7778
func main() {
78-
if err := run(os.Args[1:]); err != nil {
79+
if err := run(context.Background(), os.Args[1:]); err != nil {
7980
slog.Error("migrate-sidekick failed", "error", err)
8081
os.Exit(1)
8182
}
8283
}
8384

84-
func run(args []string) error {
85+
func run(ctx context.Context, args []string) error {
8586
flagSet := flag.NewFlagSet("migrate-sidekick", flag.ContinueOnError)
8687
outputPath := flagSet.String("output", "./librarian.yaml", "Output file path (default: ./librarian.yaml)")
8788
if err := flagSet.Parse(args); err != nil {
@@ -136,7 +137,7 @@ func run(args []string) error {
136137
}
137138
slog.Info("Wrote config to output file", "path", *outputPath)
138139

139-
if err := librarian.RunTidy(); err != nil {
140+
if err := librarian.RunTidy(ctx); err != nil {
140141
slog.Error(errTidyFailed.Error(), "error", err)
141142
return errTidyFailed
142143
}

devtools/cmd/migrate-sidekick/main_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -840,7 +840,7 @@ func TestRunMigrateCommand(t *testing.T) {
840840
wantReleaseBranch := "main"
841841
wantReleaseRemote := "upstream"
842842

843-
if err := run([]string{test.path}); err != nil {
843+
if err := run(t.Context(), []string{test.path}); err != nil {
844844
if test.wantErr == nil {
845845
t.Fatal(err)
846846
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[source]
2+
googleapis-root = 'https://github.com/googleapis/googleapis/archive/fe58211356a91f4140ed51893703910db05ade91.tar.gz'
3+
googleapis-sha256 = 'c809264b7973949c157aaaacfbab015263fd8cea3a262a9297a676f4b92dd0bd'

internal/librarian/generate.go

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"strings"
2525

2626
"github.com/googleapis/librarian/internal/config"
27-
"github.com/googleapis/librarian/internal/fetch"
2827
"github.com/googleapis/librarian/internal/librarian/internal/python"
2928
"github.com/googleapis/librarian/internal/librarian/internal/rust"
3029
"github.com/googleapis/librarian/internal/serviceconfig"
@@ -326,21 +325,6 @@ func formatLibrary(ctx context.Context, language string, library *config.Library
326325
return fmt.Errorf("format not implemented for %q", language)
327326
}
328327

329-
func fetchSource(ctx context.Context, source *config.Source, repo string) (string, error) {
330-
if source == nil {
331-
return "", nil
332-
}
333-
if source.Dir != "" {
334-
return source.Dir, nil
335-
}
336-
337-
dir, err := fetch.RepoDir(ctx, repo, source.Commit, source.SHA256)
338-
if err != nil {
339-
return "", fmt.Errorf("failed to fetch %s: %w", repo, err)
340-
}
341-
return dir, nil
342-
}
343-
344328
// cleanOutput removes all files in dir except those in keep. The keep list
345329
// should contain paths relative to dir. It returns an error if the directory
346330
// does not exist or any file in keep does not exist.

internal/librarian/source.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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/config"
22+
"github.com/googleapis/librarian/internal/fetch"
23+
)
24+
25+
// fetchSource fetches a repository source.
26+
func fetchSource(ctx context.Context, source *config.Source, repo string) (string, error) {
27+
if source == nil {
28+
return "", nil
29+
}
30+
if source.Dir != "" {
31+
return source.Dir, nil
32+
}
33+
34+
dir, err := fetch.RepoDir(ctx, repo, source.Commit, source.SHA256)
35+
if err != nil {
36+
return "", fmt.Errorf("failed to fetch %s: %w", repo, err)
37+
}
38+
return dir, nil
39+
}

internal/librarian/tidy.go

Lines changed: 23 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import (
2222
"strings"
2323

2424
"github.com/googleapis/librarian/internal/config"
25+
"github.com/googleapis/librarian/internal/serviceconfig"
2526
"github.com/googleapis/librarian/internal/yaml"
2627
"github.com/urfave/cli/v3"
2728
)
2829

2930
var (
30-
errDuplicateLibraryName = errors.New("duplicate library name")
31-
errDuplicateChannelPath = errors.New("duplicate channel path")
31+
errDuplicateLibraryName = errors.New("duplicate library name")
32+
errDuplicateChannelPath = errors.New("duplicate channel path")
33+
errNoGoogleapiSourceInfo = errors.New("googleapis source information is not provided in Librarian.yaml file")
3234
)
3335

3436
func tidyCommand() *cli.Command {
@@ -37,13 +39,13 @@ func tidyCommand() *cli.Command {
3739
Usage: "format and validate librarian.yaml",
3840
UsageText: "librarian tidy [path]",
3941
Action: func(ctx context.Context, cmd *cli.Command) error {
40-
return RunTidy()
42+
return RunTidy(ctx)
4143
},
4244
}
4345
}
4446

4547
// RunTidy formats and validates the librarian configuration file.
46-
func RunTidy() error {
48+
func RunTidy(ctx context.Context) error {
4749
cfg, err := yaml.Read[config.Config](librarianConfigPath)
4850
if err != nil {
4951
return err
@@ -52,6 +54,14 @@ func RunTidy() error {
5254
return err
5355
}
5456

57+
if cfg.Sources == nil || cfg.Sources.Googleapis == nil {
58+
return errNoGoogleapiSourceInfo
59+
}
60+
googleapisDir, err := fetchSource(ctx, cfg.Sources.Googleapis, googleapisRepo)
61+
if err != nil {
62+
return err
63+
}
64+
5565
for _, lib := range cfg.Libraries {
5666
if lib.Output != "" && len(lib.Channels) == 1 && isDerivableOutput(cfg, lib) {
5767
lib.Output = ""
@@ -60,7 +70,7 @@ func RunTidy() error {
6070
if isDerivableChannelPath(cfg.Language, lib, ch) {
6171
ch.Path = ""
6272
}
63-
if isDerivableServiceConfig(cfg.Language, lib, ch) {
73+
if isDerivableServiceConfig(cfg.Language, lib, ch, googleapisDir) {
6474
ch.ServiceConfig = ""
6575
}
6676
}
@@ -82,38 +92,19 @@ func isDerivableChannelPath(language string, lib *config.Library, ch *config.Cha
8292
return ch.Path == deriveChannelPath(language, lib)
8393
}
8494

85-
func isDerivableServiceConfig(language string, lib *config.Library, ch *config.Channel) bool {
95+
func isDerivableServiceConfig(language string, lib *config.Library, ch *config.Channel, googleapisDir string) bool {
96+
if ch.ServiceConfig == "" {
97+
return false
98+
}
8699
path := ch.Path
87100
if path == "" {
88101
path = deriveChannelPath(language, lib)
89102
}
90-
return ch.ServiceConfig != "" && ch.ServiceConfig == deriveServiceConfig(path)
91-
}
92-
93-
// deriveServiceConfig returns the conventionally derived service config
94-
// path for a given channel. For example, if resolved_path is
95-
// "google/cloud/speech/v1", it derives to
96-
// "google/cloud/speech/v1/speech_v1.yaml".
97-
//
98-
// It returns an empty string if the resolved path does not contain sufficient
99-
// components or if the version component does not start with 'v'.
100-
//
101-
// This is only used by librarian tidy as a heuristic for serviceconfig.Find,
102-
// since that command currently does not have access to the googleapis
103-
// directory.
104-
//
105-
// TODO(https://github.com/googleapis/librarian/issues/3358): use
106-
// serviceconfig.Find instead.
107-
func deriveServiceConfig(resolvedPath string) string {
108-
parts := strings.Split(resolvedPath, "/")
109-
if len(parts) >= 2 {
110-
version := parts[len(parts)-1]
111-
service := parts[len(parts)-2]
112-
if strings.HasPrefix(version, "v") {
113-
return fmt.Sprintf("%s/%s_%s.yaml", resolvedPath, service, version)
114-
}
103+
derived, err := serviceconfig.Find(googleapisDir, path)
104+
if err != nil {
105+
return false
115106
}
116-
return ""
107+
return ch.ServiceConfig == derived
117108
}
118109

119110
func validateLibraries(cfg *config.Config) error {

internal/librarian/tidy_test.go

Lines changed: 59 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ func TestTidyCommand(t *testing.T) {
9595
configContent := `language: rust
9696
sources:
9797
googleapis:
98-
commit: abc123
98+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
99+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
99100
libraries:
100101
- name: google-cloud-storage-v1
101102
version: "1.0.0"
@@ -139,6 +140,10 @@ func TestTidy_DerivableFields(t *testing.T) {
139140
{
140141
name: "derivable fields removed",
141142
configContent: `
143+
sources:
144+
googleapis:
145+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
146+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
142147
libraries:
143148
- name: google-cloud-accessapproval-v1
144149
channels:
@@ -153,34 +158,28 @@ libraries:
153158
{
154159
name: "non-derivable path not removed",
155160
configContent: `
161+
sources:
162+
googleapis:
163+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
164+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
156165
libraries:
157-
- name: google-cloud-api-servicecontrol-v1
158-
channels:
159-
- path: google/api/servicecontrol/v1
160-
service_config: google/api/servicecontrol/v1/servicecontrol.yaml
161-
`,
162-
wantPath: "google/api/servicecontrol/v1",
163-
wantSvcCfg: "google/api/servicecontrol/v1/servicecontrol.yaml",
164-
wantNumLibs: 1,
165-
wantNumChnls: 1,
166-
},
167-
{
168-
name: "only derivable service config removed",
169-
configContent: `
170-
libraries:
171-
- name: google-cloud-vision-v1
166+
- name: google-cloud-aiplatform-v1-schema-predict-instance
172167
channels:
173-
- path: google/some/other/domain/vision/v1
174-
service_config: google/some/other/domain/vision/v1/vision_v1.yaml
168+
- path: src/generated/cloud/aiplatform/schema/predict/instance
169+
service_config: google/cloud/aiplatform/v1/schema/aiplatform_v1.yaml
175170
`,
176-
wantPath: "google/some/other/domain/vision/v1",
177-
wantSvcCfg: "",
171+
wantPath: "src/generated/cloud/aiplatform/schema/predict/instance",
172+
wantSvcCfg: "google/cloud/aiplatform/v1/schema/aiplatform_v1.yaml",
178173
wantNumLibs: 1,
179174
wantNumChnls: 1,
180175
},
181176
{
182177
name: "path needs to be resolved",
183178
configContent: `
179+
sources:
180+
googleapis:
181+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
182+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
184183
libraries:
185184
- name: google-cloud-vision-v1
186185
channels:
@@ -194,6 +193,10 @@ libraries:
194193
{
195194
name: "service config not derivable (no version at end of path)",
196195
configContent: `
196+
sources:
197+
googleapis:
198+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
199+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
197200
libraries:
198201
- name: google-cloud-speech
199202
channels:
@@ -208,6 +211,10 @@ libraries:
208211
{
209212
name: "channel removed if service config does not exist",
210213
configContent: `
214+
sources:
215+
googleapis:
216+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
217+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
211218
libraries:
212219
- name: google-cloud-orgpolicy-v1
213220
channels:
@@ -288,6 +295,10 @@ func TestTidy_DerivableOutput(t *testing.T) {
288295
language: rust
289296
default:
290297
output: generated/
298+
sources:
299+
googleapis:
300+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
301+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
291302
libraries:
292303
- name: google-cloud-secretmanager-v1
293304
output: generated/cloud/secretmanager/v1
@@ -297,7 +308,7 @@ libraries:
297308
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
298309
t.Fatal(err)
299310
}
300-
if err := RunTidy(); err != nil {
311+
if err := RunTidy(t.Context()); err != nil {
301312
t.Fatal(err)
302313
}
303314
cfg, err := yaml.Read[config.Config](configPath)
@@ -323,6 +334,10 @@ func TestTidyLanguageConfig_Rust(t *testing.T) {
323334
name: "empty_module_removed",
324335
configContent: `
325336
language: rust
337+
sources:
338+
googleapis:
339+
commit: 94ccedca05acb0bb60780789e93371c9e4100ddc
340+
sha256: fff40946e897d96bbdccd566cb993048a87029b7e08eacee3fe99eac792721ba
326341
default:
327342
output: generated/
328343
libraries:
@@ -367,3 +382,26 @@ libraries:
367382
})
368383
}
369384
}
385+
386+
func TestTidyCommandMissingGoogleApisSource(t *testing.T) {
387+
tempDir := t.TempDir()
388+
t.Chdir(tempDir)
389+
configPath := filepath.Join(tempDir, librarianConfigPath)
390+
configContent := `language: rust
391+
libraries:
392+
- name: google-cloud-storage-v1
393+
version: "1.0.0"
394+
- name: google-cloud-bigquery-v1
395+
version: "2.0.0"
396+
`
397+
if err := os.WriteFile(configPath, []byte(configContent), 0644); err != nil {
398+
t.Fatal(err)
399+
}
400+
err := Run(t.Context(), "librarian", "tidy")
401+
if err == nil {
402+
t.Fatalf("expected error, got %v", nil)
403+
}
404+
if !errors.Is(err, errNoGoogleapiSourceInfo) {
405+
t.Errorf("mismatch error want %v got %v", errNoGoogleapiSourceInfo, err)
406+
}
407+
}

0 commit comments

Comments
 (0)