package main import ( "context" "fmt" "log" "os" "os/signal" "sync" "syscall" "time" ) // Version information var ( Version = "v3.1.0" BuildTime = "unknown" GitCommit = "unknown" ) func main() { // Print version information log.Printf("[MAIN] Jenkins-Cron %s (built: %s, commit: %s)", Version, BuildTime, GitCommit) // Get configuration path configPath := GetConfigPath() log.Printf("[MAIN] Loading configuration from: %s", configPath) // Load configuration with proper error handling cfg, err := LoadConfig(configPath) if err != nil { log.Fatalf("[MAIN] Failed to load configuration: %v", err) } // Validate configuration if len(cfg.Jobs) == 0 { log.Fatalf("[MAIN] No jobs configured") } if cfg.Jenkins.URL == "" { log.Fatalf("[MAIN] No Jenkins URL configured") } // Create a cancelable context ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Setup signal handling for graceful shutdown setupSignalHandler(cancel) // Clean Gradle caches if err := cleanGradleCaches(ctx, cfg); err != nil { log.Printf("[MAIN] Warning: Error cleaning Gradle caches: %v", err) } // Create Jenkins client var jenkinsSvc JenkinsService = NewJenkinsClient(&cfg.Jenkins) // Trigger Jenkins jobs if err := triggerJobs(ctx, jenkinsSvc, cfg); err != nil { log.Printf("[MAIN] Error triggering jobs: %v", err) os.Exit(1) } log.Printf("[MAIN] All operations completed successfully") } // setupSignalHandler sets up handling of OS signals for graceful shutdown func setupSignalHandler(cancel context.CancelFunc) { c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { sig := <-c log.Printf("[MAIN] Received signal %v, shutting down gracefully", sig) cancel() // Force exit after 5 seconds if graceful shutdown is taking too long time.Sleep(5 * time.Second) log.Printf("[MAIN] Forced shutdown after timeout") os.Exit(1) }() } // cleanGradleCaches cleans Gradle caches in parallel func cleanGradleCaches(ctx context.Context, cfg *Config) error { if len(cfg.Gradle.Caches) == 0 { log.Printf("[MAIN] No Gradle caches configured, skipping cleanup") return nil } log.Printf("[MAIN] Starting Gradle cache cleanup") stats := &RemoveStats{} var wg sync.WaitGroup var errs []error var mu sync.Mutex for _, cache := range cfg.Gradle.Caches { wg.Add(1) go func(cachePath string) { defer wg.Done() log.Printf("[MAIN] Cleaning cache: %s", cachePath) err := Remove(ctx, cachePath, RemoveOptions{ Workers: 4, Retries: 3, Stats: stats, Logger: log.Printf, Progress: func(current int64) { if current > 0 && current%100 == 0 { log.Printf("[MAIN] Removed %d files from %s", current, cachePath) } }, }) if err != nil { log.Printf("[MAIN] Error removing cache %s: %v", cachePath, err) mu.Lock() errs = append(errs, fmt.Errorf("cache %s: %w", cachePath, err)) mu.Unlock() } }(cache) } wg.Wait() log.Printf("[MAIN] Cache cleanup summary: Total=%d Success=%d Failed=%d", stats.Total, stats.Success, stats.Failed) if len(errs) > 0 { return fmt.Errorf("encountered %d errors during cache cleanup", len(errs)) } return nil } // triggerJobs triggers all configured Jenkins jobs func triggerJobs(ctx context.Context, js JenkinsService, cfg *Config) error { log.Printf("[MAIN] Starting to trigger %d Jenkins jobs", len(cfg.Jobs)) var wg sync.WaitGroup errChan := make(chan error, len(cfg.Jobs)) // Create a timeout context for the entire operation ctx, cancel := context.WithTimeout(ctx, 5*time.Minute) defer cancel() for _, job := range cfg.Jobs { wg.Add(1) go func(jobName string) { defer wg.Done() if err := processJob(ctx, js, cfg, jobName); err != nil { log.Printf("[MAIN] Error processing job %s: %v", jobName, err) errChan <- fmt.Errorf("job %s: %w", jobName, err) } }(job) } // Wait for all jobs to complete wg.Wait() close(errChan) // Collect errors var errs []error for err := range errChan { errs = append(errs, err) } if len(errs) > 0 { return fmt.Errorf("encountered %d errors while triggering jobs", len(errs)) } return nil } // processJob handles the processing of a single Jenkins job func processJob(ctx context.Context, js JenkinsService, cfg *Config, job string) error { log.Printf("[MAIN] Processing job: %s", job) // Fetch build with retry build, err := js.FetchBuildWithRetry(ctx, job, cfg.Retries) if err != nil { return fmt.Errorf("failed to fetch build: %w", err) } log.Printf("[MAIN] Successfully fetched build #%d for job: %s", build.Number, job) // Extract and merge parameters latestParams := ExtractBuildParams(build) skipLatestBuild := false for _, skip := range cfg.Jenkins.SkipParameters { if IsSubset(latestParams, skip) { log.Printf("[MAIN] Skipping build (subset of latest) for job %s: %+v", job, skip) skipLatestBuild = true break } } if !skipLatestBuild { merged := MergeParams(latestParams, cfg.Jenkins.DefaultParameters) // Trigger build with default parameters log.Printf("[MAIN] Triggering build with default parameters for job: %s", job) if err := js.TriggerBuildWithRetry(ctx, build, StringMapToInterfaceMap(merged), cfg.Retries); err != nil { log.Printf("[MAIN] Failed to trigger build with default parameters for job %s: %v", job, err) // Continue with special parameters even if default build fails } } // Process special parameters for _, special := range cfg.Jenkins.SpecialParameters { if IsSubset(latestParams, special) { log.Printf("[MAIN] Skipping special parameters build (subset of latest) for %s: %+v", job, special) continue } log.Printf("[MAIN] Triggering build with special parameters for job %s: %+v", job, special) specialLatestParams := make(map[string]string) for k, v := range latestParams { specialLatestParams[k] = v } for k, v := range special { specialLatestParams[k] = fmt.Sprintf("%v", v) } specialMerged := MergeParams(specialLatestParams, cfg.Jenkins.DefaultParameters) if err := js.TriggerBuildWithRetry(ctx, build, StringMapToInterfaceMap(specialMerged), cfg.Retries); err != nil { log.Printf("[MAIN] Failed to trigger build with special parameters for job %s: %v", job, err) // Continue with other special parameters even if one fails } } return nil }