diff --git a/README.md b/README.md index 6bbc2f0..c3171b4 100644 --- a/README.md +++ b/README.md @@ -400,10 +400,14 @@ images: env: GITHUB_TOKEN # - id: npmrc # file: ./.npmrc - cache_from: # buildx --cache-from sources + cache_from: # buildx --cache-from sources; empty-rendering + # entries are skipped, so an env-templated value enables caching only + # where the environment provides it (e.g. CI) and stays inert locally - "type=registry,ref=ghcr.io/acme/myapp:buildcache" - cache_to: # buildx --cache-to destinations + # - '{{ index .Env "STEVEDORE_CACHE_FROM" }}' + cache_to: # buildx --cache-to destinations (same skip rule) - "type=registry,ref=ghcr.io/acme/myapp:buildcache,mode=max" + # - '{{ index .Env "STEVEDORE_CACHE_TO" }}' paths: # change-detection globs (see Monorepos); ** supported - "services/myapp/**" project: "" # or a .csproj to auto-derive paths from its graph diff --git a/internal/builder/builder.go b/internal/builder/builder.go index 6bfbd80..6ea7d39 100644 --- a/internal/builder/builder.go +++ b/internal/builder/builder.go @@ -71,11 +71,18 @@ func Build(r *run.Runner, s Spec) (string, error) { for _, sec := range s.Secrets { args = append(args, "--secret", secretArg(sec)) } + // Empty entries (e.g. a {{ index .Env "STEVEDORE_CACHE_TO" }} template rendering to + // "" outside CI) are skipped, so configs can gate caching on environment + // presence without breaking local builds. for _, c := range s.CacheFrom { - args = append(args, "--cache-from", c) + if c != "" { + args = append(args, "--cache-from", c) + } } for _, c := range s.CacheTo { - args = append(args, "--cache-to", c) + if c != "" { + args = append(args, "--cache-to", c) + } } switch { case s.Push: diff --git a/internal/builder/builder_test.go b/internal/builder/builder_test.go index 59e3315..37100c2 100644 --- a/internal/builder/builder_test.go +++ b/internal/builder/builder_test.go @@ -3,9 +3,11 @@ package builder import ( "os" "path/filepath" + "strings" "testing" "github.com/blairham/stevedore/internal/config" + "github.com/blairham/stevedore/internal/run" ) func TestSecretArg(t *testing.T) { @@ -61,3 +63,36 @@ func TestReadDigest(t *testing.T) { t.Error("expected error for missing file") } } + +func TestBuildSkipsEmptyCacheEntries(t *testing.T) { + stderr, err := os.CreateTemp(t.TempDir(), "stderr") + if err != nil { + t.Fatal(err) + } + r := &run.Runner{DryRun: true, Stderr: stderr} + + _, err = Build(r, Spec{ + Dockerfile: "Dockerfile", + Context: ".", + CacheFrom: []string{"", "type=gha,scope=build"}, + CacheTo: []string{""}, + }) + if err != nil { + t.Fatal(err) + } + + out, err := os.ReadFile(stderr.Name()) + if err != nil { + t.Fatal(err) + } + cmd := string(out) + if !strings.Contains(cmd, "--cache-from type=gha,scope=build") { + t.Errorf("non-empty cache_from entry missing from command: %s", cmd) + } + if strings.Count(cmd, "--cache-from") != 1 { + t.Errorf("empty cache_from entry should be skipped: %s", cmd) + } + if strings.Contains(cmd, "--cache-to") { + t.Errorf("empty cache_to entry should be skipped: %s", cmd) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 6e79541..e66bd1a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -207,12 +207,14 @@ type Image struct { // CacheFrom lists buildx --cache-from sources, e.g. // "type=registry,ref=ghcr.io/acme/myapp:buildcache". Values may contain - // templates. + // templates; entries that render to an empty string are skipped, so a + // value like '{{ index .Env "STEVEDORE_CACHE_FROM" }}' enables caching only where + // the environment provides it (e.g. CI) without breaking local builds. CacheFrom []string `yaml:"cache_from"` // CacheTo lists buildx --cache-to destinations, e.g. // "type=registry,ref=ghcr.io/acme/myapp:buildcache,mode=max". Values may - // contain templates. + // contain templates; empty-rendering entries are skipped (see CacheFrom). CacheTo []string `yaml:"cache_to"` // ExtraFlags are passed verbatim to `docker buildx build`.