fix: stop artifact uploads depending on the cache server reaching Gitea (#1216)

Cache v2 makes the cache server the `ACTIONS_RESULTS_URL` origin, so artifact calls arrived there and were proxied on to Gitea, failing whenever it could not reach the instance.

- Artifact calls are answered with a redirect, so the cache server opens no connection to Gitea. A scheme change or an untrusted instance is still proxied, but there the cache server is the runner itself, which already reaches Gitea.
- Failures answer in twirp, not an empty `502` that clients report as `Unexpected end of JSON input`.
- `cache.v2: false` really points artifacts at Gitea now.
- Cache reservations are bound to the job that made them, so two jobs saving one key cannot commit against each other's upload, and a retry after a lost answer no longer fails a saved entry.
- The toolkit patch, which edits the GitHub-host check out of an action's bundle, was left in the shared checkout where a job running with `runner.patch_actions: false` could inherit it. It is put back after the job's copy.
- `exec` names an origin for the cache v2 it advertises, and masks its runtime token.

Behaviour changes: `no_proxy` no longer exempts `cache.external_server`, and `cache.enabled: false` also stops external registration.

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

Assisted by Claude (Opus 5).

Reviewed-on: https://gitea.com/gitea/runner/pulls/1216
Reviewed-by: bircni <[email protected]>
Co-authored-by: silverwind <[email protected]>
This commit is contained in:
silverwind authored and bircni committed 2026-09-08 18:45:10 +00:00
1 parent ff9965e940
commit 2ed8cdb76e
22 files changed
+533 -82

No files matched your search

+21
View File
@@ -28,3 +28,24 @@ func testActionsCacheRoundTrip(t *testing.T) {
})
}
}
func testArtifactRoundTrip(t *testing.T) {
t.Parallel()
v2 := true
for _, tc := range []struct {
name string
options runnerOptions
}{
{"through_the_cache_server", runnerOptions{cacheV2: &v2}},
{"direct_with_no_reachable_cache", runnerOptions{cacheHost: "cache.invalid", cacheV2: new(bool)}},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
api, repo, _ := startIsolatedScenario(t, "artifact.yml", "e2e-artifact", tc.options)
wfRun := waitForRun(t, api, repo)
requireSuccess(t, api, repo, wfRun.ID)
})
}
}
+1
View File
@@ -27,6 +27,7 @@ func TestCompatibility(t *testing.T) {
t.Run("payloads", testPayloads)
t.Run("cache", testActionsCacheRoundTrip)
t.Run("artifact", testArtifactRoundTrip)
t.Run("cancellation_and_log_streaming", testRunCancellation)
t.Run("dispatch", testWorkflowDispatch)
t.Run("ephemeral", testEphemeralRunner)
+2
View File
@@ -27,6 +27,7 @@ type runnerOptions struct {
capacity int
ephemeral bool
cacheV2 *bool
cacheHost string
}
func startRunner(t *testing.T, repo, labelName string, options runnerOptions) *poll.Poller {
@@ -49,6 +50,7 @@ func startRunner(t *testing.T, repo, labelName string, options runnerOptions) *p
cfg.Container.DockerHost = "unix:///var/run/docker.sock"
}
cfg.Cache.Dir = t.TempDir() + "/cache"
cfg.Cache.Host = options.cacheHost
cfg.Runner.Insecure = true
cfg.Runner.FetchInterval = 250 * time.Millisecond // faster than prod defaults for local fixture
cfg.Runner.FetchIntervalMax = 250 * time.Millisecond
+28
View File
@@ -0,0 +1,28 @@
name: artifact
on: push
jobs:
upload:
runs-on: e2e-artifact
steps:
- run: |
echo "results=$ACTIONS_RESULTS_URL cache=$ACTIONS_CACHE_URL v2=${ACTIONS_CACHE_SERVICE_V2:-unset}"
if [ "$ACTIONS_CACHE_SERVICE_V2" = "true" ]; then
test "$ACTIONS_RESULTS_URL/" = "$ACTIONS_CACHE_URL"
else
test "$ACTIONS_RESULTS_URL/" != "$ACTIONS_CACHE_URL"
fi
mkdir -p out
echo "artifact-payload" > out/data.txt
- uses: actions/upload-artifact@v7
with:
name: e2e-artifact-${{ github.run_id }}
path: out/data.txt
download:
needs: upload
runs-on: e2e-artifact
steps:
- uses: actions/download-artifact@v8
with:
name: e2e-artifact-${{ github.run_id }}
path: got
- run: grep -q artifact-payload got/data.txt