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 -6
View File
@@ -8,17 +8,17 @@ import (
"errors"
"strings"
"connectrpc.com/connect"
"gitea.dev/actionslib/runner/v1"
"connectrpc.com/connect/v2"
runnerv1 "gitea.dev/actionslib/runner/v1"
)
// A Client manages communication with the runner.
type Client interface {
Address() string
Declare(context.Context, *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error)
FetchTask(context.Context, *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error)
UpdateLog(context.Context, *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error)
UpdateTask(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error)
Declare(context.Context, *runnerv1.DeclareRequest) (*runnerv1.DeclareResponse, error)
FetchTask(context.Context, *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error)
UpdateLog(context.Context, *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error)
UpdateTask(context.Context, *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error)
}
// IsRegistrationRejected reports whether Gitea itself answered "unregistered runner", unlike a proxy's 401.
+17 -23
View File
@@ -13,7 +13,8 @@ import (
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
"connectrpc.com/connect/v2"
"connectrpc.com/connect/v2/connecthttp"
"gitea.dev/actionslib/ping/v1/pingv1connect"
"gitea.dev/actionslib/pkg/protocol"
"gitea.dev/actionslib/runner/v1/runnerv1connect"
@@ -57,40 +58,33 @@ func getHTTPClient(endpoint string, tlsConfig *tls.Config, timeout time.Duration
// New returns a new runner client. timeout bounds every RPC: without it a
// stalled connection parks the reporter for the whole job context, so logs and
// heartbeats stop together and the task is reaped as a zombie.
func New(endpoint string, tlsConfig *tls.Config, uuid, token string, timeout time.Duration, extraHeaders map[string]string, opts ...connect.ClientOption) *HTTPClient {
func New(endpoint string, tlsConfig *tls.Config, uuid, token string, timeout time.Duration, extraHeaders map[string]string, opts ...connecthttp.Option) *HTTPClient {
baseURL := strings.TrimRight(endpoint, "/") + "/api/actions"
opts = append(opts, connect.WithInterceptors(connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
opts = append([]connecthttp.Option{connecthttp.WithReadMaxBytes(0)}, opts...)
rpcClient := connect.NewClient(connecthttp.NewTransport(getHTTPClient(endpoint, tlsConfig, timeout, extraHeaders), baseURL, opts...), func(next connect.ClientFunc) connect.ClientFunc {
return func(ctx context.Context, spec connect.Spec) (connect.ClientStream, error) {
info, _ := connect.CallInfoForClientContext(ctx)
for name, value := range extraHeaders {
if req.Header().Get(name) == "" { // keep protocol headers such as Content-Type
req.Header().Set(name, value)
if info.RequestHeader().Get(name) == "" {
info.RequestHeader().Set(name, value)
}
}
req.Header().Set("User-Agent", "gitea-runner/"+ver.Version())
info.RequestHeader().Set("User-Agent", "gitea-runner/"+ver.Version())
if uuid != "" {
req.Header().Set(protocol.UUIDHeader, uuid)
info.RequestHeader().Set(protocol.UUIDHeader, uuid)
}
if token != "" {
req.Header().Set(protocol.TokenHeader, token)
info.RequestHeader().Set(protocol.TokenHeader, token)
}
return next(ctx, req)
return next(ctx, spec)
}
})))
})
httpClient := getHTTPClient(endpoint, tlsConfig, timeout, extraHeaders)
return &HTTPClient{
PingServiceClient: pingv1connect.NewPingServiceClient(
httpClient,
baseURL,
opts...,
),
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(
httpClient,
baseURL,
opts...,
),
endpoint: endpoint,
PingServiceClient: pingv1connect.NewPingServiceClient(rpcClient),
RunnerServiceClient: runnerv1connect.NewRunnerServiceClient(rpcClient),
endpoint: endpoint,
}
}
+18 -7
View File
@@ -12,10 +12,12 @@ import (
"testing"
"time"
"connectrpc.com/connect"
"gitea.com/gitea/runner/internal/pkg/ver"
pingv1 "gitea.dev/actionslib/ping/v1"
"gitea.dev/actionslib/pkg/protocol"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
func TestGetHTTPClientUsesProxyFromEnvironment(t *testing.T) {
@@ -44,10 +46,14 @@ func TestGetHTTPClientAppliesTLSConfigOnlyToHTTPS(t *testing.T) {
func TestNewSetsBaseURLAndHeaders(t *testing.T) {
var gotPath string
gotHeaders := make(http.Header)
data := strings.Repeat("x", 4<<20)
body, err := proto.Marshal(&pingv1.PingResponse{Data: data})
require.NoError(t, err)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gotPath = r.URL.Path
gotHeaders = r.Header.Clone()
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/proto")
_, _ = w.Write(body)
}))
defer server.Close()
@@ -61,14 +67,19 @@ func TestNewSetsBaseURLAndHeaders(t *testing.T) {
// Address returns the endpoint as supplied (untrimmed)
require.Equal(t, server.URL+"/", c.Address())
// the call is expected to fail (server returns 500), we only assert what was sent
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
resp, err := c.Ping(t.Context(), &pingv1.PingRequest{Data: "hi"})
require.NoError(t, err)
require.Equal(t, data, resp.Data)
require.True(t, strings.HasPrefix(gotPath, "/api/actions/"), "unexpected path %q", gotPath)
require.Equal(t, "the-uuid", gotHeaders.Get(protocol.UUIDHeader))
require.Equal(t, "the-token", gotHeaders.Get(protocol.TokenHeader))
require.Equal(t, "proxy-token", gotHeaders.Get("X-Proxy-Token"))
require.Equal(t, "application/proto", gotHeaders.Get("Content-Type"))
require.Equal(t, "gitea-runner/"+ver.Version(), gotHeaders.Get("User-Agent"))
require.Equal(t, "1", gotHeaders.Get("Connect-Protocol-Version"))
require.Equal(t, "gzip", gotHeaders.Get("Accept-Encoding"))
require.Empty(t, gotHeaders.Get("Content-Encoding"))
}
func TestNewOmitsEmptyHeaders(t *testing.T) {
@@ -80,7 +91,7 @@ func TestNewOmitsEmptyHeaders(t *testing.T) {
defer server.Close()
c := New(server.URL, nil, "", "", time.Minute, nil)
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
_, _ = c.Ping(t.Context(), &pingv1.PingRequest{Data: "hi"})
require.Empty(t, gotHeaders.Get(protocol.UUIDHeader))
require.Empty(t, gotHeaders.Get(protocol.TokenHeader))
@@ -93,7 +104,7 @@ func TestNewStopsRedirectLoop(t *testing.T) {
defer server.Close()
c := New(server.URL, nil, "", "", time.Minute, nil)
_, err := c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
_, err := c.Ping(t.Context(), &pingv1.PingRequest{Data: "hi"})
require.ErrorContains(t, err, "stopped after 10 redirects")
}
@@ -119,7 +130,7 @@ func TestNewRedirectKeepsExtraHeadersOnlyOnSameHost(t *testing.T) {
gotHeaders = nil
origin := tc.newOrigin(http.RedirectHandler("http://"+net.JoinHostPort(tc.host, port), http.StatusPermanentRedirect))
c := New(origin.URL, &tls.Config{InsecureSkipVerify: true}, "the-uuid", "the-token", time.Minute, map[string]string{"X-Proxy-Token": "proxy-token", "Content-Type": "text/plain"})
_, _ = c.Ping(t.Context(), connect.NewRequest(&pingv1.PingRequest{Data: "hi"}))
_, _ = c.Ping(t.Context(), &pingv1.PingRequest{Data: "hi"})
origin.Close()
require.NotNil(t, gotHeaders, origin.URL)
+9 -10
View File
@@ -7,8 +7,7 @@ import (
"context"
"fmt"
"connectrpc.com/connect"
"gitea.dev/actionslib/runner/v1"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/stretchr/testify/mock"
)
@@ -21,15 +20,15 @@ func (m *Client) Address() string {
return m.AddressValue
}
func call[Request, Response any](ctx context.Context, m *Client, method string, request *connect.Request[Request]) (*connect.Response[Response], error) {
func call[Request, Response any](ctx context.Context, m *Client, method string, request *Request) (*Response, error) {
returns := m.MethodCalled(method, ctx, request)
if callback, ok := returns.Get(0).(func(context.Context, *connect.Request[Request]) (*connect.Response[Response], error)); ok {
if callback, ok := returns.Get(0).(func(context.Context, *Request) (*Response, error)); ok {
return callback(ctx, request)
}
var response *connect.Response[Response]
var response *Response
if value := returns.Get(0); value != nil {
var ok bool
response, ok = value.(*connect.Response[Response])
response, ok = value.(*Response)
if !ok {
panic(fmt.Sprintf("unexpected response type %T for %s", value, method))
}
@@ -37,19 +36,19 @@ func call[Request, Response any](ctx context.Context, m *Client, method string,
return response, returns.Error(1)
}
func (m *Client) Declare(ctx context.Context, request *connect.Request[runnerv1.DeclareRequest]) (*connect.Response[runnerv1.DeclareResponse], error) {
func (m *Client) Declare(ctx context.Context, request *runnerv1.DeclareRequest) (*runnerv1.DeclareResponse, error) {
return call[runnerv1.DeclareRequest, runnerv1.DeclareResponse](ctx, m, "Declare", request)
}
func (m *Client) FetchTask(ctx context.Context, request *connect.Request[runnerv1.FetchTaskRequest]) (*connect.Response[runnerv1.FetchTaskResponse], error) {
func (m *Client) FetchTask(ctx context.Context, request *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
return call[runnerv1.FetchTaskRequest, runnerv1.FetchTaskResponse](ctx, m, "FetchTask", request)
}
func (m *Client) UpdateLog(ctx context.Context, request *connect.Request[runnerv1.UpdateLogRequest]) (*connect.Response[runnerv1.UpdateLogResponse], error) {
func (m *Client) UpdateLog(ctx context.Context, request *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
return call[runnerv1.UpdateLogRequest, runnerv1.UpdateLogResponse](ctx, m, "UpdateLog", request)
}
func (m *Client) UpdateTask(ctx context.Context, request *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) {
func (m *Client) UpdateTask(ctx context.Context, request *runnerv1.UpdateTaskRequest) (*runnerv1.UpdateTaskResponse, error) {
return call[runnerv1.UpdateTaskRequest, runnerv1.UpdateTaskResponse](ctx, m, "UpdateTask", request)
}