Skip to content

Commit 8368771

Browse files
author
David Cavazos
authored
feat: test-isolation package setup configs (#3951)
* add initial config * revert packege to dev * add secrets * load package setup * add setup file to config * support default values * handle packages without setup file * support secrets * support nodejs_version * ignore config files * find setup files on separate command * implement finding setup files in tooling * move timeout to test itself * ignore workflow files * add debugging information * more tries on indexing by path * experiment with CI_SETUP env var * attempt to fix setup configs * move back timeout to test command * fix timeout * join secrets with newline * revert to join by comma * experiment with env vars * inspect env vars * made secrets and env consistent * remove test vars * changed default values * change secrets condition * use default node version * minor cleanups * add tests * add ci-setup-filename * mark generative-ai as prod * make paths file required * support setup files on prod * nit fixes * update to use prod config * rename setups to setup-files * retry test * revert retries
1 parent aae42e1 commit 8368771

File tree

14 files changed

+323
-92
lines changed

14 files changed

+323
-92
lines changed

.github/cloud-samples-tools/cmd/main.go

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ import (
2929
var usage = `usage: tools <command> ...
3030
3131
commands:
32-
affected path/to/config.jsonc path/to/diffs.txt
33-
run-all path/to/config.jsonc path/to/script.sh
32+
affected path/to/config.jsonc diffs.txt paths.txt
33+
setup-files path/to/config.jsonc paths.txt
3434
`
3535

3636
// Entry point to validate command line arguments.
@@ -48,21 +48,34 @@ func main() {
4848
if configFile == "" {
4949
log.Fatalln("❌ no config file specified\n", usage)
5050
}
51-
5251
diffsFile := flag.Arg(2)
5352
if diffsFile == "" {
5453
log.Fatalln("❌ no diffs file specified\n", usage)
5554
}
55+
pathsFile := flag.Arg(3)
56+
if pathsFile == "" {
57+
log.Fatalln("❌ no paths file specified\n", usage)
58+
}
59+
affectedCmd(configFile, diffsFile, pathsFile)
5660

57-
affectedCmd(configFile, diffsFile)
61+
case "setup-files":
62+
configFile := flag.Arg(1)
63+
if configFile == "" {
64+
log.Fatalln("❌ no config file specified\n", usage)
65+
}
66+
pathsFile := flag.Arg(2)
67+
if pathsFile == "" {
68+
log.Fatalln("❌ no paths file specified\n", usage)
69+
}
70+
setupFilesCmd(configFile, pathsFile)
5871

5972
default:
6073
log.Fatalln("❌ unknown command: ", command, "\n", usage)
6174
}
6275
}
6376

64-
// affected command entry point to validate inputs.
65-
func affectedCmd(configFile string, diffsFile string) {
77+
// affectedCmd command entry point to validate inputs.
78+
func affectedCmd(configFile string, diffsFile string, pathsFile string) {
6679
config, err := c.LoadConfig(configFile)
6780
if err != nil {
6881
log.Fatalln("❌ error loading the config file: ", configFile, "\n", err)
@@ -76,23 +89,58 @@ func affectedCmd(configFile string, diffsFile string) {
7689
diffs := strings.Split(strings.TrimSpace(string(diffsBytes)), "\n")
7790

7891
// Log to stderr since GitHub Actions expects the output on stdout.
79-
packages, err := config.Affected(os.Stderr, diffs)
92+
paths, err := config.Affected(os.Stderr, diffs)
8093
if err != nil {
8194
log.Fatalln("❌ error finding the affected packages.\n", err)
8295
}
83-
if len(packages) > 256 {
96+
if len(paths) > 256 {
8497
log.Fatalln(
8598
"❌ Error: GitHub Actions only supports up to 256 packages, got ",
86-
len(packages),
99+
len(paths),
87100
" packages, for more details see:\n",
88101
"https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/running-variations-of-jobs-in-a-workflow",
89102
)
90103
}
91104

92-
packagesJson, err := json.Marshal(packages)
105+
if pathsFile != "" {
106+
file, err := os.Create(pathsFile)
107+
if err != nil {
108+
log.Fatalln("❌ eror creating output file.\n", err)
109+
}
110+
for _, path := range paths {
111+
fmt.Fprintf(file, "%v\n", path)
112+
}
113+
}
114+
115+
output, err := json.Marshal(paths)
116+
if err != nil {
117+
log.Fatalln("❌ error marshaling paths to JSON.\n", err)
118+
}
119+
fmt.Println(string(output))
120+
}
121+
122+
// setupFilesCmd command entry point to validate inputs.
123+
func setupFilesCmd(configFile string, pathsFile string) {
124+
config, err := c.LoadConfig(configFile)
93125
if err != nil {
94-
log.Fatalln("❌ error marshaling packages to JSON.\n", err)
126+
log.Fatalln("❌ error loading the config file: ", configFile, "\n", err)
95127
}
96128

97-
fmt.Println(string(packagesJson))
129+
pathsBytes, err := os.ReadFile(pathsFile)
130+
if err != nil {
131+
log.Fatalln("❌ error getting the diffs: ", pathsFile, "\n", err)
132+
}
133+
// Trim whitespace to remove extra newline from diff output.
134+
paths := strings.Split(strings.TrimSpace(string(pathsBytes)), "\n")
135+
136+
setups, err := config.FindSetupFiles(paths)
137+
if err != nil {
138+
log.Fatalln("❌ error finding setup files.\n", err)
139+
}
140+
141+
output, err := json.Marshal(setups)
142+
if err != nil {
143+
log.Fatalln("❌ error marshaling setups to JSON.\n", err)
144+
}
145+
fmt.Println(string(output))
98146
}

.github/cloud-samples-tools/pkg/config/config.go

Lines changed: 66 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"io/fs"
2525
"os"
2626
"path/filepath"
27-
"regexp"
2827
"slices"
2928
"strings"
3029
)
@@ -33,6 +32,12 @@ type Config struct {
3332
// Filename to look for the root of a package.
3433
PackageFile []string `json:"package-file"`
3534

35+
// CI setup file, must be located in the same directory as the package file.
36+
CISetupFileName string `json:"ci-setup-filename"`
37+
38+
// CI setup defaults, used when no setup file or field is not sepcified in file.
39+
CISetupDefaults CISetup `json:"ci-setup-defaults"`
40+
3641
// Pattern to match filenames or directories.
3742
Match []string `json:"match"`
3843

@@ -43,8 +48,7 @@ type Config struct {
4348
ExcludePackages []string `json:"exclude-packages"`
4449
}
4550

46-
var multiLineCommentsRegex = regexp.MustCompile(`(?s)\s*/\*.*?\*/`)
47-
var singleLineCommentsRegex = regexp.MustCompile(`\s*//.*\s*`)
51+
type CISetup = map[string]any
4852

4953
// Saves the config to the given file.
5054
func (c *Config) Save(file *os.File) error {
@@ -61,30 +65,22 @@ func (c *Config) Save(file *os.File) error {
6165

6266
// LoadConfig loads the config from the given path.
6367
func LoadConfig(path string) (*Config, error) {
64-
// Read the JSONC file.
65-
sourceJsonc, err := os.ReadFile(path)
66-
if err != nil {
67-
return nil, err
68+
// Set the config default values.
69+
config := Config{
70+
Match: []string{"*"},
6871
}
6972

70-
// Strip the comments and load the JSON.
71-
sourceJson := multiLineCommentsRegex.ReplaceAll(sourceJsonc, []byte{})
72-
sourceJson = singleLineCommentsRegex.ReplaceAll(sourceJson, []byte{})
73-
74-
var config Config
75-
err = json.Unmarshal(sourceJson, &config)
73+
// This mutates `config` so there's no need to reassign it.
74+
// It keeps the default values if they're not in the JSON file.
75+
err := readJsonc(path, &config)
7676
if err != nil {
7777
return nil, err
7878
}
7979

80-
// Set default values if they are not set.
80+
// Validate for required values.
8181
if config.PackageFile == nil {
8282
return nil, errors.New("package-file is required")
8383
}
84-
if config.Match == nil {
85-
config.Match = []string{"*"}
86-
}
87-
8884
return &config, nil
8985
}
9086

@@ -110,15 +106,14 @@ func (c *Config) Matches(path string) bool {
110106
// IsPackageDir returns true if the path is a package directory.
111107
func (c *Config) IsPackageDir(dir string) bool {
112108
for _, filename := range c.PackageFile {
113-
packageFile := filepath.Join(dir, filename)
114-
if fileExists(packageFile) {
109+
if fileExists(filepath.Join(dir, filename)) {
115110
return true
116111
}
117112
}
118113
return false
119114
}
120115

121-
// FindPackage returns the package name for the given path.
116+
// FindPackage returns the most specific package path for the given filename.
122117
func (c *Config) FindPackage(path string) string {
123118
dir := filepath.Dir(path)
124119
if dir == "." || c.IsPackageDir(dir) {
@@ -127,9 +122,9 @@ func (c *Config) FindPackage(path string) string {
127122
return c.FindPackage(dir)
128123
}
129124

130-
// FindAllPackages finds all the packages in the given root directory.
125+
// FindAllPackages finds all the package paths in the given root directory.
131126
func (c *Config) FindAllPackages(root string) ([]string, error) {
132-
var packages []string
127+
var paths []string
133128
err := fs.WalkDir(os.DirFS(root), ".",
134129
func(path string, d os.DirEntry, err error) error {
135130
if err != nil {
@@ -142,26 +137,15 @@ func (c *Config) FindAllPackages(root string) ([]string, error) {
142137
return nil
143138
}
144139
if d.IsDir() && c.Matches(path) && c.IsPackageDir(path) {
145-
packages = append(packages, path)
140+
paths = append(paths, path)
146141
return nil
147142
}
148143
return nil
149144
})
150145
if err != nil {
151146
return []string{}, err
152147
}
153-
return packages, nil
154-
}
155-
156-
// Affected returns the packages that have been affected from diffs.
157-
// If there are diffs on at leat one global file affecting all packages,
158-
// then this returns all packages matched by the config.
159-
func (c *Config) Affected(log io.Writer, diffs []string) ([]string, error) {
160-
changed := c.Changed(log, diffs)
161-
if slices.Contains(changed, ".") {
162-
return c.FindAllPackages(".")
163-
}
164-
return changed, nil
148+
return paths, nil
165149
}
166150

167151
// Changed returns the packages that have changed.
@@ -173,17 +157,57 @@ func (c *Config) Changed(log io.Writer, diffs []string) []string {
173157
if !c.Matches(diff) {
174158
continue
175159
}
176-
pkg := c.FindPackage(diff)
177-
changedUnique[pkg] = true
160+
path := c.FindPackage(diff)
161+
if path == "." {
162+
fmt.Fprintf(log, "ℹ️ Global file changed: %q\n", diff)
163+
}
164+
changedUnique[path] = true
178165
}
179166

180167
changed := make([]string, 0, len(changedUnique))
181-
for pkg := range changedUnique {
182-
if slices.Contains(c.ExcludePackages, pkg) {
183-
fmt.Fprintf(log, "ℹ️ Excluded package %q, skipping.\n", pkg)
168+
for path := range changedUnique {
169+
if slices.Contains(c.ExcludePackages, path) {
170+
fmt.Fprintf(log, "ℹ️ Excluded package %q, skipping.\n", path)
184171
continue
185172
}
186-
changed = append(changed, pkg)
173+
changed = append(changed, path)
187174
}
188175
return changed
189176
}
177+
178+
// Affected returns the packages that have been affected from diffs.
179+
// If there are diffs on at leat one global file affecting all packages,
180+
// then this returns all packages matched by the config.
181+
func (c *Config) Affected(log io.Writer, diffs []string) ([]string, error) {
182+
paths := c.Changed(log, diffs)
183+
if slices.Contains(paths, ".") {
184+
fmt.Fprintf(log, "One or more global files were affected, all packages marked as affected.\n")
185+
allPackages, err := c.FindAllPackages(".")
186+
if err != nil {
187+
return nil, err
188+
}
189+
paths = allPackages
190+
}
191+
return paths, nil
192+
}
193+
194+
func (c *Config) FindSetupFiles(paths []string) (*map[string]CISetup, error) {
195+
setups := make(map[string]CISetup, len(paths))
196+
for _, path := range paths {
197+
setup := make(CISetup, len(c.CISetupDefaults))
198+
for k, v := range c.CISetupDefaults {
199+
setup[k] = v
200+
}
201+
setupFile := filepath.Join(path, c.CISetupFileName)
202+
if c.CISetupFileName != "" && fileExists(setupFile) {
203+
// This mutates `setup` so there's no need to reassign it.
204+
// It keeps the default values if they're not in the JSON file.
205+
err := readJsonc(setupFile, &setup)
206+
if err != nil {
207+
return nil, err
208+
}
209+
}
210+
setups[path] = setup
211+
}
212+
return &setups, nil
213+
}

0 commit comments

Comments
 (0)