mirror of
https://gitea.com/gitea/runner.git
synced 2026-11-04 14:35:32 +00:00
Adds a weekly and on-demand workflow that runs the cross-compiled tests on Windows Server 2025 Core in a [dockur/windows](https://github.com/dockur/windows) QEMU VM, and fixes the Windows bugs it found in host and UNC paths, env handling, shell quoting, post-task scripts, health checks, checkout, action symlinks, step summaries, Git client certificates and docker mode from a Windows host. Host-mode steps now emit UTF-8, `bash` falls back to Git's bash instead of the WSL launcher, the runner can run as a native Windows service, steps can keep background helpers with `RUNNER_TRACKING_ID`, and Windows arm64 binaries are released. Fixes: https://gitea.com/gitea/runner/issues/1339 Fixes: https://gitea.com/gitea/runner/issues/637 Fixes: https://gitea.com/gitea/runner/issues/1172 Fixes: https://gitea.com/gitea/runner/issues/347 Reviewed-on: https://gitea.com/gitea/runner/pulls/1348 Reviewed-by: bircni <[email protected]>
194 lines
7.0 KiB
Go
194 lines
7.0 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.com/gitea/runner/act/common/logging"
|
|
"gitea.com/gitea/runner/internal/pkg/config"
|
|
|
|
"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"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
func TestResolveLabels(t *testing.T) {
|
|
var (
|
|
cfgLabels = []string{"cfg:host"}
|
|
regLabels = []string{"reg:host"}
|
|
)
|
|
|
|
tests := []struct {
|
|
name string
|
|
arg string
|
|
cfg []string
|
|
reg []string
|
|
want []string
|
|
}{
|
|
{"flag wins", "flag:host,other", cfgLabels, regLabels, []string{"flag:host", "other"}},
|
|
{"config wins over registration", "", cfgLabels, regLabels, cfgLabels},
|
|
{"registration is the fallback", "", nil, regLabels, regLabels},
|
|
{"blank flag is ignored", " , ", cfgLabels, regLabels, cfgLabels},
|
|
{"nothing configured", "", nil, nil, nil},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.want, resolveLabels(tt.arg, tt.cfg, tt.reg))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetDockerSocketPathUsesConfigAndEnvironment(t *testing.T) {
|
|
got, err := getDockerSocketPath("tcp://docker.example:2376")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "tcp://docker.example:2376", got)
|
|
|
|
t.Setenv("DOCKER_HOST", "unix:///tmp/docker.sock")
|
|
got, err = getDockerSocketPath("-")
|
|
require.NoError(t, err)
|
|
require.Equal(t, "unix:///tmp/docker.sock", got)
|
|
}
|
|
|
|
func TestInitLoggingSetsLevelAndCaller(t *testing.T) {
|
|
oldLevel, oldOut := logging.GetLevel(), logging.Output()
|
|
t.Cleanup(func() {
|
|
logging.SetLevel(oldLevel)
|
|
logging.SetOutput(oldOut)
|
|
logging.SetReportCaller(false)
|
|
logging.SetTextFormat(logging.TextOptions{})
|
|
})
|
|
|
|
cfg := &config.Config{}
|
|
cfg.Log.Level = "debug"
|
|
initLogging(cfg)
|
|
|
|
require.Equal(t, logging.LevelDebug, logging.GetLevel())
|
|
var buf strings.Builder
|
|
logging.SetOutput(&buf)
|
|
logging.Debugf("line")
|
|
require.Contains(t, buf.String(), "[daemon_test.go:")
|
|
}
|
|
|
|
var errUnregisteredRunner = connect.NewError(connect.CodeUnknown, "rpc error: code = Unauthenticated desc = unregistered runner")
|
|
|
|
type daemonTestService struct {
|
|
runnerv1connect.UnimplementedRunnerServiceHandler
|
|
declareErr, fetchErr error
|
|
declareCalls atomic.Int32
|
|
beforeTask bool
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
func (s *daemonTestService) Declare(context.Context, *runnerv1.DeclareRequest) (*runnerv1.DeclareResponse, error) {
|
|
if s.declareCalls.Add(1) == 1 {
|
|
return nil, connect.NewError(connect.CodeUnavailable, "bad gateway")
|
|
}
|
|
if s.declareErr != nil {
|
|
if s.beforeTask {
|
|
s.cancel()
|
|
}
|
|
return nil, s.declareErr
|
|
}
|
|
return &runnerv1.DeclareResponse{Runner: &runnerv1.Runner{Name: "test"}}, nil
|
|
}
|
|
|
|
func (s *daemonTestService) FetchTask(context.Context, *runnerv1.FetchTaskRequest) (*runnerv1.FetchTaskResponse, error) {
|
|
if s.fetchErr != nil {
|
|
return nil, s.fetchErr
|
|
}
|
|
if s.beforeTask {
|
|
s.cancel()
|
|
return nil, context.Canceled
|
|
}
|
|
return &runnerv1.FetchTaskResponse{Task: &runnerv1.Task{Id: 1, Context: &structpb.Struct{}, WorkflowPayload: []byte("invalid: [")}}, 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 *runnerv1.UpdateLogRequest) (*runnerv1.UpdateLogResponse, error) {
|
|
return &runnerv1.UpdateLogResponse{AckIndex: req.Index + int64(len(req.Rows))}, nil
|
|
}
|
|
|
|
func TestDaemonRetriesDeclareOutagesAndRemovesOnlyConsumedEphemeralRegistration(t *testing.T) {
|
|
defer func(delay time.Duration) { declareRetryDelay = delay }(declareRetryDelay)
|
|
declareRetryDelay = 0
|
|
for _, tc := range []struct {
|
|
name string
|
|
ephemeral, beforeTask bool
|
|
declareErr, fetchErr error
|
|
removed bool
|
|
}{
|
|
{"ephemeral failed task", true, false, nil, nil, true},
|
|
{"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, "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, "bad gateway"), nil, false},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
service := &daemonTestService{declareErr: tc.declareErr, fetchErr: tc.fetchErr, beforeTask: tc.beforeTask, cancel: cancel}
|
|
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")
|
|
configFile := filepath.Join(dir, "config.yaml")
|
|
require.NoError(t, os.WriteFile(configFile, []byte("runner:\n file: "+regFile+"\n idle_cleanup_interval: 0s\ncache:\n enabled: false\n"), 0o600))
|
|
require.NoError(t, config.SaveRegistration(regFile, &config.Registration{Address: server.URL, UUID: "test", Name: "test", Token: "test", Labels: []string{"host:host"}, Ephemeral: tc.ephemeral}))
|
|
err := runDaemon(ctx, &daemonArgs{Once: true}, &configFile)(nil, nil)
|
|
if tc.declareErr == nil && tc.fetchErr == nil || tc.beforeTask {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.Error(t, err)
|
|
}
|
|
require.Equal(t, int32(2), service.declareCalls.Load())
|
|
if tc.removed {
|
|
require.NoFileExists(t, regFile)
|
|
} else {
|
|
require.FileExists(t, regFile)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestShouldRetryDeclare(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
err error
|
|
retry bool
|
|
}{
|
|
{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())
|
|
}
|
|
}
|