commit aaad8b143fef5548218b9a907176ed33d2ce413b Author: “xHuPo” <7513325+vrocwang@users.noreply.github.com> Date: Fri May 9 17:21:06 2025 +0800 first commit diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..2857fad --- /dev/null +++ b/readme.md @@ -0,0 +1 @@ +CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o jenkins-cron diff --git a/v1/Dockerfile b/v1/Dockerfile new file mode 100644 index 0000000..297548b --- /dev/null +++ b/v1/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/v1/clean.go b/v1/clean.go new file mode 100644 index 0000000..655b23f --- /dev/null +++ b/v1/clean.go @@ -0,0 +1,33 @@ +package main + +import ( + "log" + "os" + "path/filepath" +) + +func Remove(path string) error { + entries, err := os.ReadDir(path) + if err != nil { + log.Printf("clean: Error reading directory: %v", err) + return err + } + + for _, entry := range entries { + if entry.IsDir() { + err := Remove(filepath.Join(path, entry.Name())) + if err != nil { + log.Printf("clean: Error removing directory: %v", err) + return err + } + } else { + err := os.Remove(filepath.Join(path, entry.Name())) + if err != nil { + log.Printf("clean: Error removing file: %v", err) + return err + } + } + } + log.Printf("clean: Removed directory successfully: %s", path) + return nil +} diff --git a/v1/config.go b/v1/config.go new file mode 100644 index 0000000..e42ce6c --- /dev/null +++ b/v1/config.go @@ -0,0 +1,46 @@ +package main + +import ( + "log" + + "github.com/spf13/viper" +) + +type Gradle struct { + Caches []string `mapstructure:"caches"` +} + +type Jenkins struct { + Schema string `mapstructure:"schema"` + URL string `mapstructure:"url"` + User string `mapstructure:"user"` + Token string `mapstructure:"token"` + Number string `mapstructure:"number"` + DefaultParameters []map[string]interface{} `mapstructure:"default_parameters"` +} + +type Config struct { + Gradle Gradle + Jenkins Jenkins + Jobs []string `mapstructure:"jobs"` +} + +func LoadConfig(path string) (*Config, error) { + v := viper.New() + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(path) + + if err := v.ReadInConfig(); err != nil { + log.Fatal("config: Error reading config file:", err) + return nil, err + } + + var config Config + if err := v.Unmarshal(&config); err != nil { + log.Fatal("config: Unable to decode config into struct:", err) + return nil, err + } + + return &config, nil +} diff --git a/v1/config.yaml b/v1/config.yaml new file mode 100644 index 0000000..c96fb2e --- /dev/null +++ b/v1/config.yaml @@ -0,0 +1,16 @@ +gradle: + caches: + - /home/caches + +jenkins: + schema: https + url: jenkins-ops.shasoapp.com + user: admin + token: 1234567890 + number: lastBuild + default_parameters: + - only_build: true + +jobs: + - echo-rework + diff --git a/v1/go.mod b/v1/go.mod new file mode 100644 index 0000000..4480fe1 --- /dev/null +++ b/v1/go.mod @@ -0,0 +1,21 @@ +module jenkins-cron + +go 1.21.1 + +require ( + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // 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/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/v1/go.sum b/v1/go.sum new file mode 100644 index 0000000..51edcb7 --- /dev/null +++ b/v1/go.sum @@ -0,0 +1,36 @@ +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/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.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= +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/v1/jenkins.go b/v1/jenkins.go new file mode 100644 index 0000000..401033e --- /dev/null +++ b/v1/jenkins.go @@ -0,0 +1,125 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "path" + "strings" + "time" +) + +type Build struct { + Actions []Action `json:"actions"` + Number interface{} `json:"number"` + URL string `json:"url"` +} + +type Action struct { + Class string `json:"_class"` + Parameters []Parameter `json:"parameters,omitempty"` +} + +type Parameter struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +var client = &http.Client{ + Timeout: 10 * time.Second, +} + +func FetchBuild(cfg *Config, job string) (*Build, error) { + buildID := cfg.Jenkins.Number + api := fmt.Sprintf("%s://%s/job/%s/%s/api/json", + cfg.Jenkins.Schema, + cfg.Jenkins.URL, + job, + buildID, + ) + + req, err := http.NewRequest("GET", api, nil) + if err != nil { + log.Printf("failed to create GET request %s: %v", job, err) + return nil, err + } + req.SetBasicAuth(cfg.Jenkins.User, cfg.Jenkins.Token) + + resp, err := client.Do(req) + if err != nil { + log.Printf("failed to fetch build %s: %v", job, err) + return nil, err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Printf("failed to fetch build %s: %s", job, resp.Status) + return nil, fmt.Errorf("failed to fetch build: %s", resp.Status) + } + + // io限制,1m + body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + + var build Build + if err := json.Unmarshal(body, &build); err != nil { + log.Printf("failed to unmarshal build %s: %v", job, err) + return nil, err + } + + return &build, nil +} + +func GenerateBuildURL(cfg *Config, build *Build) string { + values := url.Values{} + + for _, action := range build.Actions { + if action.Class == "hudson.model.ParametersAction" { + for _, param := range action.Parameters { + values.Set(param.Name, fmt.Sprintf("%v", param.Value)) + } + } + } + + for _, param := range cfg.Jenkins.DefaultParameters { + for k, v := range param { + values.Set(k, fmt.Sprintf("%v", v)) + } + } + + u, err := url.Parse(build.URL) + if err != nil { + log.Fatalf("failed to parse build URL: %v", err) + } + + parts := strings.Split(strings.TrimSuffix(u.Path, "/"), "/") + if len(parts) > 0 { + u.Path = path.Join(parts[:len(parts)-1]...) + "/buildWithParameters" + } + + u.RawQuery = values.Encode() + return u.String() +} + +func TriggerBuild(cfg *Config, build *Build) error { + url := GenerateBuildURL(cfg, build) + + req, err := http.NewRequest("POST", url, nil) + if err != nil { + log.Printf("failed to create POST request: %v", err) + return err + } + req.SetBasicAuth(cfg.Jenkins.User, cfg.Jenkins.Token) + + resp, err := client.Do(req) + if err != nil { + log.Printf("failed to trigger build %s: %v", url, err) + return err + } + defer resp.Body.Close() + + log.Printf("triggered build %s: %s", url, resp.Status) + return nil +} diff --git a/v1/main.go b/v1/main.go new file mode 100644 index 0000000..3d13900 --- /dev/null +++ b/v1/main.go @@ -0,0 +1,32 @@ +package main + +import "log" + +func main() { + cfg, _ := LoadConfig("/app") + + if len(cfg.Jobs) == 0 { + log.Printf("main: No jobs configured") + return + } + + for _, cache := range cfg.Gradle.Caches { + log.Printf("main: Removing gradle cache %s", cache) + if err := Remove(cache); err != nil { + log.Printf("main: Error removing gradle caches %s: %s", cache, err) + } + } + + for _, job := range cfg.Jobs { + log.Printf("main: Triggering build for job %s", job) + build, err := FetchBuild(cfg, job) + if err != nil { + log.Printf("main: Error fetching build for job %s: %s", job, err) + return + } + + if err := TriggerBuild(cfg, build); err != nil { + log.Printf("main: Error triggering build for job %s: %s", job, err) + } + } +} diff --git a/v2/Dockerfile b/v2/Dockerfile new file mode 100644 index 0000000..297548b --- /dev/null +++ b/v2/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/v2/clean.go b/v2/clean.go new file mode 100644 index 0000000..ee0f231 --- /dev/null +++ b/v2/clean.go @@ -0,0 +1,178 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "sync" + "sync/atomic" + "time" +) + +type RemoveStats struct { + Total int64 + Success int64 + Failed int64 + Errors []error +} + +type RemoveOptions struct { + Workers int // 并发 worker 数 + Logger func(format string, args ...any) + Retries int // 删除失败重试次数 + Stats *RemoveStats // 删除统计(可选) + Progress func(current int64) // 删除进度回调(可选) +} + +type removeResult struct { + Err error +} + +func Remove(ctx context.Context, path string, opts RemoveOptions) error { + 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{} + } + + // 检查路径是否存在 + if _, err := os.Stat(path); os.IsNotExist(err) { + opts.Logger("[clean] Path does not exist, skipping: %s", path) + return nil + } + + workChan := make(chan string, opts.Workers*2) + resultChan := make(chan removeResult, opts.Workers*2) + + var wg sync.WaitGroup + for i := 0; i < opts.Workers; i++ { + wg.Add(1) + go removeWorker(ctx, &wg, workChan, resultChan, opts) + } + + // 目录遍历 + go func() { + defer close(workChan) + _ = walkDir(ctx, path, workChan, opts) + }() + + // 收集结果 + go func() { + wg.Wait() + close(resultChan) + }() + + var ( + firstErr error + errorsMu sync.Mutex + ) + + for res := range resultChan { + atomic.AddInt64(&opts.Stats.Total, 1) + if res.Err != nil { + atomic.AddInt64(&opts.Stats.Failed, 1) + errorsMu.Lock() + opts.Stats.Errors = append(opts.Stats.Errors, res.Err) + errorsMu.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)) + } + } + + // 尝试删除根目录 + if err := tryRemoveRoot(ctx, path, opts); err != nil && firstErr == nil { + firstErr = err + opts.Stats.Failed++ + opts.Stats.Errors = append(opts.Stats.Errors, err) + } + + return firstErr +} + +// 遍历目录,逐一发送子路径(不含根目录)到 workChan,深度优先、逆序删除 +func walkDir(ctx context.Context, root string, workChan chan<- string, opts RemoveOptions) error { + var paths []string + err := filepath.WalkDir(root, func(subPath string, d os.DirEntry, err error) error { + if ctx.Err() != nil { + return ctx.Err() + } + if err != nil { + opts.Logger("[clean] Walk error on %s: %v", subPath, err) + return nil // 跳过错误继续 + } + if subPath != root { + paths = append(paths, subPath) + } + return nil + }) + // 逆序删除(先深后浅) + for i := len(paths) - 1; i >= 0; i-- { + select { + case <-ctx.Done(): + return ctx.Err() + case workChan <- paths[i]: + } + } + return err +} + +// Worker 执行删除任务,支持重试 +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 + } + var err error + for i := 0; i <= opts.Retries; i++ { + err = os.RemoveAll(subPath) + if err == nil { + break + } + time.Sleep(100 * time.Millisecond) + } + if err != nil { + opts.Logger("[clean] Failed to remove %s: %v", subPath, err) + resultChan <- removeResult{Err: fmt.Errorf("remove %q: %w", subPath, err)} + } else { + opts.Logger("[clean] Removed: %s", subPath) + resultChan <- removeResult{} + } + } + } +} + +// 删除根目录 +func tryRemoveRoot(ctx context.Context, path string, opts RemoveOptions) error { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + if err := os.Remove(path); err != nil { + opts.Logger("[clean] Failed to remove root %s: %v", path, err) + return fmt.Errorf("remove root %q: %w", path, err) + } + opts.Logger("[clean] Removed root directory: %s", path) + return nil +} diff --git a/v2/config.go b/v2/config.go new file mode 100644 index 0000000..e9d2443 --- /dev/null +++ b/v2/config.go @@ -0,0 +1,46 @@ +package main + +import ( + "log" + + "github.com/spf13/viper" +) + +type Gradle struct { + Caches []string `mapstructure:"caches"` +} + +type Jenkins struct { + Schema string `mapstructure:"schema"` + URL string `mapstructure:"url"` + User string `mapstructure:"user"` + Token string `mapstructure:"token"` + Number string `mapstructure:"number"` + DefaultParameters []map[string]interface{} `mapstructure:"default_parameters"` +} + +type Config struct { + Gradle Gradle + Jenkins Jenkins + Jobs []string `mapstructure:"jobs"` +} + +func LoadConfig(path string) (*Config, error) { + v := viper.New() + v.SetConfigName("config") + v.SetConfigType("yaml") + v.AddConfigPath(path) + + if err := v.ReadInConfig(); err != nil { + log.Fatal("[config] Error reading config file:", err) + return nil, err + } + + var config Config + if err := v.Unmarshal(&config); err != nil { + log.Fatal("[config] Unable to decode config into struct:", err) + return nil, err + } + + return &config, nil +} diff --git a/v2/config.yaml b/v2/config.yaml new file mode 100644 index 0000000..c96fb2e --- /dev/null +++ b/v2/config.yaml @@ -0,0 +1,16 @@ +gradle: + caches: + - /home/caches + +jenkins: + schema: https + url: jenkins-ops.shasoapp.com + user: admin + token: 1234567890 + number: lastBuild + default_parameters: + - only_build: true + +jobs: + - echo-rework + diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..4480fe1 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,21 @@ +module jenkins-cron + +go 1.21.1 + +require ( + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // 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/sys v0.29.0 // indirect + golang.org/x/text v0.21.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/v2/go.sum b/v2/go.sum new file mode 100644 index 0000000..51edcb7 --- /dev/null +++ b/v2/go.sum @@ -0,0 +1,36 @@ +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/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +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/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.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= +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/v2/jenkins.go b/v2/jenkins.go new file mode 100644 index 0000000..8ea5843 --- /dev/null +++ b/v2/jenkins.go @@ -0,0 +1,155 @@ +package main + +import ( + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "net/url" + "path" + "strings" + "time" +) + +type Build struct { + Actions []Action `json:"actions"` + Number int `json:"number"` + URL string `json:"url"` +} + +type Action struct { + Class string `json:"_class"` + Parameters []Parameter `json:"parameters,omitempty"` +} + +type Parameter struct { + Name string `json:"name"` + Value interface{} `json:"value"` +} + +type JenkinsClient struct { + Client *http.Client + Config *Jenkins +} + +func NewJenkinsClient(cfg *Jenkins) *JenkinsClient { + return &JenkinsClient{ + Client: &http.Client{Timeout: 10 * time.Second}, + Config: cfg, + } +} + +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, + ) +} + +func (c *JenkinsClient) FetchBuild(job string) (*Build, error) { + api := c.buildAPIURL(job) + + req, err := http.NewRequest("GET", api, nil) + if err != nil { + log.Printf("[jenkins] Failed to create GET request %s: %v", job, err) + return nil, err + } + req.SetBasicAuth(c.Config.User, c.Config.Token) + + resp, err := c.Client.Do(req) + if err != nil { + log.Printf("[jenkins] Failed to fetch build %s: %v", job, err) + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + log.Printf("[jenkins] Failed to fetch build %s: %s", job, resp.Status) + return nil, fmt.Errorf("unexpected status: %s", resp.Status) + } + + // io限制,1m + body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20)) + if err != nil { + log.Printf("[jenkins] Failed to read response body: %v", err) + return nil, fmt.Errorf("failed to read response body: %w", err) + } + + var build Build + if err := json.Unmarshal(body, &build); err != nil { + log.Printf("[jenkins] Failed to unmarshal build %s: %v", job, err) + return nil, fmt.Errorf("failed to unmarshal response body: %w", err) + } + + return &build, nil +} + +// 构建构建触发 URL(带参数) +func (c *JenkinsClient) GenerateBuildURL(build *Build) string { + values := url.Values{} + + // 取出构建参数 + for _, action := range build.Actions { + if action.Class == "hudson.model.ParametersAction" { + for _, param := range action.Parameters { + values.Set(param.Name, fmt.Sprintf("%v", param.Value)) + } + } + } + + // 加入默认参数 + for _, param := range c.Config.DefaultParameters { + for k, v := range param { + values.Set(k, fmt.Sprintf("%v", v)) + } + } + + // 构建新的构建路径 + u, err := url.Parse(build.URL) + if err != nil { + log.Printf("[jenkins] Failed to parse build URL: %v", err) + return "" + } + + u.Path = getBuildWithParametersPath(u.Path) + + u.RawQuery = values.Encode() + return u.String() +} + +// 替换为 buildWithParameters 路径 +func getBuildWithParametersPath(p string) string { + parts := strings.Split(strings.TrimSuffix(p, "/"), "/") + if len(parts) > 0 { + return path.Join(parts[:len(parts)-1]...) + "/buildWithParameters" + } + return p +} + +// 触发构建 +func (c *JenkinsClient) TriggerBuild(build *Build) error { + triggerURL := c.GenerateBuildURL(build) + if triggerURL == "" { + return fmt.Errorf("failed to generate trigger URL") + } + + req, err := http.NewRequest("POST", triggerURL, nil) + if err != nil { + log.Printf("[jenkins] Failed to create POST request: %v", err) + return fmt.Errorf("created trigger request failed: %w", err) + } + req.SetBasicAuth(c.Config.User, c.Config.Token) + + resp, err := c.Client.Do(req) + if err != nil { + log.Printf("[jenkins] Failed to trigger build %s: %v", triggerURL, err) + return fmt.Errorf("trigger request failed: %w", err) + } + defer resp.Body.Close() + + log.Printf("[jenkins] Triggered build %s: %s", triggerURL, resp.Status) + return nil +} diff --git a/v2/main.go b/v2/main.go new file mode 100644 index 0000000..755ddff --- /dev/null +++ b/v2/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "log" +) + +func main() { + cfg, _ := LoadConfig("/app") + + if len(cfg.Jobs) == 0 { + log.Printf("[main] No jobs configured") + return + } + + ctx := context.Background() + stats := &RemoveStats{} + + for _, cache := range cfg.Gradle.Caches { + err := Remove(ctx, cache, RemoveOptions{ + Workers: 4, + Retries: 3, + Stats: stats, + Logger: log.Printf, + Progress: func(current int64) { log.Printf("[main] Removed %d files", current) }, + }) + if err != nil { + log.Printf("[main] Error removing cache %s: %s", cache, err) + } + log.Printf("Summary: Total=%d Success=%d Failed=%d", stats.Total, stats.Success, stats.Failed) + } + + jc := NewJenkinsClient(&cfg.Jenkins) + + for _, job := range cfg.Jobs { + log.Printf("[main] Triggering build for job %s", job) + build, err := jc.FetchBuild(job) + if err != nil { + log.Printf("[main] Error fetching build for job %s: %s", job, err) + return + } + + if err := jc.TriggerBuild(build); err != nil { + log.Printf("[main] Error triggering build for job %s: %s", job, err) + } + } +} +