feat(cache): add shared S3-compatible storage (#1244)

Store the cache used by `actions/cache` in an S3-compatible bucket such as AWS S3 or RustFS, with no `cache-server`. Runners with the same settings use the same cache.

A cache saved on one runner can be restored on the others within 30 seconds. The runner talks to the bucket over the S3 REST API with SigV4, without an SDK. Lifecycle rules on the bucket expire old caches.

*Description rewritten by Claude.*

---------

Co-authored-by: silverwind <[email protected]>
Reviewed-on: https://gitea.com/gitea/runner/pulls/1244
Reviewed-by: silverwind <[email protected]>
Co-authored-by: bircni <[email protected]>
This commit is contained in:
bircniandsilverwind authored and silverwind committed 2026-09-24 19:46:12 +00:00
1 parent ace8653105
commit 81add27459
13 files changed
+689 -41

No files matched your search

+3
View File
@@ -29,6 +29,9 @@ func runCacheServer(configFile *string, cacheArgs *cacheServerArgs) func(cmd *co
}
initLogging(cfg)
if cfg.Cache.S3 != nil {
return errors.New("cache-server does not support cache.s3; set cache.s3 on the runners instead")
}
var (
dir = cfg.Cache.Dir
+20 -6
View File
@@ -98,15 +98,14 @@ func NewRunner(cfg *config.Config, reg *config.Registration, cli client.Client)
maps.Copy(envs, cfg.Runner.Envs)
var cacheHandler *artifactcache.Handler
if cacheEnabled(cfg) {
if cfg.Cache.ExternalServer != "" {
warnIgnoredCachePolicy(cfg)
}
warnIgnoredCachePolicy(cfg)
warnIgnoredCacheSecret(cfg)
handler, err := artifactcache.StartHandler(artifactcache.Options{
Dir: cfg.Cache.Dir,
OutboundIP: cfg.Cache.Host,
Port: cfg.Cache.Port,
Upstream: cfg.Cache.ExternalServer,
S3: cacheS3(cfg),
Policy: CachePolicy(cfg),
Logger: log.StandardLogger().WithField("module", "cache_request"),
})
@@ -804,12 +803,27 @@ func CachePolicy(cfg *config.Config) artifactcache.Policy {
}
}
// warnIgnoredCachePolicy flags eviction settings configured on a runner that points at an external cache server.
func cacheS3(cfg *config.Config) *artifactcache.S3Options {
s3 := cfg.Cache.S3
if s3 == nil {
return nil
}
return &artifactcache.S3Options{
Endpoint: s3.Endpoint, Region: s3.Region, Bucket: s3.Bucket, Prefix: s3.Prefix,
PathStyle: s3.PathStyle, AccessKey: s3.AccessKey, SecretKey: s3.SecretKey,
}
}
// warnIgnoredCachePolicy flags eviction settings configured on a runner that does not apply them.
func warnIgnoredCachePolicy(cfg *config.Config) {
defaults := config.DefaultCache()
if cfg.Cache.Retention != defaults.Retention || cfg.Cache.RepoSizeLimit != defaults.RepoSizeLimit ||
cfg.Cache.SizeLimit != defaults.SizeLimit || cfg.Cache.SweepInterval != defaults.SweepInterval {
limits := cfg.Cache.Retention != defaults.Retention || cfg.Cache.RepoSizeLimit != defaults.RepoSizeLimit ||
cfg.Cache.SizeLimit != defaults.SizeLimit
switch {
case cfg.Cache.ExternalServer != "" && (limits || cfg.Cache.SweepInterval != defaults.SweepInterval):
log.Warn("cache eviction settings are ignored when cache.external_server is set; configure them on that server instead")
case cfg.Cache.S3 != nil && limits:
log.Warn("cache eviction settings are ignored when cache.s3 is set; use lifecycle rules on the bucket instead")
}
}