refactor: migrate to connect-go v2 (#1325)

Migrates the runner to `connectrpc.com/connect/v2` by pinning actionslib v1.5.0, which carries https://gitea.com/gitea/actionslib/pulls/28. The wire behaviour towards Gitea is unchanged and works against Gitea servers on connect v1 and v2. The client keeps v1's unlimited response size, and the error codes and messages the runner checks stay the same.

Reviewed-on: https://gitea.com/gitea/runner/pulls/1325
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
This commit is contained in:
silverwind authored and silverwind committed 2026-10-21 16:10:58 +00:00
1 parent 430f7bb2cf
commit 6f0d4f1b48
17 files changed
+292 -301

No files matched your search

+6 -7
View File
@@ -31,7 +31,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/telemetry"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
"connectrpc.com/connect/v2"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/spf13/cobra"
"golang.org/x/term"
@@ -188,7 +188,7 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}()
// declare the labels of the runner before fetching tasks
var resp *connect.Response[runnerv1.DeclareResponse]
var resp *runnerv1.DeclareResponse
for attempt := 0; ; attempt++ {
resp, err = runner.Declare(ctx, ls.Names())
if err == nil || !shouldRetryDeclare(err) || ctx.Err() != nil {
@@ -218,14 +218,13 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
return err
default:
logging.Infof("runner: %s, with version: %s, with labels: %v, declare successfully",
resp.Msg.Runner.Name, resp.Msg.Runner.Version, resp.Msg.Runner.Labels)
resp.Runner.Name, resp.Runner.Version, resp.Runner.Labels)
}
runner.SetCapabilitiesFromDeclare(resp)
poller := poll.New(cfg, cli, runner)
metrics.Init() // OTLP exports the registry even without the server
metrics.RunnerInfo.WithLabelValues(ver.Version(), resp.Msg.Runner.Name).Set(1)
metrics.RunnerInfo.WithLabelValues(ver.Version(), resp.Runner.Name).Set(1)
metrics.RunnerCapacity.Set(float64(cfg.Runner.Capacity))
metrics.RegisterUptimeFunc(time.Now())
metrics.RegisterRunningJobsFunc(runner.RunningCount, cfg.Runner.Capacity)
@@ -267,14 +266,14 @@ func runDaemon(ctx context.Context, daemArgs *daemonArgs, configFile *string) fu
}
}
logging.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Msg.Runner.Name, cfg.Runner.ShutdownTimeout)
logging.Infof("runner: %s shutdown initiated, waiting %s for running jobs to complete before shutting down", resp.Runner.Name, cfg.Runner.ShutdownTimeout)
ctx, cancel := context.WithTimeout(context.Background(), cfg.Runner.ShutdownTimeout)
defer cancel()
err = poller.Shutdown(ctx)
if err != nil {
logging.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Msg.Runner.Name)
logging.Warnf("runner: %s cancelled in progress jobs during shutdown", resp.Runner.Name)
}
if poller.RegistrationRejected() {
+27 -24
View File
@@ -6,7 +6,6 @@ package cmd
import (
"context"
"crypto/tls"
"errors"
"net/http"
"net/http/httptest"
"os"
@@ -19,7 +18,8 @@ import (
"gitea.com/gitea/runner/act/common/logging"
"gitea.com/gitea/runner/internal/pkg/config"
"connectrpc.com/connect"
"connectrpc.com/connect/v2"
"connectrpc.com/connect/v2/connecthttp"
runnerv1 "gitea.dev/actionslib/runner/v1"
"gitea.dev/actionslib/runner/v1/runnerv1connect"
"github.com/stretchr/testify/require"
@@ -83,7 +83,7 @@ func TestInitLoggingSetsLevelAndCaller(t *testing.T) {
require.Contains(t, buf.String(), "[daemon_test.go:")
}
var errUnregisteredRunner = errors.New("rpc error: code = Unauthenticated desc = unregistered runner")
var errUnregisteredRunner = connect.NewError(connect.CodeUnknown, "rpc error: code = Unauthenticated desc = unregistered runner")
type daemonTestService struct {
runnerv1connect.UnimplementedRunnerServiceHandler
@@ -93,9 +93,9 @@ type daemonTestService struct {
cancel context.CancelFunc
}
func (s *daemonTestService) Declare(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
func (s *daemonTestService) Declare(context.Context, *runnerv1.DeclareRequest) (*runnerv1.DeclareResponse, error) {
if s.declareCalls.Add(1) == 1 {
return nil, connect.NewError(connect.CodeUnavailable, errors.New("bad gateway"))
return nil, connect.NewError(connect.CodeUnavailable, "bad gateway")
}
if s.declareErr != nil {
if s.beforeTask {
@@ -103,10 +103,10 @@ func (s *daemonTestService) Declare(context.Context, *connect.Request[runnerv1.D
}
return nil, s.declareErr
}
return connect.NewResponse(&runnerv1.DeclareResponse{Runner: &runnerv1.Runner{Name: "test"}}), nil
return &runnerv1.DeclareResponse{Runner: &runnerv1.Runner{Name: "test"}}, nil
}
func (s *daemonTestService) FetchTask(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
func (s *daemonTestService) FetchTask(context.Context, *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
if s.fetchErr != nil {
return nil, s.fetchErr
}
@@ -114,15 +114,15 @@ func (s *daemonTestService) FetchTask(context.Context, *connect.Request[runnerv1
s.cancel()
return nil, context.Canceled
}
return connect.NewResponse(&runnerv1.FetchTaskResponse{Task: &runnerv1.Task{Id: 1, Context: &structpb.Struct{}, WorkflowPayload: []byte("invalid: [")}}), nil
return &runnerv1.FetchTaskResponse{Task: &runnerv1.Task{Id: 1, Context: &structpb.Struct{}, WorkflowPayload: []byte("invalid: [")}}, nil
}
func (*daemonTestService) UpdateTask(_ context.Context, req *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
return connect.NewResponse(&runnerv1.UpdateTaskResponse{State: req.Msg.State}), nil
func (*daemonTestService) UpdateTask(_ context.Context, req *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
return &runnerv1.UpdateTaskResponse{State: req.State}, nil
}
func (*daemonTestService) UpdateLog(_ context.Context, req *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
return connect.NewResponse(&runnerv1.UpdateLogResponse{AckIndex: req.Msg.Index + int64(len(req.Msg.Rows))}), nil
func (*daemonTestService) UpdateLog(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
}
func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
@@ -138,18 +138,21 @@ func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(
{"ephemeral interrupted before task", true, true, nil, nil, false},
{"persistent once", false, false, nil, nil, false},
{"ephemeral rejected on fetch", true, false, nil, errUnregisteredRunner, true},
{"ephemeral rejected on fetch with 401", true, false, nil, connect.NewError(connect.CodeUnauthenticated, errors.New("unregistered runner")), true},
{"ephemeral proxy 401 on fetch", true, false, nil, connect.NewError(connect.CodeUnauthenticated, errors.New("HTTP status 401 Unauthorized")), false},
{"ephemeral rejected on fetch with 401", true, false, nil, connect.NewError(connect.CodeUnauthenticated, "unregistered runner"), true},
{"ephemeral proxy 401 on fetch", true, false, nil, connect.NewError(connect.CodeUnauthenticated, "HTTP status 401 Unauthorized"), false},
{"ephemeral rejected on declare", true, false, errUnregisteredRunner, nil, true},
{"persistent rejected on declare", false, false, errUnregisteredRunner, nil, false},
{"ephemeral stopped during declare outage", true, true, connect.NewError(connect.CodeUnavailable, errors.New("bad gateway")), nil, false},
{"ephemeral stopped during declare outage", true, true, connect.NewError(connect.CodeUnavailable, "bad gateway"), nil, false},
} {
t.Run(tc.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
defer cancel()
service := &daemonTestService{declareErr: tc.declareErr, fetchErr: tc.fetchErr, beforeTask: tc.beforeTask, cancel: cancel}
_, handler := runnerv1connect.NewRunnerServiceHandler(service)
server := httptest.NewServer(http.StripPrefix("/api/actions", handler))
rpcServer := connect.NewServer()
runnerv1connect.RegisterRunnerServiceHandler(rpcServer, service)
mux := http.NewServeMux()
connecthttp.Mount(mux, rpcServer, connecthttp.WithReadMaxBytes(0))
server := httptest.NewServer(http.StripPrefix("/api/actions", mux))
defer server.Close()
dir := t.TempDir()
regFile := filepath.Join(dir, "registration.json")
@@ -177,13 +180,13 @@ func TestShouldRetryDeclare(t *testing.T) {
err error
retry bool
}{
{connect.NewError(connect.CodeUnavailable, errors.New("bad gateway")), true},
{connect.NewWireError(connect.CodeInternal, errors.New("update runner")), true},
{connect.NewError(connect.CodeUnavailable, &tls.CertificateVerificationError{Err: errors.New("unknown authority")}), false},
{connect.NewError(connect.CodeUnavailable, http.ErrSchemeMismatch), false},
{connect.NewError(connect.CodeUnavailable, tls.AlertError(42)), false},
{connect.NewError(connect.CodePermissionDenied, errors.New("forbidden")), false},
{connect.NewWireError(connect.CodeUnknown, errors.New("unregistered runner")), false},
{connect.NewError(connect.CodeUnavailable, "bad gateway"), true},
{connect.NewError(connect.CodeInternal, "update runner").WithRemote(), true},
{connect.NewError(connect.CodeUnavailable, "certificate verification").WithCause(&tls.CertificateVerificationError{}), false},
{connect.NewError(connect.CodeUnavailable, "scheme mismatch").WithCause(http.ErrSchemeMismatch), false},
{connect.NewError(connect.CodeUnavailable, "TLS alert").WithCause(tls.AlertError(42)), false},
{connect.NewError(connect.CodePermissionDenied, "forbidden"), false},
{connect.NewError(connect.CodeUnknown, "unregistered runner").WithRemote(), false},
} {
require.Equal(t, tc.retry, shouldRetryDeclare(tc.err), tc.err.Error())
}
+9 -10
View File
@@ -21,7 +21,6 @@ import (
"gitea.com/gitea/runner/internal/pkg/lock"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
pingv1 "gitea.dev/actionslib/ping/v1"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/spf13/cobra"
@@ -371,9 +370,9 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
cfg.Runner.ExtraHeaders,
)
if _, err := cli.Ping(ctx, connect.NewRequest(&pingv1.PingRequest{
if _, err := cli.Ping(ctx, &pingv1.PingRequest{
Data: inputs.RunnerName,
})); err != nil {
}); err != nil {
return fmt.Errorf("cannot ping the Gitea instance server: %w", err)
}
logging.Debug("Successfully pinged the Gitea instance server")
@@ -395,25 +394,25 @@ func doRegister(ctx context.Context, cfg *config.Config, inputs *registerInputs)
ls[i] = l.Name
}
// register new runner.
resp, err := cli.Register(ctx, connect.NewRequest(&runnerv1.RegisterRequest{
resp, err := cli.Register(ctx, &runnerv1.RegisterRequest{
Name: reg.Name,
Token: reg.Token,
Version: ver.Version(),
Labels: ls,
Ephemeral: reg.Ephemeral,
Capabilities: run.RunnerCapabilities(),
}))
})
if err != nil {
logging.WithError(err).Error("poller: cannot register new runner")
return err
}
reg.ID = resp.Msg.Runner.Id
reg.UUID = resp.Msg.Runner.Uuid
reg.Name = resp.Msg.Runner.Name
reg.Token = resp.Msg.Runner.Token
reg.ID = resp.Runner.Id
reg.UUID = resp.Runner.Uuid
reg.Name = resp.Runner.Name
reg.Token = resp.Runner.Token
if inputs.Ephemeral != resp.Msg.Runner.Ephemeral {
if inputs.Ephemeral != resp.Runner.Ephemeral {
// TODO we cannot remove the configuration via runner api, if we return an error here we just fill the database
logging.Error("poller: cannot register new runner as ephemeral upgrade Gitea to gain security, run-once will be used automatically")
}
+9 -9
View File
@@ -17,7 +17,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/metrics"
"connectrpc.com/connect"
"connectrpc.com/connect/v2"
runnerv1 "gitea.dev/actionslib/runner/v1"
)
@@ -355,9 +355,9 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task,
// Load the version value that was in the cache when the request was sent.
v := p.tasksVersion.Load()
start := time.Now()
resp, err := p.client.FetchTask(reqCtx, connect.NewRequest(&runnerv1.FetchTaskRequest{
resp, err := p.client.FetchTask(reqCtx, &runnerv1.FetchTaskRequest{
TasksVersion: v,
}))
})
// Our own deadline proves nothing either way: today Gitea answers immediately,
// so it means a slow server, and once it holds the request open it is an idle
@@ -405,27 +405,27 @@ func (p *Poller) fetchTask(ctx context.Context, s *workerState) (*runnerv1.Task,
// Successful response — reset error counter.
s.consecutiveErrors = 0
if resp == nil || resp.Msg == nil {
if resp == nil {
s.consecutiveEmpty++
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultEmpty).Inc()
return nil, false
}
if resp.Msg.TasksVersion > v {
p.tasksVersion.CompareAndSwap(v, resp.Msg.TasksVersion)
if resp.TasksVersion > v {
p.tasksVersion.CompareAndSwap(v, resp.TasksVersion)
}
if resp.Msg.Task == nil {
if resp.Task == nil {
s.consecutiveEmpty++
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultEmpty).Inc()
return nil, false
}
// got a task, set `tasksVersion` to zero to force query db in next request.
p.tasksVersion.CompareAndSwap(resp.Msg.TasksVersion, 0)
p.tasksVersion.CompareAndSwap(resp.TasksVersion, 0)
metrics.PollFetchTotal.WithLabelValues(metrics.LabelResultTask).Inc()
return resp.Msg.Task, true
return resp.Task, true
}
func (p *Poller) markHealthyPoll() {
+19 -19
View File
@@ -15,7 +15,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/client/mocks"
"gitea.com/gitea/runner/internal/pkg/config"
connect_go "connectrpc.com/connect"
"connectrpc.com/connect/v2"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -28,9 +28,9 @@ import (
func TestPoller_WorkerStateCounters(t *testing.T) {
client := mocks.NewClient(t)
client.On("FetchTask", mock.Anything, mock.Anything).Return(
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
// Always return an empty response.
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil
return &runnerv1.FetchTaskResponse{}, nil
},
)
@@ -109,8 +109,8 @@ func TestPoller_FetchUnauthenticatedStopsPolling(t *testing.T) {
fetchErr error
rejected bool
}{
"proxy": {connect_go.NewError(connect_go.CodeUnauthenticated, errors.New("401 Unauthorized")), false},
"gitea": {connect_go.NewWireError(connect_go.CodeUnknown, errors.New("rpc error: code = Unauthenticated desc = unregistered runner")), true},
"proxy": {connect.NewError(connect.CodeUnauthenticated, "401 Unauthorized"), false},
"gitea": {connect.NewError(connect.CodeUnknown, "rpc error: code = Unauthenticated desc = unregistered runner").WithRemote(), true},
} {
t.Run(name, func(t *testing.T) {
client := mocks.NewClient(t)
@@ -242,7 +242,7 @@ func TestPollerPausesAndResumesForLocalAvailability(t *testing.T) {
cli := mocks.NewClient(t)
cli.On("FetchTask", mock.Anything, mock.Anything).Maybe().Run(func(mock.Arguments) {
fetches.Add(1)
}).Return(connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil)
}).Return(&runnerv1.FetchTaskResponse{}, nil)
cfg, err := config.LoadDefault("")
require.NoError(t, err)
@@ -294,8 +294,8 @@ func (m *idleAwareRunner) OnIdle(_ context.Context) {
func TestPollerPollCallsOnIdle(t *testing.T) {
cli := mocks.NewClient(t)
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
return &runnerv1.FetchTaskResponse{}, nil
},
)
@@ -324,8 +324,8 @@ func TestPollerPollCallsOnIdle(t *testing.T) {
func TestPollerPollOnceCallsOnIdle(t *testing.T) {
cli := mocks.NewClient(t)
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
return &runnerv1.FetchTaskResponse{}, nil
},
)
@@ -352,7 +352,7 @@ func TestPollerPollOnceCallsOnIdle(t *testing.T) {
func TestPollerPollOnceCallsOnTaskFetchedBeforeRunningIt(t *testing.T) {
cli := mocks.NewClient(t)
cli.On("FetchTask", mock.Anything, mock.Anything).Return(connect_go.NewResponse(&runnerv1.FetchTaskResponse{Task: &runnerv1.Task{Id: 1}}), nil)
cli.On("FetchTask", mock.Anything, mock.Anything).Return(&runnerv1.FetchTaskResponse{Task: &runnerv1.Task{Id: 1}}, nil)
cfg, err := config.LoadDefault("")
require.NoError(t, err)
runner := &mockRunner{}
@@ -380,17 +380,17 @@ func TestPoller_ConcurrencyLimitedByCapacity(t *testing.T) {
cli := mocks.NewClient(t)
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
atomicMax(&maxFetchConcur, fetchConcur.Add(1))
defer fetchConcur.Add(-1)
n := tasksReturned.Add(1)
if n <= totalTasks {
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{
return &runnerv1.FetchTaskResponse{
Task: &runnerv1.Task{Id: n},
}), nil
}, nil
}
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil
return &runnerv1.FetchTaskResponse{}, nil
},
)
@@ -438,13 +438,13 @@ func TestPoller_ShutdownForcesJobsOnTimeout(t *testing.T) {
var served atomic.Bool
cli := mocks.NewClient(t)
cli.On("FetchTask", mock.Anything, mock.Anything).Return(
func(_ context.Context, _ *connect_go.Request[runnerv1.FetchTaskRequest]) (*connect_go.Response[runnerv1.FetchTaskResponse], error) {
func(_ context.Context, _ *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
if served.CompareAndSwap(false, true) {
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{
return &runnerv1.FetchTaskResponse{
Task: &runnerv1.Task{Id: 1},
}), nil
}, nil
}
return connect_go.NewResponse(&runnerv1.FetchTaskResponse{}), nil
return &runnerv1.FetchTaskResponse{}, nil
},
)
+9 -12
View File
@@ -41,7 +41,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/telemetry"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
"connectrpc.com/connect/v2"
"gitea.dev/actionslib/pkg/model"
runnerv1 "gitea.dev/actionslib/runner/v1"
docker_container "github.com/moby/moby/api/types/container"
@@ -293,14 +293,6 @@ func (r *Runner) cleanupStaleDirs(ctx context.Context, root string, match func(n
}
}
func (r *Runner) SetCapabilitiesFromDeclare(resp *connect.Response[runnerv1.DeclareResponse]) {
if resp == nil {
return
}
// Capability negotiation is done via response headers to avoid a hard proto bump.
r.capabilities = strings.TrimSpace(resp.Header().Get("X-Gitea-Actions-Capabilities"))
}
func (r *Runner) Run(ctx context.Context, task *runnerv1.Task) error {
if _, ok := r.runningTasks.LoadOrStore(task.Id, struct{}{}); ok {
return fmt.Errorf("task %d is already running", task.Id)
@@ -851,12 +843,17 @@ func nearestExistingPath(path string) string {
return "."
}
func (r *Runner) Declare(ctx context.Context, labels []string) (*connect.Response[runnerv1.DeclareResponse], error) {
return r.client.Declare(ctx, connect.NewRequest(&runnerv1.DeclareRequest{
func (r *Runner) Declare(ctx context.Context, labels []string) (*runnerv1.DeclareResponse, error) {
ctx, info := connect.NewClientContext(ctx)
resp, err := r.client.Declare(ctx, &runnerv1.DeclareRequest{
Version: ver.Version(),
Labels: labels,
Capabilities: RunnerCapabilities(),
}))
})
if err == nil {
r.capabilities = strings.TrimSpace(info.ResponseHeader().Get("X-Gitea-Actions-Capabilities"))
}
return resp, err
}
// minFreeDisk keeps the cache from growing past the point where the runner stops taking
+18 -24
View File
@@ -25,7 +25,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/labels"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
"connectrpc.com/connect/v2"
"gitea.dev/actionslib/pkg/model"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/stretchr/testify/assert"
@@ -38,27 +38,21 @@ func TestRunnerCapabilitiesAndDeclare(t *testing.T) {
require.Equal(t, []string{CapabilityCancelling}, RunnerCapabilities())
cli := clientmocks.NewClient(t)
cli.On("Declare", mock.Anything, mock.MatchedBy(func(req *connect.Request[runnerv1.DeclareRequest]) bool {
return req.Msg.Version == ver.Version() &&
len(req.Msg.Labels) == 1 &&
req.Msg.Labels[0] == "ubuntu" &&
len(req.Msg.Capabilities) == 1 &&
req.Msg.Capabilities[0] == CapabilityCancelling
})).Return(connect.NewResponse(&runnerv1.DeclareResponse{}), nil)
cli.On("Declare", mock.Anything, mock.MatchedBy(func(req *runnerv1.DeclareRequest) bool {
return req.Version == ver.Version() &&
len(req.Labels) == 1 &&
req.Labels[0] == "ubuntu" &&
len(req.Capabilities) == 1 &&
req.Capabilities[0] == CapabilityCancelling
})).Return(&runnerv1.DeclareResponse{}, nil).Run(func(args mock.Arguments) {
info, ok := connect.CallInfoForClientContext(args.Get(0).(context.Context))
require.True(t, ok)
info.ResponseHeader().Set("X-Gitea-Actions-Capabilities", " cancelling,cache-v2 ")
})
r := &Runner{client: cli}
_, err := r.Declare(t.Context(), []string{"ubuntu"})
require.NoError(t, err)
}
func TestRunnerSetCapabilitiesFromDeclare(t *testing.T) {
r := &Runner{}
r.SetCapabilitiesFromDeclare(nil)
require.Empty(t, r.capabilities)
resp := connect.NewResponse(&runnerv1.DeclareResponse{})
resp.Header().Set("X-Gitea-Actions-Capabilities", " cancelling,cache-v2 ")
r.SetCapabilitiesFromDeclare(resp)
require.Equal(t, "cancelling,cache-v2", r.capabilities)
}
@@ -104,20 +98,20 @@ func TestRunnerReclaimsVolumesAfterReporting(t *testing.T) {
r := NewRunner(cfg, &config.Registration{UUID: "runner-1", Labels: []string{"ubuntu:docker://node:20"}}, cli)
var reported atomic.Bool
var removed atomic.Int64
cli.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
cli.On("UpdateLog", mock.Anything, mock.Anything).Return(func(_ context.Context, req *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
assert.False(t, reported.Load())
return connect.NewResponse(&runnerv1.UpdateLogResponse{AckIndex: req.Msg.Index + int64(len(req.Msg.Rows))}), nil
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
})
cli.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
if req.Msg.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, req.Msg.State.Result)
cli.On("UpdateTask", mock.Anything, mock.Anything).Return(func(_ context.Context, req *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
if req.State.Result != runnerv1.Result_RESULT_UNSPECIFIED {
assert.Equal(t, runnerv1.Result_RESULT_SUCCESS, req.State.Result)
assert.Equal(t, int64(1), r.RunningCount())
if mode == "post-task script" {
assert.FileExists(t, cfg.Runner.PostTaskScript+".done")
}
reported.Store(true)
}
return connect.NewResponse(&runnerv1.UpdateTaskResponse{State: req.Msg.State}), nil
return &runnerv1.UpdateTaskResponse{State: req.State}, nil
})
var volumesCreated atomic.Bool
daemon := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {