From d1b4c5902a34fbf474d5d91b7239625f466f5d86 Mon Sep 17 00:00:00 2001 From: sxwebdev Date: Mon, 17 Mar 2025 00:06:17 +0300 Subject: [PATCH 1/4] add deploy via ssh and deploy alerts --- README.md | 186 ++++++++++++++++++++++++++++++----- cmd/gcx/main.go | 257 +++++++++++++++++++++++++++++++++++++++++++----- gcx.yaml | 91 +++++++++++++++++ go.mod | 19 ++-- go.sum | 98 +++++++++++++++--- 5 files changed, 582 insertions(+), 69 deletions(-) create mode 100644 gcx.yaml diff --git a/README.md b/README.md index 1f530b9..658a8ef 100644 --- a/README.md +++ b/README.md @@ -5,10 +5,13 @@ ## Features - **Cross-compilation:** Build Go binaries for multiple OS/architecture combinations. -- **Automated publishing:** Upload build artifacts to S3 (including self-hosted endpoints). -- **Configuration driven:** Use a YAML config file (`.gcx.yaml`) to define build, archive, and publish settings. +- **Automated publishing:** Upload build artifacts to S3 (including self-hosted endpoints) or SSH. +- **Configuration driven:** Use a YAML config file (`gcx.yaml`) to define build, archive, and publish settings. - **Versioning:** Automatically determine the version using the current Git tag. - **CI/CD friendly:** Easily integrate with CI pipelines (e.g., GitLab CI). +- **Hooks system:** Execute commands before and after build process. +- **Archiving:** Create archives (tar.gz) of your binaries with customizable naming. +- **Deployment:** Deploy your artifacts to servers via SSH with custom commands. ## Installation @@ -28,61 +31,172 @@ docker pull sxwebdev/gcx:latest ## Configuration -Create a YAML configuration file named `.gcx.yaml` in your project root. An example configuration: +Create a YAML configuration file named `gcx.yaml` in your project root. An example configuration: ```yaml version: 1 - out_dir: dist +# Pre-build hooks before: hooks: - go mod tidy +# Post-build hooks after: hooks: - - ./binary version + - echo "Build completed!" + - ./scripts/notify-telegram.sh "New build ready!" +# Build configuration builds: - main: ./cmd/myapp env: - CGO_ENABLED=0 goos: - linux + - darwin goarch: - amd64 + - arm64 flags: - -trimpath ldflags: - - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} +# Archive configuration archives: - formats: ["tar.gz"] name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" +# Artifact publishing configuration blobs: - provider: s3 bucket: your-bucket-name directory: "releases/{{.ProjectID}}/{{.Version}}" region: us-west-1 endpoint: https://s3.example.com + + - provider: ssh + server: "storage.example.com" + user: "deployer" + key_path: "~/.ssh/deploy_key" + directory: "/var/www/releases/{{.ProjectID}}/{{.Version}}" + +# Deployment configuration +deploys: + - name: "production" + provider: "ssh" + server: "prod.example.com" + user: "deployer" + key_path: "~/.ssh/deploy_key" + commands: + - systemctl stop myapp + - cp /var/www/releases/myapp/latest/myapp /usr/local/bin/ + - chmod +x /usr/local/bin/myapp + - systemctl start myapp + alerts: + urls: + - "telegram://token@telegram?channels=channel-1" + - "slack://token-a/token-b/token-c" + - "discord://token@channel" + - "teams://token-a/token-b/token-c" + + - name: "staging" + provider: "ssh" + server: "staging.example.com" + user: "deployer" + key_path: "~/.ssh/deploy_key" + commands: + - docker-compose -f /opt/myapp/docker-compose.yml down + - cp /var/www/releases/myapp/latest/myapp /opt/myapp/ + - docker-compose -f /opt/myapp/docker-compose.yml up -d + alerts: + urls: + - "telegram://token@telegram?channels=staging-alerts" + - "slack://token-a/token-b/token-c" ``` ### Template Variables -- **out_dir:** Sets the output directory for build artifacts (default is `dist`). -- **Version:** Automatically set from the current Git tag. If no tag is found, defaults to `0.0.0` (with a log message). -- **ProjectID:** If the `PROJECT_ID` environment variable is not set, the tool uses the name of the current working directory. +Available in various template strings throughout the configuration: + +- **Version:** Current Git tag (defaults to `0.0.0` if no tag found) +- **Binary:** Name of the binary being built +- **Os:** Target operating system +- **Arch:** Target architecture +- **ProjectID:** Project identifier (from env or directory name) ## Environment Variables Set the following environment variables (either in your system or in a `.env` file): -- `AWS_ACCESS_KEY_ID` - Your AWS access key. -- `AWS_SECRET_ACCESS_KEY` - Your AWS secret key. +- `AWS_ACCESS_KEY_ID` - Your AWS access key (for S3 provider) +- `AWS_SECRET_ACCESS_KEY` - Your AWS secret key (for S3 provider) - `PROJECT_ID` (optional) - Your project identifier. If not provided, the current directory name is used. -You can also set additional variables required for your build or publish process. +## Alerts Configuration + +The tool supports sending deployment status notifications using [shoutrrr](https://containrrr.dev/shoutrrr/). You can configure alerts for each deployment to notify different channels about success or failure of the deployment. + +### Supported Services + +- Telegram +- Slack +- Discord +- Microsoft Teams +- And many more (see [shoutrrr services](https://containrrr.dev/shoutrrr/services/overview/)) + +### URL Formats + +Here are examples of URL formats for different services: + +```yaml +alerts: + urls: + # Telegram + - "telegram://token@telegram?channels=channel-1,channel-2" + + # Slack + - "slack://token-a/token-b/token-c" + + # Discord + - "discord://token@channel" + + # Microsoft Teams + - "teams://token-a/token-b/token-c" + + # Generic Webhook + - "generic://example.com/webhook?token=token" +``` + +### Alert Message Format + +The alert message includes: + +- Application name (from deploy configuration) +- Version (current Git tag) +- Deployment status (Success/Failed) +- Error details (in case of failure) + +Example success message: + +```text +Deployment Status Update +Application: myapp-production +Version: v1.2.3 +Status: Success +``` + +Example failure message: + +```text +Deployment Status Update +Application: myapp-production +Version: v1.2.3 +Status: Failed +Error: command 'systemctl start myapp' failed: exit status 1 +``` ## CLI Usage @@ -91,18 +205,36 @@ Once installed, you can run the following commands: - **Build binaries:** ```bash - gcx build --config .gcx.yaml + gcx build --config gcx.yaml ``` - This command runs any pre-build hooks (e.g., `go mod tidy`) and compiles binaries for the specified targets, storing them in the `dist/` directory. + This command: + + 1. Runs pre-build hooks + 2. Compiles binaries for specified targets + 3. Creates archives if configured + 4. Runs post-build hooks + 5. Stores results in the output directory - **Publish artifacts:** ```bash - gcx publish --config .gcx.yaml + gcx publish --config gcx.yaml + ``` + + Uploads all files from the output directory to configured destinations (S3 or SSH). + +- **Deploy:** + + ```bash + # Deploy all configurations + gcx deploy --config gcx.yaml + + # Deploy specific configuration + gcx deploy --config gcx.yaml --name production ``` - This command uploads all files from the `dist/` directory to the configured S3 bucket using the specified settings. + Executes deployment commands on target servers via SSH. - **Show version:** @@ -110,21 +242,18 @@ Once installed, you can run the following commands: gcx version ``` - This prints the current version, commit, and build date of the tool. - ## GitLab CI/CD Integration Example -If the `gcx` image is available on Docker Hub, you can integrate it into your GitLab CI pipeline as follows: - ```yaml image: sxwebdev/gcx:latest stages: - build - publish + - deploy variables: - GCX_CONFIG: .gcx.yaml + GCX_CONFIG: gcx.yaml build: stage: build @@ -140,13 +269,22 @@ publish: - gcx publish --config $GCX_CONFIG only: - tags + +deploy: + stage: deploy + script: + - gcx deploy --config $GCX_CONFIG --name production + only: + - tags + when: manual ``` In this pipeline: -- The `build` stage compiles binaries and stores them in `dist/`. -- The `publish` stage (triggered only when a Git tag is created) uploads the artifacts to S3. -- Ensure that all necessary environment variables (AWS credentials, etc.) are set in your GitLab CI/CD settings. +- The `build` stage compiles binaries, creates archives, and stores them in `dist/` +- The `publish` stage uploads artifacts to configured destinations +- The `deploy` stage (manual trigger) deploys the application to production +- Ensure all necessary environment variables are set in your GitLab CI/CD settings ## License diff --git a/cmd/gcx/main.go b/cmd/gcx/main.go index 5178a0b..30a637f 100644 --- a/cmd/gcx/main.go +++ b/cmd/gcx/main.go @@ -15,6 +15,7 @@ import ( "text/template" "time" + "github.com/containrrr/shoutrrr" "github.com/joho/godotenv" "github.com/melbahja/goph" "github.com/minio/minio-go/v7" @@ -39,6 +40,7 @@ type Config struct { Builds []BuildConfig `yaml:"builds"` Archives []ArchiveConfig `yaml:"archives"` Blobs []BlobConfig `yaml:"blobs"` + Deploys []DeployConfig `yaml:"deploys"` } type HooksConfig struct { @@ -60,7 +62,7 @@ type ArchiveConfig struct { NameTemplate string `yaml:"name_template"` } -// ArchiveTemplateData содержит данные для шаблона имени архива +// ArchiveTemplateData contains data for archive name template type ArchiveTemplateData struct { Binary string Version string @@ -82,6 +84,31 @@ type BlobConfig struct { Directory string `yaml:"directory"` } +type DeployConfig struct { + Name string `yaml:"name"` + Provider string `yaml:"provider"` + // SSH config fields + Server string `yaml:"server,omitempty"` + User string `yaml:"user,omitempty"` + KeyPath string `yaml:"key_path,omitempty"` + Commands []string `yaml:"commands"` + // Alert configuration + Alerts AlertConfig `yaml:"alerts"` +} + +// AlertConfig contains notification settings +type AlertConfig struct { + URLs []string `yaml:"urls"` // URLs in shoutrrr format +} + +// AlertTemplateData contains data for message template +type AlertTemplateData struct { + AppName string + Version string + Status string + Error string +} + // ToS3Config converts BlobConfig to S3Config if provider is s3 func (c *BlobConfig) ToS3Config() *S3Config { if c.Provider != "s3" { @@ -108,6 +135,20 @@ func (c *BlobConfig) ToSSHConfig() *SSHConfig { } } +// ToSSHDeployConfig converts DeployConfig to SSHDeployConfig if provider is ssh +func (c *DeployConfig) ToSSHDeployConfig() *SSHDeployConfig { + if c.Provider != "ssh" { + return nil + } + return &SSHDeployConfig{ + Name: c.Name, + Server: c.Server, + User: c.User, + KeyPath: c.KeyPath, + Commands: c.Commands, + } +} + // Internal config types for type safety type S3Config struct { Bucket string @@ -123,6 +164,15 @@ type SSHConfig struct { Directory string } +// Internal config types for type safety +type SSHDeployConfig struct { + Name string + Server string + User string + KeyPath string + Commands []string +} + // loadConfig reads the YAML configuration from the specified file. func loadConfig(configPath string) (*Config, error) { data, err := os.ReadFile(configPath) @@ -450,28 +500,28 @@ func publishToSSH(cfg *SSHConfig, artifactsDir string, tmplData map[string]strin return nil } -// createArchives создает архивы для всех собранных бинарных файлов +// createArchives creates archives for all built binaries func createArchives(cfg *Config, artifactsDir string) error { if len(cfg.Archives) == 0 { return nil } - // Получаем текущую версию + // Get current version version := getGitTag() - // Читаем все файлы в директории артефактов + // Read all files in artifacts directory files, err := os.ReadDir(artifactsDir) if err != nil { return fmt.Errorf("failed to read artifacts directory: %v", err) } - // Для каждого файла создаем архивы согласно конфигурации + // Create archives for each file according to configuration for _, file := range files { if file.IsDir() { continue } - // Парсим имя файла для получения информации о платформе + // Parse filename to get platform information fileName := file.Name() parts := strings.Split(fileName, "_") if len(parts) < 3 { @@ -482,7 +532,7 @@ func createArchives(cfg *Config, artifactsDir string) error { os := parts[1] arch := parts[2] - // Данные для шаблона + // Template data tmplData := ArchiveTemplateData{ Binary: binary, Version: version, @@ -490,9 +540,9 @@ func createArchives(cfg *Config, artifactsDir string) error { Arch: arch, } - // Для каждой конфигурации архива + // For each archive configuration for _, archive := range cfg.Archives { - // Создаем имя архива из шаблона + // Create archive name from template tmpl, err := template.New("archive").Parse(archive.NameTemplate) if err != nil { return fmt.Errorf("failed to parse archive name template: %v", err) @@ -503,7 +553,7 @@ func createArchives(cfg *Config, artifactsDir string) error { return fmt.Errorf("failed to execute archive name template: %v", err) } - // Для каждого формата архива + // For each archive format for _, format := range archive.Formats { archiveName := nameBuffer.String() + "." + format archivePath := filepath.Join(artifactsDir, archiveName) @@ -513,7 +563,7 @@ func createArchives(cfg *Config, artifactsDir string) error { if err := createTarGz(filepath.Join(artifactsDir, fileName), archivePath); err != nil { return fmt.Errorf("failed to create tar.gz archive: %v", err) } - // Здесь можно добавить поддержку других форматов архивов + // Here you can add support for other archive formats default: log.Printf("Unsupported archive format: %s", format) } @@ -524,37 +574,37 @@ func createArchives(cfg *Config, artifactsDir string) error { return nil } -// createTarGz создает tar.gz архив из файла +// createTarGz creates a tar.gz archive from a file func createTarGz(srcFile, destFile string) error { - // Создаем файл архива + // Create archive file archive, err := os.Create(destFile) if err != nil { return fmt.Errorf("failed to create archive file: %v", err) } defer archive.Close() - // Создаем gzip writer + // Create gzip writer gw := gzip.NewWriter(archive) defer gw.Close() - // Создаем tar writer + // Create tar writer tw := tar.NewWriter(gw) defer tw.Close() - // Открываем исходный файл + // Open source file file, err := os.Open(srcFile) if err != nil { return fmt.Errorf("failed to open source file: %v", err) } defer file.Close() - // Получаем информацию о файле + // Get file info stat, err := file.Stat() if err != nil { return fmt.Errorf("failed to get file info: %v", err) } - // Создаем заголовок tar + // Create tar header header := &tar.Header{ Name: filepath.Base(srcFile), Size: stat.Size(), @@ -562,12 +612,12 @@ func createTarGz(srcFile, destFile string) error { ModTime: stat.ModTime(), } - // Записываем заголовок + // Write header if err := tw.WriteHeader(header); err != nil { return fmt.Errorf("failed to write tar header: %v", err) } - // Копируем содержимое файла в архив + // Copy file contents to archive if _, err := io.Copy(tw, file); err != nil { return fmt.Errorf("failed to write file to tar: %v", err) } @@ -575,6 +625,144 @@ func createTarGz(srcFile, destFile string) error { return nil } +// deployArtifacts executes deployment according to the configuration +func deployArtifacts(cfg *Config, deployName string) error { + if len(cfg.Deploys) == 0 { + return fmt.Errorf("no deploy configurations found") + } + + // If deployName is specified, execute only that deploy + if deployName != "" { + for _, deploy := range cfg.Deploys { + if deploy.Name == deployName { + return executeDeploy(&deploy) + } + } + return fmt.Errorf("deploy configuration '%s' not found", deployName) + } + + // Execute all deploys + for _, deploy := range cfg.Deploys { + if err := executeDeploy(&deploy); err != nil { + return fmt.Errorf("deploy '%s' failed: %v", deploy.Name, err) + } + } + + return nil +} + +// sendAlert sends notification through shoutrrr +func sendAlert(urls []string, tmplData AlertTemplateData) error { + if len(urls) == 0 { + return nil + } + + // Create message template + const msgTemplate = ` +Deployment Status Update +Application: {{.AppName}} +Version: {{.Version}} +Status: {{.Status}} +{{if .Error}}Error: {{.Error}}{{end}} +` + + tmpl, err := template.New("alert").Parse(msgTemplate) + if err != nil { + return fmt.Errorf("failed to parse alert template: %v", err) + } + + var msgBuffer strings.Builder + if err := tmpl.Execute(&msgBuffer, tmplData); err != nil { + return fmt.Errorf("failed to execute alert template: %v", err) + } + + // Create sender for all URLs + sender, err := shoutrrr.CreateSender(urls...) + if err != nil { + return fmt.Errorf("failed to create alert sender: %v", err) + } + + // Send notification + errs := sender.Send(msgBuffer.String(), nil) + if len(errs) > 0 { + return fmt.Errorf("failed to send alerts: %v", errs) + } + + return nil +} + +// executeDeploy executes a single deployment configuration +func executeDeploy(deploy *DeployConfig) error { + log.Printf("Executing deploy: %s", deploy.Name) + + // Get current version for notifications + version := getGitTag() + + // Prepare notification data + alertData := AlertTemplateData{ + AppName: deploy.Name, + Version: version, + } + + var deployErr error + switch deploy.Provider { + case "ssh": + deployErr = executeSSHDeploy(deploy.ToSSHDeployConfig()) + default: + deployErr = fmt.Errorf("unsupported deploy provider: %s", deploy.Provider) + } + + // Send notification based on result + if deployErr != nil { + alertData.Status = "Failed" + alertData.Error = deployErr.Error() + // Send error notification + if err := sendAlert(deploy.Alerts.URLs, alertData); err != nil { + log.Printf("Failed to send failure alert: %v", err) + } + return deployErr + } + + // Send success notification + alertData.Status = "Success" + if err := sendAlert(deploy.Alerts.URLs, alertData); err != nil { + log.Printf("Failed to send success alert: %v", err) + } + + return nil +} + +// executeSSHDeploy executes deployment commands over SSH +func executeSSHDeploy(cfg *SSHDeployConfig) error { + if cfg == nil { + return fmt.Errorf("ssh configuration is required for ssh provider") + } + + // Create SSH client + auth, err := goph.Key(cfg.KeyPath, "") + if err != nil { + return fmt.Errorf("failed to load SSH key: %v", err) + } + + client, err := goph.New(cfg.User, cfg.Server, auth) + if err != nil { + return fmt.Errorf("failed to create SSH client: %v", err) + } + defer client.Close() + + // Execute each command + for _, cmd := range cfg.Commands { + log.Printf("Executing command: %s", cmd) + out, err := client.Run(cmd) + if err != nil { + return fmt.Errorf("command '%s' failed: %v", cmd, err) + } + log.Printf("Command output:\n%s", string(out)) + } + + return nil +} + func main() { // Load environment variables from .env file, if it exists. godotenv.Load() @@ -591,7 +779,7 @@ func main() { Name: "config", Aliases: []string{"c"}, Usage: "Path to the YAML configuration file", - Value: ".gcx.yaml", + Value: "gcx.yaml", }, }, Action: func(c *cli.Context) error { @@ -611,7 +799,7 @@ func main() { Name: "config", Aliases: []string{"c"}, Usage: "Path to the YAML configuration file", - Value: ".gcx.yaml", + Value: "gcx.yaml", }, }, Action: func(c *cli.Context) error { @@ -623,6 +811,31 @@ func main() { return publishArtifacts(cfg) }, }, + { + Name: "deploy", + Usage: "Deploys artifacts based on the configuration", + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "config", + Aliases: []string{"c"}, + Usage: "Path to the YAML configuration file", + Value: "gcx.yaml", + }, + &cli.StringFlag{ + Name: "name", + Aliases: []string{"n"}, + Usage: "Name of the deploy configuration to execute", + }, + }, + Action: func(c *cli.Context) error { + configPath := c.String("config") + cfg, err := loadConfig(configPath) + if err != nil { + return fmt.Errorf("error loading configuration: %v", err) + } + return deployArtifacts(cfg, c.String("name")) + }, + }, { Name: "version", Usage: "Displays the current version", diff --git a/gcx.yaml b/gcx.yaml new file mode 100644 index 0000000..2a7a980 --- /dev/null +++ b/gcx.yaml @@ -0,0 +1,91 @@ +version: 1 +out_dir: "dist" + +# Hooks executed before build +before: + hooks: + - go mod tidy + - go generate ./... + +# Hooks executed after build +after: + hooks: + - echo "Build completed!" + - ./scripts/notify-telegram.sh "New build ready!" + +# Build configuration +builds: + - main: ./cmd/myapp + goos: + - linux + - darwin + goarch: + - amd64 + - arm64 + ldflags: + - "-X main.version={{.Version}}" + - "-X main.commit={{.Commit}}" + env: + - CGO_ENABLED=0 + +# Archive configuration +archives: + - formats: ["tar.gz"] + name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + +# Artifact publishing configuration +blobs: + - provider: s3 + bucket: "my-releases" + directory: "{{.ProjectID}}/{{.Version}}" + region: "us-east-1" + endpoint: "https://s3.amazonaws.com" + + - provider: ssh + server: "storage.example.com" + user: "deployer" + key_path: "~/.ssh/deploy_key" + directory: "/var/www/releases/{{.ProjectID}}/{{.Version}}" + +# Deploy configuration +deploys: + - name: "production" + provider: "ssh" + server: "prod.example.com" + user: "deployer" + key_path: "~/.ssh/deploy_key" + commands: + - systemctl stop myapp + - cp /var/www/releases/myapp/latest/myapp /usr/local/bin/ + - chmod +x /usr/local/bin/myapp + - systemctl start myapp + - systemctl status myapp + # Alert configuration for production + alerts: + urls: + # Main Telegram channel for the team + - "telegram://123456789:AABBCCDDEEFFaabbccddee@telegram?channels=myapp-alerts" + # Backup Slack channel + - "slack://xoxb-123456789012-1234567890123-abcdefghijklmnopqrstuvwx/general" + # Discord notifications for monitoring + - "discord://123456789012345678/abcdefghijklmnopqrstuvwxyz1234567890" + # Microsoft Teams channel + - "teams://group1/tenant2/webhook3" + + - name: "staging" + provider: "ssh" + server: "staging.example.com" + user: "deployer" + key_path: "~/.ssh/deploy_key" + commands: + - docker-compose -f /opt/myapp/docker-compose.yml down + - cp /var/www/releases/myapp/latest/myapp /opt/myapp/ + - docker-compose -f /opt/myapp/docker-compose.yml up -d + - docker-compose -f /opt/myapp/docker-compose.yml ps + # Alert configuration for staging + alerts: + urls: + # Telegram channel for test environment + - "telegram://123456789:AABBCCDDEEFFaabbccddee@telegram?channels=myapp-staging" + # Webhook for external monitoring system integration + - "generic://monitoring.example.com/webhook?token=your-token-here" diff --git a/go.mod b/go.mod index 81f17a3..bdcf1c5 100644 --- a/go.mod +++ b/go.mod @@ -3,32 +3,35 @@ module gcx go 1.23.0 require ( + github.com/containrrr/shoutrrr v0.8.0 github.com/joho/godotenv v1.5.1 github.com/melbahja/goph v1.4.0 - github.com/minio/minio-go/v7 v7.0.87 - github.com/urfave/cli/v2 v2.27.5 + github.com/minio/minio-go/v7 v7.0.88 + github.com/urfave/cli/v2 v2.27.6 gopkg.in/yaml.v2 v2.4.0 ) require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/fs v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/minio/crc64nvme v1.0.1 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pkg/sftp v1.13.5 // indirect + github.com/pkg/sftp v1.13.8 // indirect github.com/rs/xid v1.6.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect - golang.org/x/crypto v0.35.0 // indirect - golang.org/x/net v0.35.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect ) diff --git a/go.sum b/go.sum index bc3769c..aca0226 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/containrrr/shoutrrr v0.8.0 h1:mfG2ATzIS7NR2Ec6XL+xyoHzN97H8WPjir8aYzJUSec= +github.com/containrrr/shoutrrr v0.8.0/go.mod h1:ioyQAyu1LJY6sILuNyKaQaw+9Ttik5QePU8atnAdO2o= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -5,12 +7,26 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE= +github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc= +github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -20,18 +36,27 @@ github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2 github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/melbahja/goph v1.4.0 h1:z0PgDbBFe66lRYl3v5dGb9aFgPy0kotuQ37QOwSQFqs= github.com/melbahja/goph v1.4.0/go.mod h1:uG+VfK2Dlhk+O32zFrRlc3kYKTlV6+BtvPWd/kK7U68= github.com/minio/crc64nvme v1.0.1 h1:DHQPrYPdqK7jQG/Ls5CTBZWeex/2FMS3G5XGkycuFrY= github.com/minio/crc64nvme v1.0.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg= github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34= github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM= -github.com/minio/minio-go/v7 v7.0.87 h1:nkr9x0u53PespfxfUqxP3UYWiE2a41gaofgNnC4Y8WQ= -github.com/minio/minio-go/v7 v7.0.87/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= +github.com/minio/minio-go/v7 v7.0.88 h1:v8MoIJjwYxOkehp+eiLIuvXk87P2raUtoU5klrAAshs= +github.com/minio/minio-go/v7 v7.0.88/go.mod h1:33+O8h0tO7pCeCWwBVa07RhVVfB/3vS4kEX7rwYKmIg= +github.com/onsi/ginkgo/v2 v2.9.2 h1:BA2GMJOtfGAfagzYtrAlufIP0lq6QERkFmHLMLPwFSU= +github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxeMeDuts= +github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= +github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= +github.com/pkg/sftp v1.13.8 h1:Xt7eJ/xqXv7s0VuzFw7JXhZj6Oc1zI6l4GK8KP9sFB0= +github.com/pkg/sftp v1.13.8/go.mod h1:DmvEkvKE2lshEeuo2JMp06yqcx9HVnR7e3zqQl42F3U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= @@ -39,11 +64,14 @@ github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= -github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +github.com/urfave/cli/v2 v2.27.6 h1:VdRdS98FNhKZ8/Az8B7MTyGQmpIr36O1EHybx/LaZ4g= +github.com/urfave/cli/v2 v2.27.6/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -51,18 +79,35 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= -golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= -golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -71,24 +116,47 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= -golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= +golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= From 0822dbe2331fa65390e06ff264fbd2e47a212517 Mon Sep 17 00:00:00 2001 From: sxwebdev Date: Mon, 17 Mar 2025 00:09:19 +0300 Subject: [PATCH 2/4] update gcx --- gcx.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gcx.yaml b/gcx.yaml index 2a7a980..152b1de 100644 --- a/gcx.yaml +++ b/gcx.yaml @@ -64,9 +64,9 @@ deploys: alerts: urls: # Main Telegram channel for the team - - "telegram://123456789:AABBCCDDEEFFaabbccddee@telegram?channels=myapp-alerts" + - "telegram://chatID:token@telegram?channels=myapp-alerts" # Backup Slack channel - - "slack://xoxb-123456789012-1234567890123-abcdefghijklmnopqrstuvwx/general" + - "slack://token/general" # Discord notifications for monitoring - "discord://123456789012345678/abcdefghijklmnopqrstuvwxyz1234567890" # Microsoft Teams channel @@ -86,6 +86,6 @@ deploys: alerts: urls: # Telegram channel for test environment - - "telegram://123456789:AABBCCDDEEFFaabbccddee@telegram?channels=myapp-staging" + - "telegram://123456789:token@telegram?channels=myapp-staging" # Webhook for external monitoring system integration - "generic://monitoring.example.com/webhook?token=your-token-here" From be2266ddf30601a18be9aaf7d75b5478488d7686 Mon Sep 17 00:00:00 2001 From: sxwebdev Date: Mon, 17 Mar 2025 00:13:32 +0300 Subject: [PATCH 3/4] update readme --- README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 658a8ef..2edcd61 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,15 @@ ## Features -- **Cross-compilation:** Build Go binaries for multiple OS/architecture combinations. -- **Automated publishing:** Upload build artifacts to S3 (including self-hosted endpoints) or SSH. -- **Configuration driven:** Use a YAML config file (`gcx.yaml`) to define build, archive, and publish settings. -- **Versioning:** Automatically determine the version using the current Git tag. -- **CI/CD friendly:** Easily integrate with CI pipelines (e.g., GitLab CI). -- **Hooks system:** Execute commands before and after build process. -- **Archiving:** Create archives (tar.gz) of your binaries with customizable naming. -- **Deployment:** Deploy your artifacts to servers via SSH with custom commands. +- 🔨 **Cross-compilation:** Build Go binaries for multiple OS/architecture combinations. +- 🚀 **Automated publishing:** Upload build artifacts to S3 (including self-hosted endpoints) or SSH. +- ⚙️ **Configuration driven:** Use a YAML config file (`gcx.yaml`) to define build, archive, and publish settings. +- 🏷️ **Versioning:** Automatically determine the version using the current Git tag. +- 🔄 **CI/CD friendly:** Easily integrate with CI pipelines (e.g., GitLab CI). +- 🎣 **Hooks system:** Execute commands before and after build process. +- 📦 **Archiving:** Create archives (tar.gz) of your binaries with customizable naming. +- 🚢 **Deployment:** Deploy your artifacts to servers via SSH with custom commands. +- 🔔 **Notifications:** Send deployment status alerts to multiple channels (Telegram, Slack, Discord, Teams) using Shoutrrr. ## Installation From 94b01e279c827b86682e70f3893baab62bbf9d13 Mon Sep 17 00:00:00 2001 From: sxwebdev Date: Mon, 17 Mar 2025 00:23:39 +0300 Subject: [PATCH 4/4] fix errors --- cmd/gcx/main.go | 97 +++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/cmd/gcx/main.go b/cmd/gcx/main.go index 30a637f..411b810 100644 --- a/cmd/gcx/main.go +++ b/cmd/gcx/main.go @@ -177,11 +177,11 @@ type SSHDeployConfig struct { func loadConfig(configPath string) (*Config, error) { data, err := os.ReadFile(configPath) if err != nil { - return nil, err + return nil, fmt.Errorf("error loading configuration: %w", err) } var cfg Config if err = yaml.Unmarshal(data, &cfg); err != nil { - return nil, err + return nil, fmt.Errorf("error parsing configuration: %w", err) } return &cfg, nil } @@ -198,7 +198,7 @@ func runHooks(hooks []string) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("error executing hook '%s': %v", hook, err) + return fmt.Errorf("error executing hook '%s': %w", hook, err) } } return nil @@ -238,7 +238,7 @@ func buildBinaries(cfg *Config) error { // Create the build directory if err := os.MkdirAll(outDir, 0o755); err != nil { - return err + return fmt.Errorf("failed to create output directory: %w", err) } // For each build configuration @@ -273,7 +273,7 @@ func buildBinaries(cfg *Config) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("build error: %v", err) + return fmt.Errorf("build error: %w", err) } } } else { @@ -293,7 +293,7 @@ func buildBinaries(cfg *Config) error { cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { - return fmt.Errorf("build error: %v", err) + return fmt.Errorf("build error: %w", err) } } } @@ -302,7 +302,7 @@ func buildBinaries(cfg *Config) error { // Create archives after successful build if err := createArchives(cfg, outDir); err != nil { - return fmt.Errorf("failed to create archives: %v", err) + return fmt.Errorf("failed to create archives: %w", err) } // Execute after hooks @@ -315,7 +315,7 @@ func buildBinaries(cfg *Config) error { return nil } -// publishArtifacts uploads artifacts (from the output directory) to S3 according to the configuration. +// publishArtifacts uploads artifacts to configured destinations func publishArtifacts(cfg *Config) error { // Determine the artifacts directory (default is "dist") artifactsDir := cfg.OutDir @@ -348,11 +348,11 @@ func publishArtifacts(cfg *Config) error { switch blob.Provider { case "s3": if err := publishToS3(blob.ToS3Config(), artifactsDir, tmplData); err != nil { - return fmt.Errorf("s3 publish error: %v", err) + return fmt.Errorf("s3 publish error: %w", err) } case "ssh": if err := publishToSSH(blob.ToSSHConfig(), artifactsDir, tmplData); err != nil { - return fmt.Errorf("ssh publish error: %v", err) + return fmt.Errorf("ssh publish error: %w", err) } default: log.Printf("Skipping unknown provider: %s", blob.Provider) @@ -377,18 +377,18 @@ func publishToS3(cfg *S3Config, artifactsDir string, tmplData map[string]string) // Process template for the publish directory tmpl, err := template.New("directory").Parse(cfg.Directory) if err != nil { - return fmt.Errorf("error parsing directory template: %v", err) + return fmt.Errorf("error parsing directory template: %w", err) } var dirBuffer strings.Builder if err = tmpl.Execute(&dirBuffer, tmplData); err != nil { - return fmt.Errorf("error executing directory template: %v", err) + return fmt.Errorf("error executing directory template: %w", err) } remoteDir := dirBuffer.String() // Parse endpoint to extract host urlData, err := url.Parse(cfg.Endpoint) if err != nil { - return fmt.Errorf("error parsing endpoint: %v", err) + return fmt.Errorf("error parsing endpoint: %w", err) } // Create an S3 client using minio-go @@ -398,26 +398,26 @@ func publishToS3(cfg *S3Config, artifactsDir string, tmplData map[string]string) Region: cfg.Region, }) if err != nil { - return fmt.Errorf("failed to create S3 client: %v", err) + return fmt.Errorf("failed to create S3 client: %w", err) } // Check if the bucket exists and create it if necessary ctx := context.Background() exists, err := s3Client.BucketExists(ctx, cfg.Bucket) if err != nil { - return fmt.Errorf("bucket check error: %v", err) + return fmt.Errorf("bucket check error: %w", err) } if !exists { log.Printf("Bucket %s does not exist, creating...", cfg.Bucket) if err = s3Client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{Region: cfg.Region}); err != nil { - return fmt.Errorf("failed to create bucket: %v", err) + return fmt.Errorf("failed to create bucket: %w", err) } } // Upload all files from the artifacts directory files, err := os.ReadDir(artifactsDir) if err != nil { - return fmt.Errorf("failed to read directory %s: %v", artifactsDir, err) + return fmt.Errorf("failed to read directory %s: %w", artifactsDir, err) } for _, file := range files { if file.IsDir() { @@ -428,17 +428,17 @@ func publishToS3(cfg *S3Config, artifactsDir string, tmplData map[string]string) log.Printf("Uploading %s to s3://%s/%s", localFilePath, cfg.Bucket, remotePath) f, err := os.Open(localFilePath) if err != nil { - return fmt.Errorf("failed to open file %s: %v", localFilePath, err) + return fmt.Errorf("failed to open file %s: %w", localFilePath, err) } stat, err := f.Stat() if err != nil { f.Close() - return fmt.Errorf("failed to get file info for %s: %v", localFilePath, err) + return fmt.Errorf("failed to get file info for %s: %w", localFilePath, err) } _, err = s3Client.PutObject(ctx, cfg.Bucket, remotePath, f, stat.Size(), minio.PutObjectOptions{}) f.Close() if err != nil { - return fmt.Errorf("failed to upload file %s: %v", localFilePath, err) + return fmt.Errorf("failed to upload file %s: %w", localFilePath, err) } } return nil @@ -453,35 +453,35 @@ func publishToSSH(cfg *SSHConfig, artifactsDir string, tmplData map[string]strin // Process template for the publish directory tmpl, err := template.New("directory").Parse(cfg.Directory) if err != nil { - return fmt.Errorf("error parsing directory template: %v", err) + return fmt.Errorf("error parsing directory template: %w", err) } var dirBuffer strings.Builder if err = tmpl.Execute(&dirBuffer, tmplData); err != nil { - return fmt.Errorf("error executing directory template: %v", err) + return fmt.Errorf("error executing directory template: %w", err) } remoteDir := dirBuffer.String() // Create SSH client auth, err := goph.Key(cfg.KeyPath, "") if err != nil { - return fmt.Errorf("failed to load SSH key: %v", err) + return fmt.Errorf("failed to load SSH key: %w", err) } client, err := goph.New(cfg.User, cfg.Server, auth) if err != nil { - return fmt.Errorf("failed to create SSH client: %v", err) + return fmt.Errorf("failed to create SSH client: %w", err) } defer client.Close() // Create remote directory if it doesn't exist if _, err := client.Run(fmt.Sprintf("mkdir -p %s", remoteDir)); err != nil { - return fmt.Errorf("failed to create remote directory: %v", err) + return fmt.Errorf("failed to create remote directory: %w", err) } // Upload all files from the artifacts directory files, err := os.ReadDir(artifactsDir) if err != nil { - return fmt.Errorf("failed to read directory %s: %v", artifactsDir, err) + return fmt.Errorf("failed to read directory %s: %w", artifactsDir, err) } for _, file := range files { @@ -493,7 +493,7 @@ func publishToSSH(cfg *SSHConfig, artifactsDir string, tmplData map[string]strin log.Printf("Uploading %s to %s:%s", localFilePath, cfg.Server, remotePath) if err := client.Upload(localFilePath, remotePath); err != nil { - return fmt.Errorf("failed to upload file %s: %v", localFilePath, err) + return fmt.Errorf("failed to upload file %s: %w", localFilePath, err) } } @@ -512,7 +512,7 @@ func createArchives(cfg *Config, artifactsDir string) error { // Read all files in artifacts directory files, err := os.ReadDir(artifactsDir) if err != nil { - return fmt.Errorf("failed to read artifacts directory: %v", err) + return fmt.Errorf("failed to read artifacts directory: %w", err) } // Create archives for each file according to configuration @@ -545,12 +545,12 @@ func createArchives(cfg *Config, artifactsDir string) error { // Create archive name from template tmpl, err := template.New("archive").Parse(archive.NameTemplate) if err != nil { - return fmt.Errorf("failed to parse archive name template: %v", err) + return fmt.Errorf("failed to parse archive name template: %w", err) } var nameBuffer strings.Builder if err := tmpl.Execute(&nameBuffer, tmplData); err != nil { - return fmt.Errorf("failed to execute archive name template: %v", err) + return fmt.Errorf("failed to execute archive name template: %w", err) } // For each archive format @@ -561,7 +561,7 @@ func createArchives(cfg *Config, artifactsDir string) error { switch format { case "tar.gz": if err := createTarGz(filepath.Join(artifactsDir, fileName), archivePath); err != nil { - return fmt.Errorf("failed to create tar.gz archive: %v", err) + return fmt.Errorf("failed to create tar.gz archive: %w", err) } // Here you can add support for other archive formats default: @@ -579,7 +579,7 @@ func createTarGz(srcFile, destFile string) error { // Create archive file archive, err := os.Create(destFile) if err != nil { - return fmt.Errorf("failed to create archive file: %v", err) + return fmt.Errorf("failed to create archive file: %w", err) } defer archive.Close() @@ -594,14 +594,14 @@ func createTarGz(srcFile, destFile string) error { // Open source file file, err := os.Open(srcFile) if err != nil { - return fmt.Errorf("failed to open source file: %v", err) + return fmt.Errorf("failed to open source file: %w", err) } defer file.Close() // Get file info stat, err := file.Stat() if err != nil { - return fmt.Errorf("failed to get file info: %v", err) + return fmt.Errorf("failed to get file info: %w", err) } // Create tar header @@ -614,12 +614,12 @@ func createTarGz(srcFile, destFile string) error { // Write header if err := tw.WriteHeader(header); err != nil { - return fmt.Errorf("failed to write tar header: %v", err) + return fmt.Errorf("failed to write tar header: %w", err) } // Copy file contents to archive if _, err := io.Copy(tw, file); err != nil { - return fmt.Errorf("failed to write file to tar: %v", err) + return fmt.Errorf("failed to write file to tar: %w", err) } return nil @@ -644,7 +644,7 @@ func deployArtifacts(cfg *Config, deployName string) error { // Execute all deploys for _, deploy := range cfg.Deploys { if err := executeDeploy(&deploy); err != nil { - return fmt.Errorf("deploy '%s' failed: %v", deploy.Name, err) + return fmt.Errorf("deploy '%s' failed: %w", deploy.Name, err) } } @@ -668,24 +668,27 @@ Status: {{.Status}} tmpl, err := template.New("alert").Parse(msgTemplate) if err != nil { - return fmt.Errorf("failed to parse alert template: %v", err) + return fmt.Errorf("failed to parse alert template: %w", err) } var msgBuffer strings.Builder if err := tmpl.Execute(&msgBuffer, tmplData); err != nil { - return fmt.Errorf("failed to execute alert template: %v", err) + return fmt.Errorf("failed to execute alert template: %w", err) } // Create sender for all URLs sender, err := shoutrrr.CreateSender(urls...) if err != nil { - return fmt.Errorf("failed to create alert sender: %v", err) + return fmt.Errorf("failed to create alert sender: %w", err) } // Send notification errs := sender.Send(msgBuffer.String(), nil) if len(errs) > 0 { - return fmt.Errorf("failed to send alerts: %v", errs) + for _, err := range errs { + log.Printf("failed to send alert: %v", err) + } + return fmt.Errorf("failed to send alerts") } return nil @@ -741,12 +744,12 @@ func executeSSHDeploy(cfg *SSHDeployConfig) error { // Create SSH client auth, err := goph.Key(cfg.KeyPath, "") if err != nil { - return fmt.Errorf("failed to load SSH key: %v", err) + return fmt.Errorf("failed to load SSH key: %w", err) } client, err := goph.New(cfg.User, cfg.Server, auth) if err != nil { - return fmt.Errorf("failed to create SSH client: %v", err) + return fmt.Errorf("failed to create SSH client: %w", err) } defer client.Close() @@ -755,7 +758,7 @@ func executeSSHDeploy(cfg *SSHDeployConfig) error { log.Printf("Executing command: %s", cmd) out, err := client.Run(cmd) if err != nil { - return fmt.Errorf("command '%s' failed: %v", cmd, err) + return fmt.Errorf("command '%s' failed: %w", cmd, err) } log.Printf("Command output:\n%s", string(out)) } @@ -786,7 +789,7 @@ func main() { configPath := c.String("config") cfg, err := loadConfig(configPath) if err != nil { - return fmt.Errorf("error loading configuration: %v", err) + return fmt.Errorf("error loading configuration: %w", err) } return buildBinaries(cfg) }, @@ -806,7 +809,7 @@ func main() { configPath := c.String("config") cfg, err := loadConfig(configPath) if err != nil { - return fmt.Errorf("error loading configuration: %v", err) + return fmt.Errorf("error loading configuration: %w", err) } return publishArtifacts(cfg) }, @@ -831,7 +834,7 @@ func main() { configPath := c.String("config") cfg, err := loadConfig(configPath) if err != nil { - return fmt.Errorf("error loading configuration: %v", err) + return fmt.Errorf("error loading configuration: %w", err) } return deployArtifacts(cfg, c.String("name")) },