diff --git a/v3.1/Dockerfile b/v3.1/Dockerfile new file mode 100644 index 0000000..297548b --- /dev/null +++ b/v3.1/Dockerfile @@ -0,0 +1,4 @@ +FROM swr.cn-east-3.myhuaweicloud.com/turingsyn/alpine:3.21.3 +ADD jenkins-cron /app/ +RUN chmod +x /app/jenkins-cron +WORKDIR /app diff --git a/v3.1/clean.go b/v3.1/clean.go new file mode 100644 index 0000000..54a8fb7 --- /dev/null +++ b/v3.1/clean.go @@ -0,0 +1,276 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "time" +) + +// RemoveStats tracks statistics about the file removal process +type RemoveStats struct { + Total int64 // Total number of files/directories processed + Success int64 // Number of successfully removed files/directories + Failed int64 // Number of files/directories that failed to be removed + Errors []error // Collection of errors encountered during removal + mu sync.Mutex // Mutex to protect concurrent access to Errors slice +} + +// RemoveOptions configures the file removal process +type RemoveOptions struct { + Workers int // Number of concurrent workers (defaults to CPU count) + Logger func(format string, args ...any) // Logger function + Retries int // Number of retries for failed removals + Stats *RemoveStats // Statistics collector (optional) + Progress func(current int64) // Progress callback function (optional) +} + +// removeResult represents the result of a single file/directory removal operation +type removeResult struct { + Path string // Path that was processed + Err error // Error if removal failed, nil if successful +} + +// Remove recursively removes a directory and all its contents with concurrency support +func Remove(ctx context.Context, path string, opts RemoveOptions) error { + // Apply default options if not specified + if opts.Workers <= 0 { + opts.Workers = runtime.NumCPU() + } + if opts.Logger == nil { + opts.Logger = log.Printf + } + if opts.Retries < 0 { + opts.Retries = 0 + } + if opts.Stats == nil { + opts.Stats = &RemoveStats{} + } + + // Validate path + if path == "" { + return fmt.Errorf("empty path provided") + } + + // Safety check: prevent removal of critical directories + absPath, err := filepath.Abs(path) + if err == nil { + // List of critical paths that should never be removed + criticalPaths := []string{ + "/", "/bin", "/boot", "/dev", "/etc", "/home", "/lib", "/lib64", + "/media", "/mnt", "/opt", "/proc", "/root", "/run", "/sbin", + "/srv", "/sys", "/tmp", "/usr", "/var", + "C:\\", "C:\\Windows", "C:\\Program Files", "C:\\Program Files (x86)", + "C:\\Users", + } + + for _, critical := range criticalPaths { + if absPath == critical { + return fmt.Errorf("refusing to remove critical system directory: %s", absPath) + } + } + } + + // Check if path exists + info, err := os.Stat(path) + if os.IsNotExist(err) { + opts.Logger("[CLEAN] Path does not exist, skipping: %s", path) + return nil + } + if err != nil { + return fmt.Errorf("failed to access path %s: %w", path, err) + } + + // Check if it's a directory + if !info.IsDir() { + opts.Logger("[CLEAN] Path is not a directory, removing file: %s", path) + if err := os.Remove(path); err != nil { + return fmt.Errorf("failed to remove file %s: %w", path, err) + } + if opts.Stats != nil { + atomic.AddInt64(&opts.Stats.Total, 1) + atomic.AddInt64(&opts.Stats.Success, 1) + } + return nil + } + + opts.Logger("[CLEAN] Starting removal of directory: %s with %d workers", path, opts.Workers) + + // Create channels for work distribution and result collection + workChan := make(chan string, opts.Workers*2) + resultChan := make(chan removeResult, opts.Workers*2) + + // Start worker goroutines + var wg sync.WaitGroup + for i := 0; i < opts.Workers; i++ { + wg.Add(1) + go removeWorker(ctx, &wg, workChan, resultChan, opts) + } + + // Start directory traversal + go func() { + defer close(workChan) + if err := walkDir(ctx, path, workChan, opts); err != nil { + opts.Logger("[CLEAN] Error walking directory %s: %v", path, err) + } + }() + + // Wait for workers to finish and close result channel + go func() { + wg.Wait() + close(resultChan) + }() + + // Process results + var firstErr error + for res := range resultChan { + atomic.AddInt64(&opts.Stats.Total, 1) + if res.Err != nil { + atomic.AddInt64(&opts.Stats.Failed, 1) + opts.Stats.mu.Lock() + opts.Stats.Errors = append(opts.Stats.Errors, res.Err) + opts.Stats.mu.Unlock() + if firstErr == nil { + firstErr = res.Err + } + } else { + atomic.AddInt64(&opts.Stats.Success, 1) + } + if opts.Progress != nil { + opts.Progress(atomic.LoadInt64(&opts.Stats.Total)) + } + } + + // Try to remove the root directory itself + if err := tryRemoveRoot(ctx, path, opts); err != nil && firstErr == nil { + firstErr = err + atomic.AddInt64(&opts.Stats.Failed, 1) + opts.Stats.mu.Lock() + opts.Stats.Errors = append(opts.Stats.Errors, err) + opts.Stats.mu.Unlock() + } + + opts.Logger("[CLEAN] Completed removal of %s: Total=%d Success=%d Failed=%d", + path, opts.Stats.Total, opts.Stats.Success, opts.Stats.Failed) + + return firstErr +} + +// walkDir traverses the directory tree and sends paths to the work channel +// Uses depth-first, reverse order to ensure children are processed before parents +func walkDir(ctx context.Context, root string, workChan chan<- string, opts RemoveOptions) error { + var paths []string + + // Walk the directory tree and collect all paths + err := filepath.WalkDir(root, func(subPath string, d os.DirEntry, err error) error { + // Check for context cancellation + if ctx.Err() != nil { + return ctx.Err() + } + + // Handle walk errors + if err != nil { + opts.Logger("[CLEAN] Walk error on %s: %v", subPath, err) + return nil // Skip errors and continue + } + + // Skip the root directory itself + if subPath != root { + paths = append(paths, subPath) + } + return nil + }) + + // Process paths in reverse order (depth-first) + for i := len(paths) - 1; i >= 0; i-- { + select { + case <-ctx.Done(): + return ctx.Err() + case workChan <- paths[i]: + // Path sent to worker + } + } + + return err +} + +// removeWorker processes paths from the work channel and removes them +func removeWorker(ctx context.Context, wg *sync.WaitGroup, workChan <-chan string, resultChan chan<- removeResult, opts RemoveOptions) { + defer wg.Done() + + for { + select { + case <-ctx.Done(): + return + case subPath, ok := <-workChan: + if !ok { + return // Channel closed, exit worker + } + + // Try to remove with retries + var err error + for i := 0; i <= opts.Retries; i++ { + // Check if context is cancelled before each attempt + if ctx.Err() != nil { + resultChan <- removeResult{Path: subPath, Err: ctx.Err()} + return + } + + err = os.RemoveAll(subPath) + if err == nil { + break // Successful removal + } + + // If not the last retry, wait before trying again + if i < opts.Retries { + // Exponential backoff: 100ms, 200ms, 400ms, ... + backoff := time.Duration(100*(1< 1 { + return os.Args[1] + } + return "/app" +} diff --git a/v3.1/config.yaml b/v3.1/config.yaml new file mode 100644 index 0000000..17c2e29 --- /dev/null +++ b/v3.1/config.yaml @@ -0,0 +1,18 @@ +gradle: + caches: + - /home/caches + +jenkins: + schema: https + url: jenkins-ops.shasoapp.com + user: admin + token: 1234567890 + number: lastBuild + default_parameters: + - only_build: true + special_parameters: [] + +retries: 2 +jobs: + - echo-rework + diff --git a/v3.1/go.mod b/v3.1/go.mod new file mode 100644 index 0000000..b463902 --- /dev/null +++ b/v3.1/go.mod @@ -0,0 +1,28 @@ +module jenkins-cron + +go 1.21.1 + +require ( + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/viper v1.20.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/v3.1/go.sum b/v3.1/go.sum new file mode 100644 index 0000000..39ac219 --- /dev/null +++ b/v3.1/go.sum @@ -0,0 +1,54 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/v3.1/jenkins.go b/v3.1/jenkins.go new file mode 100644 index 0000000..8f99167 --- /dev/null +++ b/v3.1/jenkins.go @@ -0,0 +1,299 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "path" + "strings" + "sync" + "time" +) + +// Build represents a Jenkins build +type Build struct { + Actions []Action `json:"actions"` + Number int `json:"number"` + URL string `json:"url"` +} + +// Action represents a Jenkins build action +type Action struct { + Class string `json:"_class"` + Parameters []Parameter `json:"parameters,omitempty"` +} + +// Parameter represents a Jenkins build parameter +type Parameter struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +// JenkinsClient is a client for interacting with Jenkins API +type JenkinsClient struct { + Client *http.Client + Config *Jenkins + semaphore chan struct{} // For concurrency control + mu sync.Mutex // For thread safety +} + +// JenkinsService defines the interface for Jenkins operations +type JenkinsService interface { + FetchBuild(ctx context.Context, job string) (*Build, error) + TriggerBuild(ctx context.Context, build *Build, params map[string]interface{}) error + FetchBuildWithRetry(ctx context.Context, job string, retries int) (*Build, error) + TriggerBuildWithRetry(ctx context.Context, build *Build, params map[string]interface{}, retries int) error +} + +// NewJenkinsClient creates a new Jenkins client +func NewJenkinsClient(cfg *Jenkins) *JenkinsClient { + // Configure HTTP client with reasonable defaults + transport := &http.Transport{ + MaxIdleConns: 10, + IdleConnTimeout: 30 * time.Second, + DisableCompression: false, + TLSHandshakeTimeout: 5 * time.Second, + } + + return &JenkinsClient{ + Client: &http.Client{ + Timeout: 15 * time.Second, // Increased from 10s to 15s + Transport: transport, + }, + Config: cfg, + semaphore: make(chan struct{}, 5), // Limit to 5 concurrent requests + } +} + +// buildAPIURL constructs the Jenkins API URL for a job +func (c *JenkinsClient) buildAPIURL(job string) string { + return fmt.Sprintf("%s://%s/job/%s/%s/api/json", + c.Config.Schema, + c.Config.URL, + job, + c.Config.Number, + ) +} + +// FetchBuild retrieves build information from Jenkins +func (c *JenkinsClient) FetchBuild(ctx context.Context, job string) (*Build, error) { + // Acquire semaphore + select { + case c.semaphore <- struct{}{}: + defer func() { <-c.semaphore }() + case <-ctx.Done(): + return nil, ctx.Err() + } + + api := c.buildAPIURL(job) + log.Printf("[JENKINS] Fetching build info for job: %s from %s", job, api) + + req, err := http.NewRequestWithContext(ctx, "GET", api, nil) + if err != nil { + return nil, fmt.Errorf("failed to create GET request for job %s: %w", job, err) + } + req.SetBasicAuth(c.Config.User, c.Config.Token) + req.Header.Set("Accept", "application/json") + + resp, err := c.Client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed for job %s: %w", job, err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1000)) + return nil, fmt.Errorf("unexpected status %s for job %s: %s", resp.Status, job, string(bodyBytes)) + } + + // Limit response body to 1MB to prevent memory issues + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + return nil, fmt.Errorf("failed to read response body for job %s: %w", job, err) + } + + var build Build + if err := json.Unmarshal(body, &build); err != nil { + return nil, fmt.Errorf("failed to unmarshal response for job %s: %w", job, err) + } + + log.Printf("[JENKINS] Successfully fetched build #%d for job: %s", build.Number, job) + return &build, nil +} + +// FetchBuildWithRetry attempts to fetch a build with retries +func (c *JenkinsClient) FetchBuildWithRetry(ctx context.Context, job string, retries int) (*Build, error) { + var lastErr error + for attempt := 0; attempt <= retries; attempt++ { + if attempt > 0 { + log.Printf("[JENKINS] Retry attempt %d/%d for fetching job: %s", attempt, retries, job) + // Exponential backoff: 1s, 2s, 4s, ... + backoffDuration := time.Duration(1< 0 { + return path.Join(parts[:len(parts)-1]...) + "/buildWithParameters" + } + return p +} + +// TriggerBuild triggers a Jenkins build with the given parameters +func (c *JenkinsClient) TriggerBuild(ctx context.Context, build *Build, params map[string]interface{}) error { + // Acquire semaphore + select { + case c.semaphore <- struct{}{}: + defer func() { <-c.semaphore }() + case <-ctx.Done(): + return ctx.Err() + } + + // Thread safety for params manipulation + c.mu.Lock() + merged := make(map[string]string) + for k, v := range params { + merged[k] = fmt.Sprintf("%v", v) + } + c.mu.Unlock() + + triggerURL := c.generateBuildURL(build.URL, merged) + if triggerURL == "" { + return fmt.Errorf("failed to generate trigger URL") + } + + log.Printf("[JENKINS] Triggering build at URL: %s", triggerURL) + req, err := http.NewRequestWithContext(ctx, "POST", triggerURL, nil) + if err != nil { + return fmt.Errorf("failed to create POST request: %w", err) + } + req.SetBasicAuth(c.Config.User, c.Config.Token) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + resp, err := c.Client.Do(req) + if err != nil { + return fmt.Errorf("trigger request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode >= 400 { + bodyBytes, _ := io.ReadAll(io.LimitReader(resp.Body, 1000)) + return fmt.Errorf("trigger failed with status %s: %s", resp.Status, string(bodyBytes)) + } + + log.Printf("[JENKINS] Successfully triggered build: %s", resp.Status) + return nil +} + +// TriggerBuildWithRetry attempts to trigger a build with retries +func (c *JenkinsClient) TriggerBuildWithRetry(ctx context.Context, build *Build, params map[string]interface{}, retries int) error { + var lastErr error + for attempt := 0; attempt <= retries; attempt++ { + if attempt > 0 { + log.Printf("[JENKINS] Retry attempt %d/%d for triggering build", attempt, retries) + // Exponential backoff: 1s, 2s, 4s, ... + backoffDuration := time.Duration(1< 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) + 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) + } + + speciaMerged := MergeParams(specialLatestParams, cfg.Jenkins.DefaultParameters) + if err := js.TriggerBuildWithRetry(ctx, build, StringMapToInterfaceMap(speciaMerged), 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 +}