From 587ac42be4caf74e0962c4161ff86ae74e4fc331 Mon Sep 17 00:00:00 2001 From: GitCaddy Date: Wed, 14 Jan 2026 07:26:46 +0000 Subject: [PATCH] feat: Rebrand to gitcaddy-runner with upload helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename binary from act_runner to gitcaddy-runner - Update all user-facing strings (Gitea → GitCaddy) - Add gitcaddy-upload helper with automatic retry for large files - Add upload helper package (internal/pkg/artifact) - Update Docker image name to marketally/gitcaddy-runner 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- Makefile | 4 +- cmd/upload-helper/main.go | 38 +++++++ internal/app/cmd/cmd.go | 16 +-- internal/app/cmd/daemon.go | 2 +- internal/app/cmd/exec.go | 2 +- internal/app/cmd/register.go | 4 +- internal/pkg/artifact/upload_helper.go | 145 +++++++++++++++++++++++++ internal/pkg/envcheck/bandwidth.go | 2 +- 8 files changed, 198 insertions(+), 15 deletions(-) create mode 100644 cmd/upload-helper/main.go create mode 100644 internal/pkg/artifact/upload_helper.go diff --git a/Makefile b/Makefile index 448fb10..d1a14c0 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ DIST := dist -EXECUTABLE := act_runner +EXECUTABLE := gitcaddy-runner GOFMT ?= gofumpt -l DIST_DIRS := $(DIST)/binaries $(DIST)/release GO ?= go @@ -15,7 +15,7 @@ WINDOWS_ARCHS ?= windows/amd64 GO_FMT_FILES := $(shell find . -type f -name "*.go" ! -name "generated.*") GOFILES := $(shell find . -type f -name "*.go" -o -name "go.mod" ! -name "generated.*") -DOCKER_IMAGE ?= gitea/act_runner +DOCKER_IMAGE ?= marketally/gitcaddy-runner DOCKER_TAG ?= nightly DOCKER_REF := $(DOCKER_IMAGE):$(DOCKER_TAG) DOCKER_ROOTLESS_REF := $(DOCKER_IMAGE):$(DOCKER_TAG)-dind-rootless diff --git a/cmd/upload-helper/main.go b/cmd/upload-helper/main.go new file mode 100644 index 0000000..5c59898 --- /dev/null +++ b/cmd/upload-helper/main.go @@ -0,0 +1,38 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package main + +import ( + "flag" + "fmt" + "os" + + "gitea.com/gitea/act_runner/internal/pkg/artifact" +) + +func main() { + url := flag.String("url", "", "Upload URL") + token := flag.String("token", "", "Auth token") + file := flag.String("file", "", "File to upload") + retries := flag.Int("retries", 5, "Maximum retry attempts") + flag.Parse() + + if *url == "" || *token == "" || *file == "" { + fmt.Fprintf(os.Stderr, "GitCaddy Upload Helper - Reliable file uploads with retry\n\n") + fmt.Fprintf(os.Stderr, "Usage: gitcaddy-upload -url URL -token TOKEN -file FILE\n\n") + fmt.Fprintf(os.Stderr, "Options:\n") + flag.PrintDefaults() + os.Exit(1) + } + + helper := artifact.NewUploadHelper() + helper.MaxRetries = *retries + + if err := helper.UploadWithRetry(*url, *token, *file); err != nil { + fmt.Fprintf(os.Stderr, "Upload failed: %v\n", err) + os.Exit(1) + } + + fmt.Println("Upload succeeded!") +} diff --git a/internal/app/cmd/cmd.go b/internal/app/cmd/cmd.go index 7c8e9a4..7101edf 100644 --- a/internal/app/cmd/cmd.go +++ b/internal/app/cmd/cmd.go @@ -15,9 +15,9 @@ import ( ) func Execute(ctx context.Context) { - // ./act_runner + // ./gitcaddy-runner rootCmd := &cobra.Command{ - Use: "act_runner [event name to run]\nIf no event name passed, will default to \"on: push\"", + Use: "gitcaddy-runner [event name to run]\nIf no event name passed, will default to \"on: push\"", Short: "Run GitHub actions locally by specifying the event name (e.g. `push`) or an action name directly.", Args: cobra.MaximumNArgs(1), Version: ver.Version(), @@ -26,7 +26,7 @@ func Execute(ctx context.Context) { configFile := "" rootCmd.PersistentFlags().StringVarP(&configFile, "config", "c", "", "Config file path") - // ./act_runner register + // ./gitcaddy-runner register var regArgs registerArgs registerCmd := &cobra.Command{ Use: "register", @@ -35,14 +35,14 @@ func Execute(ctx context.Context) { RunE: runRegister(ctx, ®Args, &configFile), // must use a pointer to regArgs } registerCmd.Flags().BoolVar(®Args.NoInteractive, "no-interactive", false, "Disable interactive mode") - registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "Gitea instance address") + registerCmd.Flags().StringVar(®Args.InstanceAddr, "instance", "", "GitCaddy instance address") registerCmd.Flags().StringVar(®Args.Token, "token", "", "Runner token") registerCmd.Flags().StringVar(®Args.RunnerName, "name", "", "Runner name") registerCmd.Flags().StringVar(®Args.Labels, "labels", "", "Runner tags, comma separated") registerCmd.Flags().BoolVar(®Args.Ephemeral, "ephemeral", false, "Configure the runner to be ephemeral and only ever be able to pick a single job (stricter than --once)") rootCmd.AddCommand(registerCmd) - // ./act_runner daemon + // ./gitcaddy-runner daemon var daemArgs daemonArgs daemonCmd := &cobra.Command{ Use: "daemon", @@ -53,10 +53,10 @@ func Execute(ctx context.Context) { daemonCmd.Flags().BoolVar(&daemArgs.Once, "once", false, "Run one job then exit") rootCmd.AddCommand(daemonCmd) - // ./act_runner exec + // ./gitcaddy-runner exec rootCmd.AddCommand(loadExecCmd(ctx)) - // ./act_runner config + // ./gitcaddy-runner config rootCmd.AddCommand(&cobra.Command{ Use: "generate-config", Short: "Generate an example config file", @@ -66,7 +66,7 @@ func Execute(ctx context.Context) { }, }) - // ./act_runner cache-server + // ./gitcaddy-runner cache-server var cacheArgs cacheServerArgs cacheCmd := &cobra.Command{ Use: "cache-server", diff --git a/internal/app/cmd/daemon.go b/internal/app/cmd/daemon.go index 2423e7a..7197804 100644 --- a/internal/app/cmd/daemon.go +++ b/internal/app/cmd/daemon.go @@ -175,7 +175,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu // declare the labels of the runner before fetching tasks resp, err := runner.Declare(ctx, ls.Names(), capabilitiesJson) if err != nil && connect.CodeOf(err) == connect.CodeUnimplemented { - log.Errorf("Your Gitea version is too old to support runner declare, please upgrade to v1.21 or later") + log.Errorf("Your GitCaddy version is too old to support runner declare, please upgrade to v1.21 or later") return err } else if err != nil { log.WithError(err).Error("fail to invoke Declare") diff --git a/internal/app/cmd/exec.go b/internal/app/cmd/exec.go index 6337951..8ed010d 100644 --- a/internal/app/cmd/exec.go +++ b/internal/app/cmd/exec.go @@ -505,7 +505,7 @@ func loadExecCmd(ctx context.Context) *cobra.Command { execCmd.PersistentFlags().BoolVarP(&execArg.dryrun, "dryrun", "n", false, "dryrun mode") execCmd.PersistentFlags().StringVarP(&execArg.image, "image", "i", "docker.gitea.com/runner-images:ubuntu-latest", "Docker image to use. Use \"-self-hosted\" to run directly on the host.") execCmd.PersistentFlags().StringVarP(&execArg.network, "network", "", "", "Specify the network to which the container will connect") - execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "Gitea instance to use.") + execCmd.PersistentFlags().StringVarP(&execArg.githubInstance, "gitea-instance", "", "", "GitCaddy instance to use.") return execCmd } diff --git a/internal/app/cmd/register.go b/internal/app/cmd/register.go index 482c6fb..efb68f7 100644 --- a/internal/app/cmd/register.go +++ b/internal/app/cmd/register.go @@ -272,7 +272,7 @@ func printStageHelp(stage registerStage) { case StageOverwriteLocalConfig: log.Infoln("Runner is already registered, overwrite local config? [y/N]") case StageInputInstance: - log.Infoln("Enter the Gitea instance URL (for example, https://gitea.com/):") + log.Infoln("Enter the GitCaddy instance URL (for example, https://gitea.com/):") case StageInputToken: log.Infoln("Enter the runner token:") case StageInputRunnerName: @@ -341,7 +341,7 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs) } if err != nil { log.WithError(err). - Errorln("Cannot ping the Gitea instance server") + Errorln("Cannot ping the GitCaddy instance server") // TODO: if ping failed, retry or exit time.Sleep(time.Second) } else { diff --git a/internal/pkg/artifact/upload_helper.go b/internal/pkg/artifact/upload_helper.go new file mode 100644 index 0000000..6b8bf9f --- /dev/null +++ b/internal/pkg/artifact/upload_helper.go @@ -0,0 +1,145 @@ +// Copyright 2026 MarketAlly. All rights reserved. +// SPDX-License-Identifier: MIT + +package artifact + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "time" + + log "github.com/sirupsen/logrus" +) + +// UploadHelper handles reliable file uploads with retry logic +type UploadHelper struct { + MaxRetries int + RetryDelay time.Duration + ChunkSize int64 + ConnectTimeout time.Duration + MaxTimeout time.Duration +} + +// NewUploadHelper creates a new upload helper with sensible defaults +func NewUploadHelper() *UploadHelper { + return &UploadHelper{ + MaxRetries: 5, + RetryDelay: 10 * time.Second, + ChunkSize: 10 * 1024 * 1024, // 10MB + ConnectTimeout: 120 * time.Second, + MaxTimeout: 3600 * time.Second, + } +} + +// UploadWithRetry uploads a file with automatic retry on failure +func (u *UploadHelper) UploadWithRetry(url, token, filepath string) error { + client := &http.Client{ + Timeout: u.MaxTimeout, + Transport: &http.Transport{ + MaxIdleConns: 10, + MaxIdleConnsPerHost: 5, + IdleConnTimeout: 90 * time.Second, + DisableKeepAlives: false, // Keep connections alive + ForceAttemptHTTP2: false, // Use HTTP/1.1 for large uploads + }, + } + + var lastErr error + for attempt := 0; attempt < u.MaxRetries; attempt++ { + if attempt > 0 { + delay := u.RetryDelay * time.Duration(attempt) + log.Infof("Upload attempt %d/%d, waiting %v before retry...", attempt+1, u.MaxRetries, delay) + time.Sleep(delay) + } + + // Pre-resolve DNS / warm connection + if err := u.prewarmConnection(url); err != nil { + lastErr = fmt.Errorf("connection prewarm failed: %w", err) + log.Warnf("Prewarm failed: %v", err) + continue + } + + // Attempt upload + if err := u.doUpload(client, url, token, filepath); err != nil { + lastErr = err + log.Warnf("Upload attempt %d failed: %v", attempt+1, err) + continue + } + + log.Infof("Upload succeeded on attempt %d", attempt+1) + return nil // Success + } + + return fmt.Errorf("upload failed after %d attempts: %w", u.MaxRetries, lastErr) +} + +// prewarmConnection establishes a connection to help with DNS and TCP setup +func (u *UploadHelper) prewarmConnection(url string) error { + req, err := http.NewRequest("HEAD", url, nil) + if err != nil { + return err + } + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Do(req) + if err != nil { + return err + } + resp.Body.Close() + return nil +} + +// doUpload performs the actual file upload +func (u *UploadHelper) doUpload(client *http.Client, url, token, filepath string) error { + file, err := os.Open(filepath) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + stat, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to stat file: %w", err) + } + + log.Infof("Uploading %s (%d bytes) to %s", filepath, stat.Size(), url) + + // Create multipart form + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile("attachment", stat.Name()) + if err != nil { + return fmt.Errorf("failed to create form file: %w", err) + } + + if _, err := io.Copy(part, file); err != nil { + return fmt.Errorf("failed to copy file to form: %w", err) + } + writer.Close() + + req, err := http.NewRequest("POST", url, body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Authorization", fmt.Sprintf("token %s", token)) + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Connection", "keep-alive") + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("upload request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody)) + } + + log.Infof("Upload completed successfully, status: %d", resp.StatusCode) + return nil +} diff --git a/internal/pkg/envcheck/bandwidth.go b/internal/pkg/envcheck/bandwidth.go index 0b8cf7e..765d4b0 100644 --- a/internal/pkg/envcheck/bandwidth.go +++ b/internal/pkg/envcheck/bandwidth.go @@ -84,7 +84,7 @@ func (bm *BandwidthManager) GetLastResult() *BandwidthInfo { return bm.lastResult } -// TestBandwidth tests network bandwidth to the Gitea server +// TestBandwidth tests network bandwidth to the GitCaddy server func TestBandwidth(ctx context.Context, serverURL string) *BandwidthInfo { if serverURL == "" { return nil