fix: send artifacts to Gitea when jobs cannot reach the cache server (#1225)

Container jobs on a Docker bridge network isolated from the runner's cache server now upload artifacts to Gitea directly instead of timing out, with a job log warning on how to make the cache reachable.

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

Reviewed-on: https://gitea.com/gitea/runner/pulls/1225
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-09-14 19:52:15 +00:00
committed by silverwind
parent 2cc3000369
commit 19afebc53f
7 changed files with 125 additions and 10 deletions
+29 -8
View File
@@ -11,6 +11,7 @@ import (
"fmt"
"maps"
"net/http"
"net/netip"
"net/url"
"os"
"path/filepath"
@@ -67,6 +68,8 @@ type Runner struct {
cacheHandler *artifactcache.Handler
capabilities string
isolatedCacheNetwork func() string
runningTasks sync.Map
runningCount atomic.Int64
lastIdleCleanupUnixNano atomic.Int64
@@ -110,7 +113,6 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
} else {
cacheHandler = handler
envs["ACTIONS_CACHE_URL"] = handler.ExternalURL() + "/"
warnIfCacheUnreachable(cfg, handler.ExternalURL())
}
}
}
@@ -135,6 +137,7 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
now: time.Now,
runHealthCheck: executeHealthCheck,
}
runner.isolatedCacheNetwork = sync.OnceValue(runner.detectIsolatedCacheNetwork)
return runner
}
@@ -477,7 +480,6 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
// is that server's responsibility to authenticate requests.
revokeCache, resultsURL := r.registerCacheForTask(giteaRuntimeToken, preset.Repository, reporter)
defer revokeCache()
r.setResultsService(envs, resultsURL)
eventJSON, err := json.Marshal(preset.Event)
if err != nil {
@@ -518,6 +520,13 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.
return fallbackPlatform()
}
if resultsURL != "" && r.cacheIsolatedFrom(job, platformPicker) {
reporter.Logf("::warning::%s", runner.EscapeCommandData(fmt.Sprintf("jobs cannot reach the cache server at %s on docker network %q, so caching fails and artifacts go to Gitea directly, set cache.host and cache.port to an address jobs reach, or container.network to %[2]q",
r.cacheHandler.ExternalURL(), r.isolatedCacheNetwork())))
resultsURL = ""
}
r.setResultsService(envs, resultsURL)
runnerConfig := &runner.Config{
// On Linux, Workdir will be like "/<parent_directory>/<owner>/<repo>"
// On Windows, Workdir will be like "\<parent_directory>\<owner>\<repo>"
@@ -847,12 +856,24 @@ func warnIgnoredCacheSecret(cfg *config.Config) {
log.Warnf("%s is set but cache.external_server is not; the built-in cache server does not use a shared secret, so the value is ignored", key)
}
func warnIfCacheUnreachable(cfg *config.Config, cacheURL string) {
if cfg.Cache.Host != "" || cfg.Container.Network != "" {
return
func (r *Runner) cacheIsolatedFrom(job *model.Job, pickPlatform func([]string) string) bool {
jobContainer := job.Container()
return (jobContainer != nil && jobContainer.Image != "" || pickPlatform(job.RunsOn()) != labels.SelfHostedPlatform) && r.isolatedCacheNetwork() != ""
}
func (r *Runner) detectIsolatedCacheNetwork() string {
if r.cacheHandler == nil {
return ""
}
if _, err := os.Stat("/.dockerenv"); err != nil {
return
addr, err := netip.ParseAddr(hostOf(r.cacheHandler.ExternalURL()))
if err != nil {
return ""
}
log.Warnf("jobs are given %s for the cache server; if they cannot reach it, set container.network to a network this runner is on, or cache.host", cacheURL)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
network, err := container.IsolatedNetwork(ctx, addr, r.cfg.Container.Network)
if err != nil {
log.Warnf("cannot check whether jobs reach the cache server: %v", err)
}
return network
}
+9
View File
@@ -24,6 +24,7 @@ import (
"gitea.com/gitea/runner/internal/pkg/ver"
"connectrpc.com/connect"
"gitea.dev/actionslib/pkg/model"
runnerv1 "gitea.dev/actionslib/runner/v1"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -337,6 +338,14 @@ func TestNewRunnerCacheServiceV2(t *testing.T) {
assert.Equal(t, resultsURL, envs["ACTIONS_RESULTS_URL"], instance)
assert.Empty(t, envs[runner.CacheServiceV2Env])
}
workflow, err := model.ReadWorkflow(strings.NewReader(`jobs: {native: {runs-on: native}, linux: {runs-on: linux}, containerized: {runs-on: native, container: alpine}, empty: {runs-on: native, container: ""}}`))
require.NoError(t, err)
r.isolatedCacheNetwork = func() string { return "compose" }
pickPlatform := func(runsOn []string) string { return map[string]string{"native": labels.SelfHostedPlatform}[runsOn[0]] }
for job, isolated := range map[string]bool{"native": false, "linux": true, "containerized": true, "empty": false} {
assert.Equal(t, isolated, r.cacheIsolatedFrom(workflow.GetJob(job), pickPlatform), job)
}
}
// The v1 cache client appends its path to ACTIONS_CACHE_URL without a separator, so a configured
+2 -1
View File
@@ -180,7 +180,8 @@ cache:
# Serve the actions cache service v2 API. The actions that use it fall back to v1 on any host
# they do not take for GitHub, so reaching it means editing that check out of their own bundle,
# put back after the copy into the job. That edit is made either way, this only governs the API
# advertised. A bundle that does not match is left alone. With v2, uploads need a reachable cache.
# advertised. A bundle that does not match is left alone. With v2, artifact uploads go via the
# cache server unless container jobs cannot reach its Docker network.
#v2: true
# How the cache server discards entries, ignored when external_server is set since that
# server applies its own. Leave a setting out for its default; 0s or 0 turns the three