Skip to content

Commit

Permalink
feat: test-isolation package setup configs (#3951)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
davidcavazos authored Jan 27, 2025
1 parent aae42e1 commit 8368771
Show file tree
Hide file tree
Showing 14 changed files with 323 additions and 92 deletions.
72 changes: 60 additions & 12 deletions .github/cloud-samples-tools/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ import (
var usage = `usage: tools <command> ...
commands:
affected path/to/config.jsonc path/to/diffs.txt
run-all path/to/config.jsonc path/to/script.sh
affected path/to/config.jsonc diffs.txt paths.txt
setup-files path/to/config.jsonc paths.txt
`

// Entry point to validate command line arguments.
Expand All @@ -48,21 +48,34 @@ func main() {
if configFile == "" {
log.Fatalln("❌ no config file specified\n", usage)
}

diffsFile := flag.Arg(2)
if diffsFile == "" {
log.Fatalln("❌ no diffs file specified\n", usage)
}
pathsFile := flag.Arg(3)
if pathsFile == "" {
log.Fatalln("❌ no paths file specified\n", usage)
}
affectedCmd(configFile, diffsFile, pathsFile)

affectedCmd(configFile, diffsFile)
case "setup-files":
configFile := flag.Arg(1)
if configFile == "" {
log.Fatalln("❌ no config file specified\n", usage)
}
pathsFile := flag.Arg(2)
if pathsFile == "" {
log.Fatalln("❌ no paths file specified\n", usage)
}
setupFilesCmd(configFile, pathsFile)

default:
log.Fatalln("❌ unknown command: ", command, "\n", usage)
}
}

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

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

packagesJson, err := json.Marshal(packages)
if pathsFile != "" {
file, err := os.Create(pathsFile)
if err != nil {
log.Fatalln("❌ eror creating output file.\n", err)
}
for _, path := range paths {
fmt.Fprintf(file, "%v\n", path)
}
}

output, err := json.Marshal(paths)
if err != nil {
log.Fatalln("❌ error marshaling paths to JSON.\n", err)
}
fmt.Println(string(output))
}

