-
Notifications
You must be signed in to change notification settings - Fork 70
chore(librariangen): languagecontainer package to parse release-init request #3965
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8d9e83e
75337ca
2276157
e0cf405
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| // 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 | ||
| // | ||
| // http://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 languagecontainer defines LanguageContainer interface and | ||
| // the Run function to execute commands within the container. | ||
| // This package should not have any language-specific implementation or | ||
| // Librarian CLI's implementation. | ||
| // TODO(b/447404382): Move this package to the https://github.com/googleapis/librarian | ||
| // GitHub repository once the interface is finalized. | ||
| package languagecontainer | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "flag" | ||
| "fmt" | ||
| "log/slog" | ||
| "os" | ||
| "path/filepath" | ||
|
|
||
| "cloud.google.com/java/internal/librariangen/languagecontainer/release" | ||
| "cloud.google.com/java/internal/librariangen/message" | ||
| ) | ||
|
|
||
| // LanguageContainer defines the functions for language-specific container operations. | ||
| type LanguageContainer struct { | ||
| ReleaseInit func(context.Context, *release.Config) (*message.ReleaseInitResponse, error) | ||
| // Other container functions like Generate and Build will also be part of the struct. | ||
| } | ||
|
|
||
| // Run accepts an implementation of the LanguageContainer. | ||
| func Run(args []string, container *LanguageContainer) int { | ||
| // Logic to parse args and call the appropriate method on the container. | ||
| // For example, if args[1] is "generate": | ||
| // request := ... // unmarshal the request from the expected location | ||
| // err := container.Generate(context.Background(), request) | ||
| // ... | ||
| if len(args) < 1 { | ||
| panic("args must not be empty") | ||
| } | ||
| cmd := args[0] | ||
| flags := args[1:] | ||
| switch cmd { | ||
| case "generate": | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll move the generate command parsing here. Once Mike's generate command work stop having conflict.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have any outstanding PRs right now. I think it's OK to move it. I can deal with the merge with my WIP.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll change that in a subsequent pull request.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would imagine you would check if the passed in
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's the plan. In this pull requests, LanguageContainer only has ReleaseInit function.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added that in #3968. |
||
| slog.Warn("librariangen: generate command is not yet implemented") | ||
| return 1 | ||
| case "configure": | ||
| slog.Warn("librariangen: configure command is not yet implemented") | ||
| return 1 | ||
| case "release-init": | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is a lot code to put into a
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me work on that.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added handleReleaseInit here. |
||
| return handleReleaseInit(flags, container) | ||
| case "build": | ||
| slog.Warn("librariangen: build command is not yet implemented") | ||
| return 1 | ||
| default: | ||
| slog.Error(fmt.Sprintf("librariangen: unknown command: %s (with flags %v)", cmd, flags)) | ||
| return 1 | ||
| } | ||
| return 0 | ||
| } | ||
|
|
||
| func handleReleaseInit(flags []string, container *LanguageContainer) int { | ||
| cfg := &release.Context{} | ||
| releaseInitFlags := flag.NewFlagSet("release-init", flag.ContinueOnError) | ||
| releaseInitFlags.StringVar(&cfg.LibrarianDir, "librarian", "/librarian", "Path to the librarian-tool input directory. Contains release-init-request.json.") | ||
| releaseInitFlags.StringVar(&cfg.RepoDir, "repo", "/repo", "Path to the language repo.") | ||
| releaseInitFlags.StringVar(&cfg.OutputDir, "output", "/output", "Path to the output directory.") | ||
| if err := releaseInitFlags.Parse(flags); err != nil { | ||
| slog.Error("failed to parse flags", "error", err) | ||
| return 1 | ||
| } | ||
| requestPath := filepath.Join(cfg.LibrarianDir, "release-init-request.json") | ||
| bytes, err := os.ReadFile(requestPath) | ||
| if err != nil { | ||
| slog.Error("failed to read request file", "path", requestPath, "error", err) | ||
| return 1 | ||
| } | ||
| request := &message.ReleaseInitRequest{} | ||
| if err := json.Unmarshal(bytes, request); err != nil { | ||
| slog.Error("failed to parse request JSON", "error", err) | ||
| return 1 | ||
| } | ||
| config := &release.Config{ | ||
| Context: cfg, | ||
| Request: request, | ||
| } | ||
| response, err := container.ReleaseInit(context.Background(), config) | ||
| if err != nil { | ||
| slog.Error("release-init failed", "error", err) | ||
| return 1 | ||
| } | ||
| bytes, err = json.MarshalIndent(response, "", " ") | ||
| if err != nil { | ||
| slog.Error("failed to marshal response JSON", "error", err) | ||
| return 1 | ||
| } | ||
| responsePath := filepath.Join(cfg.LibrarianDir, "release-init-response.json") | ||
| if err := os.WriteFile(responsePath, bytes, 0644); err != nil { | ||
| slog.Error("failed to write response file", "path", responsePath, "error", err) | ||
| return 1 | ||
| } | ||
| slog.Info("librariangen: release-init command executed successfully") | ||
| return 0 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,166 @@ | ||
| // 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 | ||
| // | ||
| // http://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 languagecontainer | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "cloud.google.com/java/internal/librariangen/languagecontainer/release" | ||
| "cloud.google.com/java/internal/librariangen/message" | ||
| "github.com/google/go-cmp/cmp" | ||
| ) | ||
|
|
||
| func TestRun(t *testing.T) { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This test is from main_test.go |
||
| tmpDir := t.TempDir() | ||
| if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| tests := []struct { | ||
| name string | ||
| args []string | ||
| wantCode int | ||
| wantErr bool | ||
| }{ | ||
| { | ||
| name: "unknown command", | ||
| args: []string{"foo"}, | ||
| wantCode: 1, | ||
| }, | ||
| { | ||
| name: "build command", | ||
| args: []string{"build"}, | ||
| wantCode: 1, // Not implemented yet | ||
| }, | ||
| { | ||
| name: "configure command", | ||
| args: []string{"configure"}, | ||
| wantCode: 1, // Not implemented yet | ||
| }, | ||
| { | ||
| name: "generate command", | ||
| args: []string{"generate"}, | ||
| wantCode: 1, // Not implemented yet | ||
| }, | ||
| { | ||
| name: "release-init command success", | ||
| args: []string{"release-init", "-librarian", tmpDir}, | ||
| wantCode: 0, | ||
| }, | ||
| { | ||
| name: "release-init command failure", | ||
| args: []string{"release-init", "-librarian", tmpDir}, | ||
| wantCode: 1, | ||
| wantErr: true, | ||
| }, | ||
| } | ||
| for _, tt := range tests { | ||
| t.Run(tt.name, func(t *testing.T) { | ||
| container := LanguageContainer{ | ||
| ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) { | ||
| if tt.wantErr { | ||
| return nil, os.ErrNotExist | ||
| } | ||
| return &message.ReleaseInitResponse{}, nil | ||
| }, | ||
| } | ||
| if gotCode := Run(tt.args, &container); gotCode != tt.wantCode { | ||
| t.Errorf("Run() = %v, want %v", gotCode, tt.wantCode) | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestRun_noArgs(t *testing.T) { | ||
| defer func() { | ||
| if r := recover(); r == nil { | ||
| t.Errorf("The code did not panic") | ||
| } | ||
| }() | ||
| Run([]string{}, &LanguageContainer{}) | ||
| } | ||
|
|
||
| func TestRun_ReleaseInitWritesResponse(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| if err := os.WriteFile(filepath.Join(tmpDir, "release-init-request.json"), []byte("{}"), 0644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| args := []string{"release-init", "-librarian", tmpDir} | ||
| want := &message.ReleaseInitResponse{Error: "test error"} | ||
| container := LanguageContainer{ | ||
| ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) { | ||
| return want, nil | ||
| }, | ||
| } | ||
|
|
||
| if code := Run(args, &container); code != 0 { | ||
| t.Errorf("Run() = %v, want 0", code) | ||
| } | ||
|
|
||
| responsePath := filepath.Join(tmpDir, "release-init-response.json") | ||
| bytes, err := os.ReadFile(responsePath) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| got := &message.ReleaseInitResponse{} | ||
| if err := json.Unmarshal(bytes, got); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if diff := cmp.Diff(want, got); diff != "" { | ||
| t.Errorf("response mismatch (-want +got):\n%s", diff) | ||
| } | ||
| } | ||
|
|
||
| func TestRun_ReleaseInitReadsContextArgs(t *testing.T) { | ||
| tmpDir := t.TempDir() | ||
| librarianDir := filepath.Join(tmpDir, "librarian") | ||
| if err := os.Mkdir(librarianDir, 0755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if err := os.WriteFile(filepath.Join(librarianDir, "release-init-request.json"), []byte("{}"), 0644); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| repoDir := filepath.Join(tmpDir, "repo") | ||
| if err := os.Mkdir(repoDir, 0755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| outputDir := filepath.Join(tmpDir, "output") | ||
| if err := os.Mkdir(outputDir, 0755); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| args := []string{"release-init", "-librarian", librarianDir, "-repo", repoDir, "-output", outputDir} | ||
| var gotConfig *release.Config | ||
| container := LanguageContainer{ | ||
| ReleaseInit: func(ctx context.Context, c *release.Config) (*message.ReleaseInitResponse, error) { | ||
| gotConfig = c | ||
| return &message.ReleaseInitResponse{}, nil | ||
| }, | ||
| } | ||
| if code := Run(args, &container); code != 0 { | ||
| t.Errorf("Run() = %v, want 0", code) | ||
| } | ||
| if got, want := gotConfig.Context.LibrarianDir, librarianDir; got != want { | ||
| t.Errorf("gotConfig.Context.LibrarianDir = %q, want %q", got, want) | ||
| } | ||
| if got, want := gotConfig.Context.RepoDir, repoDir; got != want { | ||
| t.Errorf("gotConfig.Context.RepoDir = %q, want %q", got, want) | ||
| } | ||
| if got, want := gotConfig.Context.OutputDir, outputDir; got != want { | ||
| t.Errorf("gotConfig.Context.OutputDir = %q, want %q", got, want) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // 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 | ||
| // | ||
| // http://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 release contains types for language container's release command. | ||
| package release | ||
|
|
||
| import "cloud.google.com/java/internal/librariangen/message" | ||
|
|
||
| // Context has the directory paths for the release-init command. | ||
| // https://github.com/googleapis/librarian/blob/main/doc/language-onboarding.md#release-init | ||
| type Context struct { | ||
| LibrarianDir string | ||
| RepoDir string | ||
| OutputDir string | ||
| } | ||
|
|
||
| // The Config for the release-init command. This holds the context (the directory paths) | ||
| // and the request parsed from the release-init-request.json file. | ||
| type Config struct { | ||
| Context *Context | ||
| // This request is parsed from the release-init-request.json file in | ||
| // the LibrarianDir of the context. | ||
| Request *message.ReleaseInitRequest | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // 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 | ||
| // | ||
| // http://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 release | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "cloud.google.com/java/internal/librariangen/message" | ||
| "github.com/google/go-cmp/cmp" | ||
| ) | ||
|
|
||
| func TestReadReleaseInitRequest(t *testing.T) { | ||
| want := &message.ReleaseInitRequest{ | ||
| Libraries: []*message.Library{ | ||
| { | ||
| ID: "google-cloud-secretmanager-v1", | ||
| Version: "1.3.0", | ||
| Changes: []*message.Change{ | ||
| { | ||
| Type: "feat", | ||
| Subject: "add new UpdateRepository API", | ||
| Body: "This adds the ability to update a repository's properties.", | ||
| PiperCLNumber: "786353207", | ||
| CommitHash: "9461532e7d19c8d71709ec3b502e5d81340fb661", | ||
| }, | ||
| { | ||
| Type: "docs", | ||
| Subject: "fix typo in BranchRule comment", | ||
| Body: "", | ||
| PiperCLNumber: "786353207", | ||
| CommitHash: "9461532e7d19c8d71709ec3b502e5d81340fb661", | ||
| }, | ||
| }, | ||
| APIs: []message.API{ | ||
| { | ||
| Path: "google/cloud/secretmanager/v1", | ||
| }, | ||
| { | ||
| Path: "google/cloud/secretmanager/v1beta", | ||
| }, | ||
| }, | ||
| SourcePaths: []string{ | ||
| "secretmanager", | ||
| "other/location/secretmanager", | ||
| }, | ||
| ReleaseTriggered: true, | ||
| }, | ||
| }, | ||
| } | ||
| bytes, err := os.ReadFile(filepath.Join("..", "testdata", "release-init-request.json")) | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| got := &message.ReleaseInitRequest{} | ||
| if err := json.Unmarshal(bytes, got); err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| if diff := cmp.Diff(want, got); diff != "" { | ||
| t.Errorf("Unmarshal() mismatch (-want +got):\n%s", diff) | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not use an interface here?
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this is an interface, then there would be package name conflict of
languagecontainer/releasepackage (language agostic) andreleasepackage (language-specific. In this case Java-specific). Here is a piece of code when I tried to introducelanguagecontainer/releaseandreleasepackages. The naming conflict happened and I had to rename one of them torelease2:I just added this observation in the description of this pull requests.
CC: @codyoss We talked about interface v.s. a struct with functions. I now think the struct is better because we don't have to rename "languagecontainer/release" package when we use (language-specific) "release" package used in main.go.