Skip to content

Commit 709adc0

Browse files
authored
feat: add RepositoriesConfig yaml struct (#1661)
Adds config file definitions for repositories.yaml which the list of repositories configured for librarian automation. Towards #1571
1 parent dd70986 commit 709adc0

File tree

3 files changed

+330
-0
lines changed

3 files changed

+330
-0
lines changed

infra/prod/repositories.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
repositories:
2+
- name: "google-cloud-python"
3+
github-token-secret-name: "google-cloud-python-github-token"
4+
supported-commands:
5+
- generate
6+
- stage-release
7+
- publish-release

internal/automation/repositories.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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 automation
16+
17+
import (
18+
"fmt"
19+
"slices"
20+
21+
"gopkg.in/yaml.v3"
22+
)
23+
24+
var availableCommands = map[string]bool{
25+
"generate": true,
26+
"stage-release": true,
27+
"publish-release": true,
28+
}
29+
30+
// RepositoryConfig represents a single registered librarian GitHub repository.
31+
type RepositoryConfig struct {
32+
Name string `yaml:"name"`
33+
SecretName string `yaml:"github-token-secret-name"`
34+
SupportedCommands []string `yaml:"supported-commands"`
35+
}
36+
37+
// RepositoriesConfig represents all the registered librarian GitHub repositories.
38+
type RepositoriesConfig struct {
39+
Repositories []*RepositoryConfig `yaml:"repositories"`
40+
}
41+
42+
// Validate checks the the RepositoryConfig is valid.
43+
func (c *RepositoryConfig) Validate() error {
44+
if c.Name == "" {
45+
return fmt.Errorf("name is required")
46+
}
47+
if c.SecretName == "" {
48+
return fmt.Errorf("secret name is required")
49+
}
50+
if len(c.SupportedCommands) == 0 {
51+
return fmt.Errorf("supported commands cannot be empty")
52+
}
53+
for _, command := range c.SupportedCommands {
54+
if !availableCommands[command] {
55+
return fmt.Errorf("unsupported command: %s", command)
56+
}
57+
}
58+
return nil
59+
}
60+
61+
// Validate checks the the RepositoriesConfig is valid.
62+
func (c *RepositoriesConfig) Validate() error {
63+
for i, r := range c.Repositories {
64+
err := r.Validate()
65+
if err != nil {
66+
return fmt.Errorf("invalid repository config at index %d: %w", i, err)
67+
}
68+
}
69+
return nil
70+
}
71+
72+
// RepositoriesForCommand return a subset of repositories that support the provided command.
73+
func (c *RepositoriesConfig) RepositoriesForCommand(command string) []*RepositoryConfig {
74+
var repositories []*RepositoryConfig
75+
for _, r := range c.Repositories {
76+
if slices.Contains(r.SupportedCommands, command) {
77+
repositories = append(repositories, r)
78+
}
79+
}
80+
return repositories
81+
}
82+
83+
func parseRepositoriesConfig(contentLoader func(file string) ([]byte, error), path string) (*RepositoriesConfig, error) {
84+
bytes, err := contentLoader(path)
85+
if err != nil {
86+
return nil, err
87+
}
88+
var c RepositoriesConfig
89+
if err := yaml.Unmarshal(bytes, &c); err != nil {
90+
return nil, fmt.Errorf("unmarshaling repositories config state: %w", err)
91+
}
92+
if err := c.Validate(); err != nil {
93+
return nil, fmt.Errorf("validating repositories config state: %w", err)
94+
}
95+
return &c, nil
96+
}
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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 automation
16+
17+
import (
18+
"testing"
19+
20+
"github.com/google/go-cmp/cmp"
21+
)
22+
23+
func TestRepositoriesConfig_Validate(t *testing.T) {
24+
for _, test := range []struct {
25+
name string
26+
config *RepositoriesConfig
27+
wantErr bool
28+
}{
29+
{
30+
name: "valid state",
31+
config: &RepositoriesConfig{
32+
Repositories: []*RepositoryConfig{
33+
{
34+
Name: "google-cloud-foo",
35+
SecretName: "google-cloud-foo-github-token",
36+
SupportedCommands: []string{"generate", "stage-release", "publish-release"},
37+
},
38+
},
39+
},
40+
wantErr: false,
41+
},
42+
{
43+
name: "missing name",
44+
config: &RepositoriesConfig{
45+
Repositories: []*RepositoryConfig{
46+
{
47+
SecretName: "google-cloud-foo-github-token",
48+
SupportedCommands: []string{"generate", "stage-release", "publish-release"},
49+
},
50+
},
51+
},
52+
wantErr: true,
53+
},
54+
{
55+
name: "missing secret name",
56+
config: &RepositoriesConfig{
57+
Repositories: []*RepositoryConfig{
58+
{
59+
Name: "google-cloud-foo",
60+
SupportedCommands: []string{"generate", "stage-release", "publish-release"},
61+
},
62+
},
63+
},
64+
wantErr: true,
65+
},
66+
{
67+
name: "missing commands",
68+
config: &RepositoriesConfig{
69+
Repositories: []*RepositoryConfig{
70+
{
71+
Name: "google-cloud-foo",
72+
SecretName: "google-cloud-foo-github-token",
73+
},
74+
},
75+
},
76+
wantErr: true,
77+
},
78+
{
79+
name: "empty commands",
80+
config: &RepositoriesConfig{
81+
Repositories: []*RepositoryConfig{
82+
{
83+
Name: "google-cloud-foo",
84+
SecretName: "google-cloud-foo-github-token",
85+
SupportedCommands: []string{},
86+
},
87+
},
88+
},
89+
wantErr: true,
90+
},
91+
{
92+
name: "invalid command",
93+
config: &RepositoriesConfig{
94+
Repositories: []*RepositoryConfig{
95+
{
96+
Name: "google-cloud-foo",
97+
SecretName: "google-cloud-foo-github-token",
98+
SupportedCommands: []string{"generate", "invalid", "publish-release"},
99+
},
100+
},
101+
},
102+
wantErr: true,
103+
},
104+
} {
105+
t.Run(test.name, func(t *testing.T) {
106+
if err := test.config.Validate(); (err != nil) != test.wantErr {
107+
t.Errorf("LibrarianState.Validate() error = %v, wantErr %v", err, test.wantErr)
108+
}
109+
})
110+
}
111+
}
112+
113+
func TestParseRepositoriesConfig(t *testing.T) {
114+
for _, test := range []struct {
115+
name string
116+
content string
117+
want *RepositoriesConfig
118+
wantErr bool
119+
}{
120+
{
121+
name: "valid state",
122+
content: `repositories:
123+
- name: google-cloud-python
124+
github-token-secret-name: google-cloud-python-github-token
125+
supported-commands:
126+
- generate
127+
- stage-release
128+
`,
129+
want: &RepositoriesConfig{
130+
Repositories: []*RepositoryConfig{
131+
{
132+
Name: "google-cloud-python",
133+
SecretName: "google-cloud-python-github-token",
134+
SupportedCommands: []string{"generate", "stage-release"},
135+
},
136+
},
137+
},
138+
},
139+
{
140+
name: "invalid yaml",
141+
content: `repositories:
142+
- name: google-cloud-python
143+
github-token-secret-name: google-cloud-python-github-token # bad indent
144+
supported-commands:
145+
- generate
146+
- stage-release
147+
`,
148+
want: nil,
149+
wantErr: true,
150+
},
151+
{
152+
name: "validation error",
153+
content: `repositories:
154+
- name: google-cloud-python
155+
github-token-secret-name: google-cloud-python-github-token
156+
# missing supported-commands
157+
`,
158+
want: nil,
159+
wantErr: true,
160+
},
161+
} {
162+
t.Run(test.name, func(t *testing.T) {
163+
contentLoader := func(path string) ([]byte, error) {
164+
return []byte(path), nil
165+
}
166+
got, err := parseRepositoriesConfig(contentLoader, test.content)
167+
if (err != nil) != test.wantErr {
168+
t.Errorf("parseRepositoriesConfig() error = %v, wantErr %v", err, test.wantErr)
169+
return
170+
}
171+
if diff := cmp.Diff(test.want, got); diff != "" {
172+
t.Errorf("parseRepositoriesConfig() mismatch (-want +got): %s", diff)
173+
}
174+
})
175+
}
176+
}
177+
178+
func TestRepositoriesForCommand(t *testing.T) {
179+
testConfig := &RepositoriesConfig{
180+
Repositories: []*RepositoryConfig{
181+
{
182+
Name: "google-cloud-python",
183+
SupportedCommands: []string{"generate", "release"},
184+
},
185+
{
186+
Name: "google-cloud-ruby",
187+
SupportedCommands: []string{"release"},
188+
},
189+
{
190+
Name: "google-cloud-dotnet",
191+
SupportedCommands: []string{"generate", "release"},
192+
},
193+
},
194+
}
195+
for _, test := range []struct {
196+
name string
197+
command string
198+
want []string
199+
}{
200+
{
201+
name: "finds subset",
202+
command: "generate",
203+
want: []string{"google-cloud-python", "google-cloud-dotnet"},
204+
},
205+
{
206+
name: "finds all",
207+
command: "release",
208+
want: []string{"google-cloud-python", "google-cloud-ruby", "google-cloud-dotnet"},
209+
},
210+
{
211+
name: "finds none",
212+
command: "non-existent",
213+
want: []string{},
214+
},
215+
} {
216+
t.Run(test.name, func(t *testing.T) {
217+
got := testConfig.RepositoriesForCommand(test.command)
218+
var names = make([]string, 0)
219+
for _, r := range got {
220+
names = append(names, r.Name)
221+
}
222+
if diff := cmp.Diff(test.want, names); diff != "" {
223+
t.Errorf("parseRepositoriesConfig() mismatch (-want +got): %s", diff)
224+
}
225+
})
226+
}
227+
}

0 commit comments

Comments
 (0)