From c7d82b4c48df205e6542b9abac3566beb7adc62e Mon Sep 17 00:00:00 2001 From: "Edgar Y. Walker" Date: Sun, 28 Sep 2025 02:01:43 -0700 Subject: [PATCH] feat: :thread: add all developer generation concurrently --- cmd/devenv/generate.go | 146 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 143 insertions(+), 3 deletions(-) diff --git a/cmd/devenv/generate.go b/cmd/devenv/generate.go index 7d29966..23ba84c 100644 --- a/cmd/devenv/generate.go +++ b/cmd/devenv/generate.go @@ -3,12 +3,27 @@ package main import ( "fmt" "os" + "path/filepath" + "time" "github.com/spf13/cobra" "github.com/walkerlab/devenv-engine/internal/config" "github.com/walkerlab/devenv-engine/internal/templates" ) +// DeveloperJob represents work to be done for one developer +type DeveloperJob struct { + Name string +} + +// ProcessingResult represents the outcome of processing one developer +type ProcessingResult struct { + Developer string + Success bool + Error error + Duration time.Duration +} + var ( // Command-specific flags for generate outputDir string @@ -45,7 +60,7 @@ Examples: if verbose { fmt.Printf("Output directory: %s\n", outputDir) } - // TODO: implement all developers logic + generateAllDevelopersWithProgress() } else { developerName := args[0] generateSingleDeveloper(developerName) @@ -62,6 +77,129 @@ func init() { } +func generateAllDevelopersWithProgress() { + // Step 1: Discover all developers + developers, err := findAllDevelopers(configDir) + if err != nil { + fmt.Fprintf(os.Stderr, "Error discovering developers: %v\n", err) + os.Exit(1) + } + + if len(developers) == 0 { + fmt.Printf("No deveolopers found in %s\n", configDir) + return + } + + fmt.Printf("Found %d developers to process.\n", len(developers)) + + // Step 2: Set up channels for worker communication + const numWorkers = 4 + jobs := make(chan DeveloperJob, len(developers)) + results := make(chan ProcessingResult, len(developers)) + + // Step 3: Start worker goroutines + for i := 0; i < numWorkers; i++ { + go developerWorker(jobs, results) + } + + // Step 4: Send all jobs to workers + for _, dev := range developers { + jobs <- DeveloperJob{Name: dev} + } + close(jobs) + + // Step 5: Collect results + var successCount, failureCount int + var failures []ProcessingResult + + for i := 0; i < len(developers); i++ { + result := <-results + if result.Success { + successCount++ + fmt.Printf("[%d/%d] āœ… %s (%.1fs)\n", + i+1, len(developers), result.Developer, result.Duration.Seconds()) + } else { + failureCount++ + failures = append(failures, result) + fmt.Printf("[%d/%d] āŒ %s (%.1fs): %v\n", + i+1, len(developers), result.Developer, result.Duration.Seconds(), result.Error) + } + } + + // Step 6: Print final summary + fmt.Printf("\nšŸŽ‰ Batch processing complete!\n") + fmt.Printf("āœ… Successful: %d\n", successCount) + if failureCount > 0 { + fmt.Printf("āŒ Failed: %d\n", failureCount) + } + + if failureCount > 0 { + fmt.Printf("\nFailures:\n") + for _, failure := range failures { + fmt.Printf(" - %s: %v\n", failure.Developer, failure.Error) + } + os.Exit(1) // Exit with error if any failures + } +} + +func developerWorker(jobs <-chan DeveloperJob, results chan<- ProcessingResult) { + for job := range jobs { + startTime := time.Now() + err := processSingleDeveloperForBatchWithError(job.Name) + + results <- ProcessingResult{ + Developer: job.Name, + Success: err == nil, + Error: err, + Duration: time.Since(startTime), + } + } +} + +// processSingleDeveloperForBatchWithError processes a single developer for batch mode +func processSingleDeveloperForBatchWithError(developerName string) error { + if verbose { + fmt.Printf("Processing developer: %s\n", developerName) + } + + cfg, err := config.LoadDeveloperConfigWithGlobalDefaults(configDir, developerName) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + // Create user-specific output directory + userOutputDir := filepath.Join(outputDir, developerName) + + if !dryRun { + if err := generateManifests(cfg, userOutputDir); err != nil { + return fmt.Errorf("failed to generate manifests: %w", err) + } + } + + return nil +} + +func findAllDevelopers(configDir string) ([]string, error) { + var developers []string + + entries, err := os.ReadDir(configDir) + if err != nil { + return nil, fmt.Errorf("failed to read config directory: %w", err) + } + + for _, entry := range entries { + if entry.IsDir() { + // Check to make sure devenv-config.yaml exists in this directory + configPath := filepath.Join(configDir, entry.Name(), "devenv-config.yaml") + if _, err := os.Stat(configPath); err == nil { + developers = append(developers, entry.Name()) + } + } + } + + return developers, nil +} + // generateSingleDeveloper handles generation for a single developer func generateSingleDeveloper(developerName string) { fmt.Printf("Generating manifests for developer: %s\n", developerName) @@ -72,6 +210,8 @@ func generateSingleDeveloper(developerName string) { fmt.Printf("Dry run mode: %t\n", dryRun) } + userOutputDir := filepath.Join(outputDir, developerName) + cfg, err := config.LoadDeveloperConfigWithGlobalDefaults(configDir, developerName) if err != nil { fmt.Fprintf(os.Stderr, "Error loading config for developer %s: %v\n", developerName, err) @@ -85,12 +225,12 @@ func generateSingleDeveloper(developerName string) { } if !dryRun { - if err := generateManifests(cfg, outputDir); err != nil { + if err := generateManifests(cfg, userOutputDir); err != nil { fmt.Fprintf(os.Stderr, "Error generating manifests: %v\n", err) os.Exit(1) } } else { - fmt.Printf("šŸ” Dry run - would generate manifests to: %s\n", outputDir) + fmt.Printf("šŸ” Dry run - would generate manifests to: %s\n", userOutputDir) } }