refactor: drop logrus and other direct dependencies (#1319)

After https://gitea.com/gitea/runner/pulls/1304, logrus stayed only to forward logs from docker/cli and moby/go-archive. The runner used a small part of those: volume parsing, the build context tar, the `ssh://` connection and registry credentials. Own code that matches them replaces them, so logrus leaves the module graph. docker/cli stays for its logrus-free `opts` and `connhelper/ssh` packages.

Other dependencies that the standard library or existing ones cover go too: mergo, httprouter (replaced by `http.ServeMux`), retry-go, go-isatty, go-selinux, container-device-interface, gotest.tools and go-cmp (replaced by testify). The binary shrinks by 5%.

Behaviour differences:

1. An unreadable file in a build context is skipped with a warning, where go-archive left a truncated tar.
2. Build contexts no longer carry `security.capability` xattrs.
3. The cache and artifact servers answer HEAD on GET routes, and no longer redirect trailing slashes or answer OPTIONS.

*Written by Claude.*

---------

Co-authored-by: bircni <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1319
Reviewed-by: bircni <[email protected]>
Reviewed-by: Lunny Xiao <[email protected]>
Co-authored-by: silverwind <[email protected]>
This commit is contained in:
silverwindandbircni authored and Lunny Xiao committed 2026-10-18 18:57:08 +00:00
1 parent e356c2ed85
commit 4158e05dae
43 files changed
+1188 -510

No files matched your search

+18 -19
View File
@@ -33,9 +33,8 @@ import (
"connectrpc.com/connect"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/avast/retry-go/v5"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"golang.org/x/term"
)
func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) func(cmd *cobra.Command, args []string) error {
@@ -189,22 +188,22 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}()
// declare the labels of the runner before fetching tasks
resp, err := retry.NewWithData[*connect.Response[runnerv1.DeclareResponse]](
retry.Context(ctx),
retry.UntilSucceeded(),
retry.Delay(declareRetryDelay),
retry.DelayType(retry.FixedDelay),
retry.RetryIf(shouldRetryDeclare),
retry.OnRetry(func(attempt uint, err error) {
if attempt == 0 {
logging.WithError(err).Warn("fail to invoke Declare, retrying until Gitea is reachable")
} else {
logging.WithError(err).Debug("fail to invoke Declare, retrying")
}
}),
).Do(func() (*connect.Response[runnerv1.DeclareResponse], error) {
return runner.Declare(ctx, ls.Names())
})
var resp *connect.Response[runnerv1.DeclareResponse]
for attempt := 0; ; attempt++ {
resp, err = runner.Declare(ctx, ls.Names())
if err == nil || !shouldRetryDeclare(err) || ctx.Err() != nil {
break
}
if attempt == 0 {
logging.WithError(err).Warn("fail to invoke Declare, retrying until Gitea is reachable")
} else {
logging.WithError(err).Debug("fail to invoke Declare, retrying")
}
select {
case <-time.After(declareRetryDelay):
case <-ctx.Done():
}
}
switch {
case err != nil && ctx.Err() != nil && !client.IsRegistrationRejected(err):
return nil
@@ -340,7 +339,7 @@ func initLogging(cfg *config.Config) {
return funcname, filename
}
isTerm := isatty.IsTerminal(os.Stdout.Fd())
isTerm := term.IsTerminal(int(os.Stdout.Fd()))
logging.SetTextFormat(logging.TextOptions{
DisableColors: !isTerm,
FullTimestamp: true,
-7
View File
@@ -188,10 +188,3 @@ func TestShouldRetryDeclare(t *testing.T) {
require.Equal(t, tc.retry, shouldRetryDeclare(tc.err), tc.err.Error())
}
}
// A dependency that reconfigures logrus's global logger when it is loaded makes its own log lines
// show up twice or not at all, so this fails when upgrading one starts doing that. It does not
// cover a dependency that does so later, while running.
func TestLogrusBridgeSurvivesDependencies(t *testing.T) {
require.True(t, logging.LogrusBridgeIntact())
}
+2 -2
View File
@@ -24,15 +24,15 @@ import (
"connectrpc.com/connect"
pingv1 "gitea.dev/actionslib/ping/v1"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/mattn/go-isatty"
"github.com/spf13/cobra"
"golang.org/x/term"
)
// runRegister registers a runner to the server
func runRegister(ctx context.Context, regArgs *registerArgs, configFile *string) func(*cobra.Command, []string) error {
return func(cmd *cobra.Command, args []string) error {
logging.SetReportCaller(false)
isTerm := isatty.IsTerminal(os.Stdout.Fd())
isTerm := term.IsTerminal(int(os.Stdout.Fd()))
logging.SetTextFormat(logging.TextOptions{
DisableColors: !isTerm,
DisableTimestamp: true,
+3 -4
View File
@@ -14,7 +14,6 @@ import (
"gitea.com/gitea/runner/internal/pkg/config"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
)
func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
@@ -23,7 +22,7 @@ func TestRegisterNonInteractiveReturnsLabelValidationError(t *testing.T) {
Token: "token",
InstanceAddr: "http://localhost:3000",
})
assert.ErrorContains(t, err, "empty label")
require.ErrorContains(t, err, "empty label")
}
func TestRegisterInputsValidate(t *testing.T) {
@@ -262,6 +261,6 @@ func TestDoRegisterReturnsPingErrorWithoutRetrying(t *testing.T) {
cfg := &config.Config{}
cfg.Runner.File = filepath.Join(t.TempDir(), ".runner")
err := doRegister(t.Context(), cfg, &registerInputs{InstanceAddr: server.URL, RunnerName: "runner"})
assert.ErrorContains(t, err, "cannot ping the Gitea instance server")
assert.Equal(t, int32(1), requests.Load())
require.ErrorContains(t, err, "cannot ping the Gitea instance server")
require.Equal(t, int32(1), requests.Load())
}
+19 -20
View File
@@ -10,7 +10,6 @@ import (
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v4"
"gotest.tools/v3/assert"
)
func Test_generateWorkflow(t *testing.T) {
@@ -58,7 +57,7 @@ jobs:
},
},
assert: func(t *testing.T, wf *model.Workflow) {
assert.DeepEqual(t, wf.GetJob("job9").Needs(), []string{"job1", "job2"})
require.Equal(t, []string{"job1", "job2"}, wf.GetJob("job9").Needs())
},
want1: "job9",
wantErr: false,
@@ -83,8 +82,8 @@ jobs:
},
assert: func(t *testing.T, wf *model.Workflow) {
job := wf.GetJob("test")
assert.DeepEqual(t, job.Needs(), []string{})
assert.Equal(t, len(job.Steps), 2)
require.Equal(t, []string{}, job.Needs())
require.Len(t, job.Steps, 2)
},
want1: "test",
wantErr: false,
@@ -125,9 +124,9 @@ jobs:
assert: func(t *testing.T, wf *model.Workflow) {
job := wf.GetJob("deploy")
needs := job.Needs()
assert.DeepEqual(t, needs, []string{"build", "lint", "test"})
assert.Equal(t, wf.Jobs["test"].Outputs["coverage"], "80%")
assert.Equal(t, wf.Jobs["lint"].Result, "failure")
require.Equal(t, []string{"build", "lint", "test"}, needs)
require.Equal(t, "80%", wf.Jobs["test"].Outputs["coverage"])
require.Equal(t, "failure", wf.Jobs["lint"].Result)
},
want1: "deploy",
wantErr: false,
@@ -165,11 +164,11 @@ jobs:
},
},
assert: func(t *testing.T, wf *model.Workflow) {
assert.Equal(t, wf.Name, "Complex workflow")
assert.Equal(t, wf.Env["NODE_ENV"], "production")
assert.Equal(t, wf.Env["CI"], "true")
require.Equal(t, "Complex workflow", wf.Name)
require.Equal(t, "production", wf.Env["NODE_ENV"])
require.Equal(t, "true", wf.Env["CI"])
job := wf.GetJob("build")
assert.Equal(t, len(job.Steps), 4)
require.Len(t, job.Steps, 4)
},
want1: "build",
wantErr: false,
@@ -200,8 +199,8 @@ jobs:
assert: func(t *testing.T, wf *model.Workflow) {
job := wf.GetJob("integration")
container := job.Container()
assert.Equal(t, container.Image, "node:18")
assert.Equal(t, job.Services["postgres"].Image, "postgres:15")
require.Equal(t, "node:18", container.Image)
require.Equal(t, "postgres:15", job.Services["postgres"].Image)
},
want1: "integration",
wantErr: false,
@@ -231,7 +230,7 @@ jobs:
job := wf.GetJob("test")
matrixes, err := job.GetMatrixes()
require.NoError(t, err)
assert.Equal(t, len(matrixes), 2)
require.Len(t, matrixes, 2)
},
want1: "test",
wantErr: false,
@@ -245,9 +244,9 @@ jobs:
},
},
assert: func(t *testing.T, wf *model.Workflow) {
assert.Equal(t, wf.Name, "Special: characters & test")
require.Equal(t, "Special: characters & test", wf.Name)
job := wf.GetJob("test")
assert.Equal(t, len(job.Steps), 3)
require.Len(t, job.Steps, 3)
},
want1: "test",
wantErr: false,
@@ -283,7 +282,7 @@ jobs:
}
require.NoError(t, err)
tt.assert(t, got)
assert.Equal(t, got1, tt.want1)
require.Equal(t, tt.want1, got1)
})
}
}
@@ -301,7 +300,7 @@ func Test_yamlV4NodeRoundTrip(t *testing.T) {
out, err := yaml.Marshal(&node)
require.NoError(t, err)
assert.Equal(t, string(out), "- a\n- b\n- c\n")
require.Equal(t, "- a\n- b\n- c\n", string(out))
})
t.Run("unmarshal and re-marshal workflow", func(t *testing.T) {
@@ -310,7 +309,7 @@ func Test_yamlV4NodeRoundTrip(t *testing.T) {
var wf map[string]any
err := yaml.Unmarshal(input, &wf)
require.NoError(t, err)
assert.Equal(t, wf["name"], "test")
require.Equal(t, "test", wf["name"])
out, err := yaml.Marshal(wf)
require.NoError(t, err)
@@ -318,7 +317,7 @@ func Test_yamlV4NodeRoundTrip(t *testing.T) {
var wf2 map[string]any
err = yaml.Unmarshal(out, &wf2)
require.NoError(t, err)
assert.Equal(t, wf2["name"], "test")
require.Equal(t, "test", wf2["name"])
})
t.Run("node kind constants", func(t *testing.T) {
+1 -2
View File
@@ -7,7 +7,6 @@ import (
"testing"
"github.com/stretchr/testify/require"
"gotest.tools/v3/assert"
)
func TestParse(t *testing.T) {
@@ -77,7 +76,7 @@ func TestParse(t *testing.T) {
return
}
require.NoError(t, err)
assert.DeepEqual(t, got, tt.want)
require.Equal(t, tt.want, got)
})
}
}
+18 -2
View File
@@ -8,6 +8,7 @@ import (
"context"
"errors"
"fmt"
"math/rand/v2"
"regexp"
"slices"
"strings"
@@ -24,7 +25,6 @@ import (
"connectrpc.com/connect"
"gitea.dev/actionslib/pkg/model"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/avast/retry-go/v5"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -777,7 +777,23 @@ func (r *Reporter) Close(lastWords string) error {
func (r *Reporter) flushFinal(fn func() error) error {
ctx, cancel := context.WithTimeout(context.Background(), 3*r.effectiveCloseTimeout())
defer cancel()
return retry.New(retry.Context(ctx)).Do(fn)
var errs []error
for attempt := range 10 {
err := fn()
if err == nil {
return nil
}
errs = append(errs, err)
if attempt == 9 {
break
}
select {
case <-time.After(100*time.Millisecond<<attempt + rand.N(100*time.Millisecond)):
case <-ctx.Done():
return errors.Join(append(errs, ctx.Err())...)
}
}
return errors.Join(errs...)
}
// effectiveCloseTimeout returns closeTimeout, or 10s when unset, so a zero
+14
View File
@@ -15,6 +15,7 @@ import (
"sync"
"sync/atomic"
"testing"
"testing/synctest"
"time"
"gitea.com/gitea/runner/act/common/logging"
@@ -1161,6 +1162,19 @@ func TestReporter_EffectiveCloseTimeout(t *testing.T) {
assert.Equal(t, 5*time.Second, (&Reporter{closeTimeout: 5 * time.Second}).effectiveCloseTimeout())
}
func TestReporter_FlushFinalRetriesUntilSuccess(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
calls := 0
require.NoError(t, (&Reporter{}).flushFinal(func() error {
if calls++; calls < 3 {
return errors.New("unavailable")
}
return nil
}))
assert.Equal(t, 3, calls)
})
}
func TestReporter_ParseResult(t *testing.T) {
r := &Reporter{}
+22 -16
View File
@@ -12,6 +12,7 @@ import (
"fmt"
"io"
"math"
"math/rand/v2"
"net/http"
"net/url"
"os"
@@ -26,7 +27,6 @@ import (
"gitea.dev/actionslib/pkg/model"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/avast/retry-go/v5"
"github.com/docker/go-connections/tlsconfig"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
@@ -226,10 +226,10 @@ func (c *client) upload(ctx context.Context, data proto.Message) error {
}
ctx, cancel := context.WithTimeout(ctx, c.timeout)
defer cancel()
return retry.New(retry.Context(ctx), retry.Attempts(5), retry.LastErrorOnly(true), retry.DelayType(retryDelay)).Do(func() error {
for attempt := 1; ; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint, bytes.NewReader(body))
if err != nil {
return retry.Unrecoverable(err)
return err
}
req.Header = c.headers.Clone()
req.Header.Set("Content-Type", "application/x-protobuf")
@@ -237,33 +237,39 @@ func (c *client) upload(ctx context.Context, data proto.Message) error {
req.Header.Set("Content-Encoding", "gzip")
}
resp, err := c.httpClient.Do(req)
if err != nil {
if err == nil {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if resp.StatusCode < http.StatusMultipleChoices {
return nil
}
err = &statusError{resp.Status, resp.Header.Get("Retry-After")}
if !slices.Contains([]int{http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout}, resp.StatusCode) {
return err
}
}
if attempt == 5 {
return err
}
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
if resp.StatusCode < http.StatusMultipleChoices {
return nil
select {
case <-time.After(retryDelay(attempt, err)):
case <-ctx.Done():
return ctx.Err()
}
err = &statusError{resp.Status, resp.Header.Get("Retry-After")}
if slices.Contains([]int{http.StatusTooManyRequests, http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout}, resp.StatusCode) {
return err
}
return retry.Unrecoverable(err)
})
}
}
type statusError struct{ status, retryAfter string }
func (e *statusError) Error() string { return "OTLP export failed: " + e.status }
func retryDelay(attempt uint, err error, config retry.DelayContext) time.Duration {
func retryDelay(attempt int, err error) time.Duration {
if status := (*statusError)(nil); errors.As(err, &status) {
if seconds, err := strconv.ParseUint(status.retryAfter, 10, 31); err == nil {
return time.Duration(seconds) * time.Second
}
}
return retry.CombineDelay(retry.BackOffDelay, retry.RandomDelay)(attempt, err, config)
return 100*time.Millisecond<<(attempt-1) + rand.N(100*time.Millisecond)
}
func StartJob(ctx context.Context, task *runnerv1.Task) (context.Context, func(runnerv1.Result)) {
+1 -1
View File
@@ -152,7 +152,7 @@ func TestExportRetriesAndMatchesOTLP(t *testing.T) {
<-traceBodies
var tracesData tracepb.TracesData
require.NoError(t, proto.Unmarshal(gunzip(<-traceBodies), &tracesData))
assert.Equal(t, 2*time.Second, retryDelay(1, &statusError{"429 Too Many Requests", "2"}, nil))
assert.Equal(t, 2*time.Second, retryDelay(1, &statusError{"429 Too Many Requests", "2"}))
assert.Equal(t, "op", tracesData.GetResourceSpans()[0].GetScopeSpans()[0].GetSpans()[0].GetName())
resourceMetrics := metricsData.GetResourceMetrics()[0]