diff --git a/cmd/build.go b/cmd/build.go index 415827cb..acd24d2d 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -231,7 +231,7 @@ func addBuildFlags(cmd *cobra.Command) { cmd.Flags().String("coverage-output-path", "", "Output path where test coverage file will be copied after running tests") cmd.Flags().Bool("disable-coverage", false, "Disable test coverage collection (defaults to false)") cmd.Flags().Bool("enable-test-tracing", false, "Enable per-test OpenTelemetry span creation (defaults to false)") - cmd.Flags().StringToString("docker-build-options", nil, "Options passed to all 'docker build' commands") + cmd.Flags().StringArray("docker-build-options", nil, "Options passed to all 'docker build' commands (can be repeated, e.g., --docker-build-options=cache-to=type=gha,mode=max)") cmd.Flags().Bool("slsa-cache-verification", false, "Enable SLSA verification for cached artifacts") cmd.Flags().String("slsa-source-uri", "", "Expected source URI for SLSA verification (required when verification enabled)") cmd.Flags().Bool("slsa-require-attestation", false, "Require SLSA attestations (missing/invalid → build locally)") @@ -367,11 +367,11 @@ func getBuildOpts(cmd *cobra.Command) ([]leeway.BuildOption, cache.LocalCache) { disableCoverage, _ := cmd.Flags().GetBool("disable-coverage") enableTestTracing, _ := cmd.Flags().GetBool("enable-test-tracing") - var dockerBuildOptions leeway.DockerBuildOptions - dockerBuildOptions, err = cmd.Flags().GetStringToString("docker-build-options") + dockerBuildOptionsSlice, err := cmd.Flags().GetStringArray("docker-build-options") if err != nil { log.Fatal(err) } + dockerBuildOptions := leeway.DockerBuildOptions(dockerBuildOptionsSlice) jailedExecution, err := cmd.Flags().GetBool("jailed-execution") if err != nil { diff --git a/pkg/leeway/build.go b/pkg/leeway/build.go index 2c33e5b5..81509466 100644 --- a/pkg/leeway/build.go +++ b/pkg/leeway/build.go @@ -495,8 +495,9 @@ type buildOptions struct { context *buildContext } -// DockerBuildOptions are options passed to "docker build" -type DockerBuildOptions map[string]string +// DockerBuildOptions are options passed to "docker build". +// Each entry is a "key=value" string that becomes "--key=value" in the docker command. +type DockerBuildOptions []string // BuildOption configures the build behaviour type BuildOption func(*buildOptions) error @@ -2408,8 +2409,8 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p buildcmd = append(buildcmd, "--squash") } if buildctx.DockerBuildOptions != nil { - for opt, v := range *buildctx.DockerBuildOptions { - buildcmd = append(buildcmd, fmt.Sprintf("--%s=%s", opt, v)) + for _, opt := range *buildctx.DockerBuildOptions { + buildcmd = append(buildcmd, fmt.Sprintf("--%s", opt)) } } buildcmd = append(buildcmd, ".") diff --git a/pkg/leeway/build_internal_test.go b/pkg/leeway/build_internal_test.go index 07e78f50..bf1bf6e4 100644 --- a/pkg/leeway/build_internal_test.go +++ b/pkg/leeway/build_internal_test.go @@ -333,3 +333,57 @@ func TestYarnAppExtraction_ScopedPackage(t *testing.T) { }) } } + +func TestDockerBuildOptions_CommaInValue(t *testing.T) { + // DockerBuildOptions should preserve comma-containing values intact. + // This is the fix for: https://github.com/gitpod-io/leeway/issues/XXX + // Previously, using StringToString flag type would split "cache-to=type=gha,mode=max" + // into two separate options, breaking docker buildx cache options. + + tests := []struct { + name string + opts DockerBuildOptions + expected []string + }{ + { + name: "single option with comma in value", + opts: DockerBuildOptions{"cache-to=type=gha,mode=max"}, + expected: []string{"--cache-to=type=gha,mode=max"}, + }, + { + name: "multiple options with commas", + opts: DockerBuildOptions{"cache-to=type=gha,mode=max", "cache-from=type=gha"}, + expected: []string{"--cache-to=type=gha,mode=max", "--cache-from=type=gha"}, + }, + { + name: "simple option without comma", + opts: DockerBuildOptions{"no-cache=true"}, + expected: []string{"--no-cache=true"}, + }, + { + name: "empty options", + opts: DockerBuildOptions{}, + expected: []string{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var result []string + for _, opt := range tt.opts { + result = append(result, fmt.Sprintf("--%s", opt)) + } + + if len(result) != len(tt.expected) { + t.Errorf("got %d options, want %d", len(result), len(tt.expected)) + return + } + + for i, got := range result { + if got != tt.expected[i] { + t.Errorf("option %d: got %q, want %q", i, got, tt.expected[i]) + } + } + }) + } +}