feat: Rebrand to gitcaddy-runner with upload helper
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, linux) (push) Successful in 1m12s
Release / build (amd64, darwin) (push) Successful in 1m16s
Release / build (arm64, darwin) (push) Successful in 1m0s
Release / build (amd64, windows) (push) Successful in 1m13s
Release / build (arm64, linux) (push) Successful in 45s
Release / release (push) Successful in 50s
Some checks are pending
CI / build-and-test (push) Waiting to run
Release / build (amd64, linux) (push) Successful in 1m12s
Release / build (amd64, darwin) (push) Successful in 1m16s
Release / build (arm64, darwin) (push) Successful in 1m0s
Release / build (amd64, windows) (push) Successful in 1m13s
Release / build (arm64, linux) (push) Successful in 45s
Release / release (push) Successful in 50s
- 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 <noreply@anthropic.com>
This commit is contained in:
4
Makefile
4
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
|
||||
|
||||
38
cmd/upload-helper/main.go
Normal file
38
cmd/upload-helper/main.go
Normal file
@@ -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!")
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
145
internal/pkg/artifact/upload_helper.go
Normal file
145
internal/pkg/artifact/upload_helper.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user