enhance: add runner.tool_cache_mode and default it to none (#1171)

The current shared tools cache is not concurrency-safe, e.g. multiple jobs can write and corrupt it, for example `setup-go` with explicit go version under concurrency reliably corrupts the tool cache and fails all jobs.

This adds a new `runner.tool_cache_mode` (and `--tool-cache-mode` exec option) option which defaults to unshared tools cache:

- `none` mounts nothing, so a job uses what its image ships there and discards what it installs
- `shared` keeps the single volume every job reuses, and warns when `runner.capacity` is above 1

Under `none` effective tool cache can only come from the image or host, which is the same as it is on GitHub Actions which ships many preinstalled tools in its fat VM images.

Co-authored-by: bircni <bircni@icloud.com>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1171
Reviewed-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
silverwind
2026-08-18 20:16:11 +00:00
committed by silverwind
parent be90c01468
commit 3f70822458
10 changed files with 147 additions and 13 deletions
+15 -4
View File
@@ -229,14 +229,19 @@ func (rc *RunContext) containerDaemonSocket() string {
return rc.Config.ContainerDaemonSocket
}
const sharedToolCacheVolume = "act-toolcache" // mounted only when the tool cache is shared
// validVolumes returns the volumes allowed on this job's containers: the configured base
// plus the volumes the runner mounts automatically. It derives a fresh slice every call and
// never mutates the shared Config (see containerDaemonSocket).
func (rc *RunContext) validVolumes() []string {
name := rc.jobContainerName()
volumes := slices.Clone(rc.Config.ValidVolumes)
if rc.Config.SharedToolCache {
volumes = append(volumes, sharedToolCacheVolume)
}
// TODO: add a new configuration to control whether the docker daemon can be mounted
return append(volumes, "act-toolcache", name, name+"-env",
return append(volumes, name, name+"-env",
getDockerDaemonSocketMountPath(rc.containerDaemonSocket()))
}
@@ -309,8 +314,10 @@ func (rc *RunContext) GetBindsAndMounts() ([]string, map[string]string) {
if daemonSocket := rc.containerDaemonSocket(); daemonSocket != "-" && !claimed["/var/run/docker.sock"] {
binds = append(binds, getDockerDaemonSocketMountPath(daemonSocket)+":/var/run/docker.sock")
}
if toolCache := rc.toolCache(container.DefaultToolCache); !claimed[toolCache] {
mounts["act-toolcache"] = toolCache
if rc.Config.SharedToolCache {
if toolCache := rc.toolCache(container.DefaultToolCache); !claimed[toolCache] {
mounts[sharedToolCacheVolume] = toolCache
}
}
mounts[name+"-env"] = ext.GetActPath() // runner-internal, never overridable
@@ -360,7 +367,11 @@ func (rc *RunContext) startHostEnvironment() common.Executor {
if err := os.MkdirAll(runnerTmp, 0o777); err != nil {
return err
}
toolCache := rc.toolCache(filepath.Join(cacheDir, "tool_cache"))
toolCacheParent := miscpath // per job, so cleanup removes it with the job
if rc.Config.SharedToolCache {
toolCacheParent = cacheDir
}
toolCache := rc.toolCache(filepath.Join(toolCacheParent, "tool_cache"))
if err := os.MkdirAll(toolCache, 0o777); err != nil {
return err
}