// setupFilesCmd command entry point to validate inputs.
func setupFilesCmd(configFile string, pathsFile string) {
config, err := c.LoadConfig(configFile)
if err != nil {
log.Fatalln("❌ error marshaling packages to JSON.\n", err)
log.Fatalln("❌ error loading the config file: ", configFile, "\n", err)
}

fmt.Println(string(packagesJson))
pathsBytes, err := os.ReadFile(pathsFile)
if err != nil {
log.Fatalln("❌ error getting the diffs: ", pathsFile, "\n", err)
}
// Trim whitespace to remove extra newline from diff output.
paths := strings.Split(strings.TrimSpace(string(pathsBytes)), "\n")

setups, err := config.FindSetupFiles(paths)
if err != nil {
log.Fatalln("❌ error finding setup files.\n", err)
}

output, err := json.Marshal(setups)
if err != nil {
log.Fatalln("❌ error marshaling setups to JSON.\n", err)
}
fmt.Println(string(output))
}
108 changes: 66 additions & 42 deletions .github/cloud-samples-tools/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import (
"io/fs"
"os"
"path/filepath"
"regexp"
"slices"
"strings"
)
Expand All @@ -33,6 +32,12 @@ type Config struct {
// Filename to look for the root of a package.
PackageFile []string `json:"package-file"`

// CI setup file, must be located in the same directory as the package file.
CISetupFileName string `json:"ci-setup-filename"`

// CI setup defaults, used when no setup file or field is not sepcified in file.
CISetupDefaults CISetup `json:"ci-setup-defaults"`

// Pattern to match filenames or directories.
Match []string `json:"match"`

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

var multiLineCommentsRegex = regexp.MustCompile(`(?s)\s*/\*.*?\*/`)
var singleLineCommentsRegex = regexp.MustCompile(`\s*//.*\s*`)
type CISetup = map[string]any

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

// LoadConfig loads the config from the given path.
func LoadConfig(path string) (*Config, error) {
// Read the JSONC file.
sourceJsonc, err := os.ReadFile(path)
if err != nil {
return nil, err
// Set the config default values.
config := Config{
Match: []string{"*"},
}

// Strip the comments and load the JSON.
sourceJson := multiLineCommentsRegex.ReplaceAll(sourceJsonc, []byte{})
sourceJson = singleLineCommentsRegex.ReplaceAll(sourceJson, []byte{})

var config Config
err = json.Unmarshal(sourceJson, &config)
// This mutates `config` so there's no need to reassign it.
// It keeps the default values if they're not in the JSON file.
err := readJsonc(path, &config)
if err != nil {
return nil, err
}

// Set default values if they are not set.
// Validate for required values.
if config.PackageFile == nil {
return nil, errors.New("package-file is required")
}
if config.Match == nil {
config.Match = []string{"*"}
}

return &config, nil
}

Expand All @@ -110,15 +106,14 @@ func (c *Config) Matches(path string) bool {
// IsPackageDir returns true if the path is a package directory.
func (c *Config) IsPackageDir(dir string) bool {
for _, filename := range c.PackageFile {
packageFile := filepath.Join(dir, filename)
if fileExists(packageFile) {
if fileExists(filepath.Join(dir, filename)) {
return true
}
}
return false
}

// FindPackage returns the package name for the given path.
// FindPackage returns the most specific package path for the given filename.
func (c *Config) FindPackage(path string) string {
dir := filepath.Dir(path)
if dir == "." || c.IsPackageDir(dir) {
Expand All @@ -127,9 +122,9 @@ func (c *Config) FindPackage(path string) string {
return c.FindPackage(dir)
}

// FindAllPackages finds all the packages in the given root directory.
// FindAllPackages finds all the package paths in the given root directory.
func (c *Config) FindAllPackages(root string) ([]string, error) {
var packages []string
var paths []string
err := fs.WalkDir(os.DirFS(root), ".",
func(path string, d os.DirEntry, err error) error {
if err != nil {
Expand All @@ -142,26 +137,15 @@ func (c *Config) FindAllPackages(root string) ([]string, error) {
return nil
}
if d.IsDir() && c.Matches(path) && c.IsPackageDir(path) {
packages = append(packages, path)
paths = append(paths, path)
return nil
}
return nil
})
if err != nil {
return []string{}, err
}
return packages, nil
}

// Affected returns the packages that have been affected from diffs.
// If there are diffs on at leat one global file affecting all packages,
// then this returns all packages matched by the config.
func (c *Config) Affected(log io.Writer, diffs []string) ([]string, error) {
changed := c.Changed(log, diffs)
if slices.Contains(changed, ".") {
return c.FindAllPackages(".")
}
return changed, nil
return paths, nil
}

// Changed returns the packages that have changed.
Expand All @@ -173,17 +157,57 @@ func (c *Config) Changed(log io.Writer, diffs []string) []string {
if !c.Matches(diff) {
continue
}
pkg := c.FindPackage(diff)
changedUnique[pkg] = true
path := c.FindPackage(diff)
if path == "." {
fmt.Fprintf(log, "ℹ️ Global file changed: %q\n", diff)
}
changedUnique[path] = true
}

changed := make([]string, 0, len(changedUnique))
for pkg := range changedUnique {
if slices.Contains(c.ExcludePackages, pkg) {
fmt.Fprintf(log, "ℹ️ Excluded package %q, skipping.\n", pkg)
for path := range changedUnique {
if slices.Contains(c.ExcludePackages, path) {
fmt.Fprintf(log, "ℹ️ Excluded package %q, skipping.\n", path)
continue
}
changed = append(changed, pkg)
changed = append(changed, path)
}
return changed
}

// Affected returns the packages that have been affected from diffs.
// If there are diffs on at leat one global file affecting all packages,
// then this returns all packages matched by the config.
func (c *Config) Affected(log io.Writer, diffs []string) ([]string, error) {
paths := c.Changed(log, diffs)
if slices.Contains(paths, ".") {
fmt.Fprintf(log, "One or more global files were affected, all packages marked as affected.\n")
allPackages, err := c.FindAllPackages(".")
if err != nil {
return nil, err
}
paths = allPackages
}
return paths, nil
}

func (c *Config) FindSetupFiles(paths []string) (*map[string]CISetup, error) {
setups := make(map[string]CISetup, len(paths))
for _, path := range paths {
setup := make(CISetup, len(c.CISetupDefaults))
for k, v := range c.CISetupDefaults {
setup[k] = v
}
setupFile := filepath.Join(path, c.CISetupFileName)
if c.CISetupFileName != "" && fileExists(setupFile) {
// This mutates `setup` so there's no need to reassign it.
// It keeps the default values if they're not in the JSON file.
err := readJsonc(setupFile, &setup)
if err != nil {
return nil, err
}
}
setups[path] = setup
}
return &setups, nil
}
Loading

0 comments on commit 8368771

Please sign in to comment.