From e67904bbedf36129c9b8d3ea589d67825620a050 Mon Sep 17 00:00:00 2001 From: Josh Friend Date: Mon, 6 Apr 2026 10:21:28 -0400 Subject: [PATCH] Add key prefix parameter --- action.yml | 5 +++++ action/src/helpers.js | 2 ++ cmd/gradle-cache/main.go | 5 +++++ dist/main/index.js | 2 ++ dist/post/index.js | 2 ++ dist/pre/index.js | 2 ++ gradlecache/gradlecache_test.go | 4 ++-- gradlecache/restore.go | 4 +++- gradlecache/save.go | 9 ++++++--- gradlecache/store.go | 25 +++++++++++++++---------- 10 files changed, 44 insertions(+), 16 deletions(-) diff --git a/action.yml b/action.yml index e594ebd..3734635 100644 --- a/action.yml +++ b/action.yml @@ -23,6 +23,11 @@ inputs: description: AWS region for the S3 bucket. required: false default: us-west-2 + key-prefix: + description: > + Optional path prefix prepended to all S3 object keys. Useful for + namespacing objects within a shared bucket. + required: false ref: description: > Git ref used to search for a base bundle. On pull_request restores, the diff --git a/action/src/helpers.js b/action/src/helpers.js index a2bf49c..1204350 100644 --- a/action/src/helpers.js +++ b/action/src/helpers.js @@ -65,6 +65,8 @@ function backendArgs() { if (bucket) { args.push("--bucket", bucket); if (region) args.push("--region", region); + const keyPrefix = core.getInput("key-prefix"); + if (keyPrefix) args.push("--key-prefix", keyPrefix); } else { // Default to GitHub Actions cache when no explicit backend is configured. args.push("--github-actions"); diff --git a/cmd/gradle-cache/main.go b/cmd/gradle-cache/main.go index 7d49387..d3c92e0 100644 --- a/cmd/gradle-cache/main.go +++ b/cmd/gradle-cache/main.go @@ -36,6 +36,7 @@ type CLI struct { type backendFlags struct { Bucket string `help:"S3 bucket name."` Region string `help:"AWS region." default:"us-west-2" env:"AWS_REGION"` + KeyPrefix string `help:"Optional path prefix prepended to all S3 object keys." name:"key-prefix"` CachewURL string `help:"Cachew server URL (e.g. http://localhost:8080). Mutually exclusive with --bucket." name:"cachew-url"` GithubActions bool `help:"Use the GitHub Actions Cache backend." name:"github-actions"` } @@ -82,6 +83,7 @@ func (c *RestoreCmd) Run(ctx context.Context, metrics gradlecache.MetricsClient) Bucket: c.Bucket, Region: c.Region, CachewURL: c.CachewURL, + KeyPrefix: c.KeyPrefix, CacheKey: c.CacheKey, GitDir: c.GitDir, Ref: c.Ref, @@ -113,6 +115,7 @@ func (c *RestoreDeltaCmd) Run(ctx context.Context, metrics gradlecache.MetricsCl Bucket: c.Bucket, Region: c.Region, CachewURL: c.CachewURL, + KeyPrefix: c.KeyPrefix, CacheKey: c.CacheKey, Branch: c.Branch, GradleUserHome: c.GradleUserHome, @@ -141,6 +144,7 @@ func (c *SaveCmd) Run(ctx context.Context, metrics gradlecache.MetricsClient) er Bucket: c.Bucket, Region: c.Region, CachewURL: c.CachewURL, + KeyPrefix: c.KeyPrefix, CacheKey: c.CacheKey, Commit: c.Commit, GitDir: c.GitDir, @@ -169,6 +173,7 @@ func (c *SaveDeltaCmd) Run(ctx context.Context, metrics gradlecache.MetricsClien Bucket: c.Bucket, Region: c.Region, CachewURL: c.CachewURL, + KeyPrefix: c.KeyPrefix, CacheKey: c.CacheKey, Branch: c.Branch, GradleUserHome: c.GradleUserHome, diff --git a/dist/main/index.js b/dist/main/index.js index 7d42a8e..ec45ae3 100644 --- a/dist/main/index.js +++ b/dist/main/index.js @@ -33589,6 +33589,8 @@ function backendArgs() { if (bucket) { args.push("--bucket", bucket); if (region) args.push("--region", region); + const keyPrefix = core.getInput("key-prefix"); + if (keyPrefix) args.push("--key-prefix", keyPrefix); } else { // Default to GitHub Actions cache when no explicit backend is configured. args.push("--github-actions"); diff --git a/dist/post/index.js b/dist/post/index.js index 13139d8..0aeeaf9 100644 --- a/dist/post/index.js +++ b/dist/post/index.js @@ -33589,6 +33589,8 @@ function backendArgs() { if (bucket) { args.push("--bucket", bucket); if (region) args.push("--region", region); + const keyPrefix = core.getInput("key-prefix"); + if (keyPrefix) args.push("--key-prefix", keyPrefix); } else { // Default to GitHub Actions cache when no explicit backend is configured. args.push("--github-actions"); diff --git a/dist/pre/index.js b/dist/pre/index.js index 94eb80f..ec29c01 100644 --- a/dist/pre/index.js +++ b/dist/pre/index.js @@ -33589,6 +33589,8 @@ function backendArgs() { if (bucket) { args.push("--bucket", bucket); if (region) args.push("--region", region); + const keyPrefix = core.getInput("key-prefix"); + if (keyPrefix) args.push("--key-prefix", keyPrefix); } else { // Default to GitHub Actions cache when no explicit backend is configured. args.push("--github-actions"); diff --git a/gradlecache/gradlecache_test.go b/gradlecache/gradlecache_test.go index 09df91a..2ec333f 100644 --- a/gradlecache/gradlecache_test.go +++ b/gradlecache/gradlecache_test.go @@ -204,7 +204,7 @@ func TestS3Key(t *testing.T) { }, } for _, tt := range tests { - if got := s3Key(tt.commit, tt.cacheKey, tt.bundleFile); got != tt.want { + if got := s3Key("", tt.commit, tt.cacheKey, tt.bundleFile); got != tt.want { t.Errorf("s3Key(%q, %q, %q) = %q, want %q", tt.commit, tt.cacheKey, tt.bundleFile, got, tt.want) } @@ -1460,7 +1460,7 @@ func TestS3BundleStoreRoundTrip(t *testing.T) { cacheKey := "apos-beta" payload := []byte("bundle contents") - legacyKey := "test-bucket/" + s3Key(commit, cacheKey, bundleFilename(cacheKey)) + legacyKey := "test-bucket/" + s3Key("", commit, cacheKey, bundleFilename(cacheKey)) fs.mu.Lock() fs.objects[legacyKey] = payload fs.mu.Unlock() diff --git a/gradlecache/restore.go b/gradlecache/restore.go index 6afd6a5..97675ae 100644 --- a/gradlecache/restore.go +++ b/gradlecache/restore.go @@ -204,6 +204,8 @@ type RestoreConfig struct { Region string // CachewURL is the cachew server URL. Mutually exclusive with Bucket. CachewURL string + // KeyPrefix is an optional path prefix prepended to all S3 object keys. + KeyPrefix string // CacheKey is the bundle identifier (e.g. "my-project:assembleRelease"). CacheKey string // GitDir is the path to the git repository for history walking. Defaults to ".". @@ -296,7 +298,7 @@ func Restore(ctx context.Context, cfg RestoreConfig) error { } log := cfg.Logger - store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL) + store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL, cfg.KeyPrefix) if err != nil { return err } diff --git a/gradlecache/save.go b/gradlecache/save.go index 6571c49..9afb9ae 100644 --- a/gradlecache/save.go +++ b/gradlecache/save.go @@ -27,6 +27,7 @@ type RestoreDeltaConfig struct { Bucket string Region string CachewURL string + KeyPrefix string CacheKey string Branch string GradleUserHome string @@ -63,7 +64,7 @@ func RestoreDelta(ctx context.Context, cfg RestoreDeltaConfig) error { return errors.Errorf("caches directory not found at %s — run restore first: %w", cachesDir, err) } - store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL) + store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL, cfg.KeyPrefix) if err != nil { return err } @@ -120,6 +121,7 @@ type SaveConfig struct { Bucket string Region string CachewURL string + KeyPrefix string CacheKey string Commit string GitDir string @@ -173,7 +175,7 @@ func Save(ctx context.Context, cfg SaveConfig) error { return errors.Errorf("caches directory not found at %s: %w", cachesDir, err) } - store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL) + store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL, cfg.KeyPrefix) if err != nil { return err } @@ -267,6 +269,7 @@ type SaveDeltaConfig struct { Bucket string Region string CachewURL string + KeyPrefix string CacheKey string Branch string GradleUserHome string @@ -345,7 +348,7 @@ func SaveDelta(ctx context.Context, cfg SaveDeltaConfig) error { return nil } - store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL) + store, err := newStore(cfg.Bucket, cfg.Region, cfg.CachewURL, cfg.KeyPrefix) if err != nil { return err } diff --git a/gradlecache/store.go b/gradlecache/store.go index c41e4b9..809c0f8 100644 --- a/gradlecache/store.go +++ b/gradlecache/store.go @@ -33,7 +33,7 @@ type bundleStore interface { putStream(ctx context.Context, commit, cacheKey string, r io.Reader) (int64, error) } -func newStore(bucket, region, cachewURL string) (bundleStore, error) { +func newStore(bucket, region, cachewURL, keyPrefix string) (bundleStore, error) { if cachewURL != "" { return newCachewClient(cachewURL), nil } @@ -46,18 +46,19 @@ func newStore(bucket, region, cachewURL string) (bundleStore, error) { if err != nil { return nil, err } - return &s3BundleStore{client: client, bucket: bucket}, nil + return &s3BundleStore{client: client, bucket: bucket, keyPrefix: keyPrefix}, nil } // ── S3 bundle store ───────────────────────────────────────────────────────── type s3BundleStore struct { - client *s3Client - bucket string + client *s3Client + bucket string + keyPrefix string // optional path prefix prepended to all object keys } func (s *s3BundleStore) stat(ctx context.Context, commit, cacheKey string) (bundleStatInfo, error) { - obj, err := s.client.stat(ctx, s.bucket, s3Key(commit, cacheKey, bundleFilename(cacheKey))) + obj, err := s.client.stat(ctx, s.bucket, s3Key(s.keyPrefix, commit, cacheKey, bundleFilename(cacheKey))) if err != nil { return bundleStatInfo{}, err } @@ -65,23 +66,27 @@ func (s *s3BundleStore) stat(ctx context.Context, commit, cacheKey string) (bund } func (s *s3BundleStore) get(ctx context.Context, commit, cacheKey string, info bundleStatInfo) (io.ReadCloser, error) { - return s.client.get(ctx, s.bucket, s3Key(commit, cacheKey, bundleFilename(cacheKey)), s3ObjInfo{Size: info.Size, ETag: info.etag}) + return s.client.get(ctx, s.bucket, s3Key(s.keyPrefix, commit, cacheKey, bundleFilename(cacheKey)), s3ObjInfo{Size: info.Size, ETag: info.etag}) } func (s *s3BundleStore) put(ctx context.Context, commit, cacheKey string, r io.ReadSeeker, size int64) error { - return s.client.put(ctx, s.bucket, s3Key(commit, cacheKey, bundleFilename(cacheKey)), r, size, "application/zstd") + return s.client.put(ctx, s.bucket, s3Key(s.keyPrefix, commit, cacheKey, bundleFilename(cacheKey)), r, size, "application/zstd") } func (s *s3BundleStore) putStream(ctx context.Context, commit, cacheKey string, r io.Reader) (int64, error) { - return s.client.putStreamingMultipart(ctx, s.bucket, s3Key(commit, cacheKey, bundleFilename(cacheKey)), r, "application/zstd") + return s.client.putStreamingMultipart(ctx, s.bucket, s3Key(s.keyPrefix, commit, cacheKey, bundleFilename(cacheKey)), r, "application/zstd") } func bundleFilename(cacheKey string) string { return strings.ReplaceAll(cacheKey, ":", "-") + ".tar.zst" } -func s3Key(commit, cacheKey, bundleFile string) string { - return commit + "/" + cacheKey + "/" + bundleFile +func s3Key(prefix, commit, cacheKey, bundleFile string) string { + key := commit + "/" + cacheKey + "/" + bundleFile + if prefix != "" { + return prefix + "/" + key + } + return key } // ── Cachew client ───────────────────────────────────────────────────────────