Files
act_runner/internal/app/run/runner_test.go
silverwind 3618385b28 fix: serve the whole results service from the cache server (#1141)
`ACTIONS_RESULTS_URL` names one origin serving every `github.actions.results.api.v1` service. Gitea serves the artifact half and this runner the cache half, so announcing `ACTIONS_CACHE_SERVICE_V2` while that URL pointed at Gitea was a promise the environment could not keep, and `docker buildx` posted its cache calls at Gitea and got a 404.

The cache server now forwards the artifact half to the instance each job registers with, so it is the whole results service and jobs are pointed at it. The announcement follows, and the bundle patch follows the cache URL instead.

Also fixes three things no JavaScript client reached: camelCase in the v2 responses where the Go clients read proto names, the missing `x-ms-request-id` on blob uploads that panics buildkit, and `cache.external_server` passed through without the trailing slash the v1 client concatenates onto.

Tests run the real actions against the services they look for: `actions/cache` over both API versions, the artifact actions up and back down through the forwarding, and `setup-node`. The regression itself is covered by asserting that whatever a job is handed as `ACTIONS_RESULTS_URL` answers a cache service call.

Fixes https://gitea.com/gitea/runner/issues/1139

Reviewed-on: https://gitea.com/gitea/runner/pulls/1141
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <2021+silverwind@noreply.gitea.com>
2026-08-03 16:41:53 +00:00

183 lines
6.8 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package run
import (
"context"
"net/http"
"strings"
"testing"
"gitea.com/gitea/runner/act/runner"
clientmocks "gitea.com/gitea/runner/internal/pkg/client/mocks"
"gitea.com/gitea/runner/internal/pkg/config"
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
runnerv1 "gitea.dev/actions-proto-go/runner/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
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)
r := &Runner{client: cli}
_, err := r.Declare(context.Background(), []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)
}
func TestRunnerDefaultActionsURLUsesMirrorOnlyForGithub(t *testing.T) {
r := &Runner{cfg: &config.Config{}}
r.cfg.Runner.GithubMirror = "https://mirror.example"
task := taskWithDefaultActionsURL("https://github.com")
require.Equal(t, "https://mirror.example", r.getDefaultActionsURL(task))
task = taskWithDefaultActionsURL("https://gitea.example")
require.Equal(t, "https://gitea.example", r.getDefaultActionsURL(task))
}
func TestRunnerRunningCountAndNullLogger(t *testing.T) {
r := &Runner{}
require.Equal(t, int64(0), r.RunningCount())
r.runningCount.Add(2)
require.Equal(t, int64(2), r.RunningCount())
logger := NullLogger{}.WithJobLogger()
require.NotNil(t, logger)
require.NotNil(t, logger.Out)
}
func TestNewRunnerInitializesLabelsAndEnvironment(t *testing.T) {
cacheEnabled := false
cfg := &config.Config{}
cfg.Cache.Enabled = &cacheEnabled
cfg.Runner.Envs = map[string]string{"EXISTING": "value"}
reg := &config.Registration{
Name: "runner",
Labels: []string{"ubuntu:host", "", "pool:e57e18d4"},
}
cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe()
r := NewRunner(cfg, reg, cli)
require.Equal(t, "runner", r.name)
require.Len(t, r.labels, 2)
require.Equal(t, []string{"ubuntu", "pool:e57e18d4"}, r.labels.Names())
require.Equal(t, "value", r.envs["EXISTING"])
require.Equal(t, "https://gitea.example/api/actions_pipeline/", r.envs["ACTIONS_RUNTIME_URL"])
require.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
require.Equal(t, "true", r.envs["GITEA_ACTIONS"])
require.NotEmpty(t, r.envs["GITEA_ACTIONS_RUNNER_VERSION"])
require.Nil(t, r.cacheHandler)
require.Empty(t, r.envs[runner.CacheServiceV2Env], "no cache server, nothing to serve v2 from")
}
// Proxy variables are assembled per task, because a job's service containers have to be
// reached directly and they are only known once the workflow is parsed.
func TestNewRunnerLeavesProxyToTheTask(t *testing.T) {
clearProxyEnv(t)
t.Setenv("http_proxy", "http://proxy:3128")
cfg := &config.Config{}
cfg.Cache.ExternalServer = "http://cache.local:8088/"
reg := &config.Registration{Name: "runner"}
cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe()
r := NewRunner(cfg, reg, cli)
require.NotContains(t, r.envs, "http_proxy")
require.NotContains(t, r.envs, "no_proxy")
}
func taskWithDefaultActionsURL(url string) *runnerv1.Task {
return &runnerv1.Task{
Context: &structpb.Struct{
Fields: map[string]*structpb.Value{
"gitea_default_actions_url": structpb.NewStringValue(url),
},
},
}
}
// The results service is decided per task, because that is where the job's token and its
// instance are both known. NewRunner only leaves the address Gitea serves.
func TestNewRunnerCacheServiceV2(t *testing.T) {
cfg := &config.Config{}
cfg.Cache.Dir, cfg.Cache.Host = t.TempDir(), "127.0.0.1"
cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe()
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
t.Cleanup(func() { _ = r.Close() })
const token = "task-token"
assert.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
assert.Empty(t, r.envs[runner.CacheServiceV2Env], "a promise the runner has not made yet")
assert.True(t, r.patchToolkit())
// The registration is what makes it true: the cache server takes the results service over,
// having been told which instance to forward the artifact half to.
revoke, resultsURL := r.registerCacheForTask(token, "owner/repo", nil)
defer revoke()
require.Equal(t, r.cacheHandler.ExternalURL(), resultsURL)
// And what is advertised has to answer the cache service. A client without the GHES escape
// hatch, docker buildx among them, posts its cache calls at exactly this URL and nowhere else,
// so a 404 here is the failure this whole change is about.
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost,
resultsURL+"/twirp/github.actions.results.api.v1.CacheService/GetCacheEntryDownloadURL",
strings.NewReader(`{"key":"k","version":"v"}`))
require.NoError(t, err)
req.Header.Set("Authorization", "Bearer "+token)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode, "the advertised results service serves no cache service")
}
// The v1 cache client appends its path to ACTIONS_CACHE_URL without a separator, so a configured
// server that is missing the slash would send it to a URL whose port swallows the path.
func TestNewRunnerNormalizesTheExternalCacheServer(t *testing.T) {
cfg := &config.Config{}
cfg.Cache.ExternalServer = "http://cache.local:8088//"
cli := clientmocks.NewClient(t)
cli.On("Address").Return("https://gitea.example/").Maybe()
r := NewRunner(cfg, &config.Registration{Name: "runner"}, cli)
assert.Equal(t, "http://cache.local:8088/", r.envs["ACTIONS_CACHE_URL"])
// Nothing to front the results service with, so the variable stays unset, but the bundles are
// still patched: artifacts v4 need that, and the patch keeps the cache client on the cache URL.
assert.Equal(t, "https://gitea.example", r.envs["ACTIONS_RESULTS_URL"])
assert.Empty(t, r.envs[runner.CacheServiceV2Env])
assert.True(t, r.patchToolkit())
}