From 436d0d2942fe78c53e9bdf6d6b0b5763b16bec14 Mon Sep 17 00:00:00 2001 From: silverwind Date: Mon, 5 Oct 2026 03:34:00 +0000 Subject: [PATCH] fix: recognize Gitea's 401 reply for an unregistered runner (#1287) Since https://github.com/go-gitea/gitea/pull/39578 (backported to v28), Gitea answers an unregistered runner with a Connect `Unauthenticated` error instead of a gRPC status wrapped as `Unknown`. `IsRegistrationRejected` missed it, so an ephemeral runner kept its spent registration file and failed on every restart. This also fails the `gitea/gitea:main-nightly` e2e job. Match on the `unregistered runner` message suffix, which both shapes share. The code can't distinguish Gitea from a proxy, because a proxy's 401 with a JSON body also decodes as a wire `Unauthenticated` error. Written by Claude Code. Reviewed-on: https://gitea.com/gitea/runner/pulls/1287 Reviewed-by: bircni Co-authored-by: silverwind --- internal/app/cmd/daemon_test.go | 1 + internal/app/poll/poller_test.go | 4 ++-- internal/pkg/client/client.go | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/app/cmd/daemon_test.go b/internal/app/cmd/daemon_test.go index ed78b1bd..a6a7a832 100644 --- a/internal/app/cmd/daemon_test.go +++ b/internal/app/cmd/daemon_test.go @@ -128,6 +128,7 @@ func TestDaemonRemovesOnlyConsumedEphemeralRegistration(t *testing.T) { {"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 declare", true, false, errUnregisteredRunner, nil, true}, {"persistent rejected on declare", false, false, errUnregisteredRunner, nil, false}, diff --git a/internal/app/poll/poller_test.go b/internal/app/poll/poller_test.go index f959accb..c18030d5 100644 --- a/internal/app/poll/poller_test.go +++ b/internal/app/poll/poller_test.go @@ -110,8 +110,8 @@ func TestPoller_FetchUnauthenticatedStopsPolling(t *testing.T) { fetchErr error rejected bool }{ - "connect": {connect_go.NewError(connect_go.CodeUnauthenticated, errors.New("unregistered runner")), false}, - "gitea": {connect_go.NewWireError(connect_go.CodeUnknown, errors.New("rpc error: code = Unauthenticated desc = unregistered runner")), true}, + "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}, } { t.Run(name, func(t *testing.T) { client := mocks.NewClient(t) diff --git a/internal/pkg/client/client.go b/internal/pkg/client/client.go index 1858e930..ef77ed6d 100644 --- a/internal/pkg/client/client.go +++ b/internal/pkg/client/client.go @@ -6,6 +6,7 @@ package client import ( "context" "errors" + "strings" "connectrpc.com/connect" "gitea.dev/actionslib/runner/v1" @@ -20,9 +21,8 @@ type Client interface { UpdateTask(context.Context, *connect.Request[runnerv1.UpdateTaskRequest]) (*connect.Response[runnerv1.UpdateTaskResponse], error) } -// IsRegistrationRejected reports whether Gitea itself answered "unregistered runner", unlike a proxy's bare 401. +// IsRegistrationRejected reports whether Gitea itself answered "unregistered runner", unlike a proxy's 401. func IsRegistrationRejected(err error) bool { var connectErr *connect.Error - return errors.As(err, &connectErr) && connectErr.Code() == connect.CodeUnknown && connect.IsWireError(err) && - connectErr.Message() == "rpc error: code = Unauthenticated desc = unregistered runner" + return errors.As(err, &connectErr) && strings.HasSuffix(connectErr.Message(), "unregistered runner") }