From 6a3335d99427880fbcf023a3d5a8f562e0bc3451 Mon Sep 17 00:00:00 2001 From: mohit-bhandari45 Date: Fri, 11 Sep 2026 12:35:16 +0530 Subject: [PATCH 1/4] feat(build): register update subcommand for shp build Signed-off-by: mohit-bhandari45 --- pkg/shp/cmd/build/build.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/shp/cmd/build/build.go b/pkg/shp/cmd/build/build.go index 80c1f6243..f538ab8d8 100644 --- a/pkg/shp/cmd/build/build.go +++ b/pkg/shp/cmd/build/build.go @@ -23,6 +23,7 @@ func Command(p *params.Params, ioStreams *genericclioptions.IOStreams) *cobra.Co // TODO: add support for `update` and `get` commands command.AddCommand( + runner.NewRunner(p, ioStreams, updateCmd()).Cmd(), runner.NewRunner(p, ioStreams, createCmd()).Cmd(), runner.NewRunner(p, ioStreams, listCmd()).Cmd(), runner.NewRunner(p, ioStreams, deleteCmd()).Cmd(), From 26d6a6eaffdff5ecf9767faa440686f9474a15ae Mon Sep 17 00:00:00 2001 From: mohit-bhandari45 Date: Fri, 11 Sep 2026 12:35:39 +0530 Subject: [PATCH 2/4] feat(build): define UpdateCommand struct and updateCmd constructor Signed-off-by: mohit-bhandari45 --- pkg/shp/cmd/build/update.go | 60 +++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 pkg/shp/cmd/build/update.go diff --git a/pkg/shp/cmd/build/update.go b/pkg/shp/cmd/build/update.go new file mode 100644 index 000000000..24487cb09 --- /dev/null +++ b/pkg/shp/cmd/build/update.go @@ -0,0 +1,60 @@ +package build // nolint:revive + +import ( + "fmt" + + buildv1beta1 "github.com/shipwright-io/build/pkg/apis/build/v1beta1" + "github.com/spf13/cobra" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/shipwright-io/cli/pkg/shp/cmd/runner" + "github.com/shipwright-io/cli/pkg/shp/flags" + "github.com/shipwright-io/cli/pkg/shp/params" +) + +// UpdateCommand contains data provided by user to update an existing Build +type UpdateCommand struct { + cmd *cobra.Command + + name string + buildSpec *buildv1beta1.BuildSpec + dockerfile *string + builderImage *string +} + +func updateCmd() runner.SubCommand { + cmd := &cobra.Command{ + Use: "update [flags]", + Short: "Update an existing Build", + Args: cobra.ExactArgs(1), + } + + buildSpecFlags, dockerfileFlag, builderImageFlag := flags.BuildSpecFromFlags(cmd.Flags()) + + return &UpdateCommand{ + cmd: cmd, + buildSpec: buildSpecFlags, + dockerfile: dockerfileFlag, + builderImage: builderImageFlag, + } +} + +// Cmd returns cobra command object +func (c *UpdateCommand) Cmd() *cobra.Command { + return c.cmd +} + +// Complete fills object with user input data +func (c *UpdateCommand) Complete(_ *params.Params, _ *genericclioptions.IOStreams, args []string) error { + c.name = args[0] + return nil +} + +// Validate checks user input data +func (c *UpdateCommand) Validate() error { + if c.name == "" { + return fmt.Errorf("name must be provided") + } + return nil +} From 9c2a502bdd11deb8b83d7ace1a8d839e2e1ff57b Mon Sep 17 00:00:00 2001 From: mohit-bhandari45 Date: Fri, 11 Sep 2026 12:36:20 +0530 Subject: [PATCH 3/4] feat(build): implement Run method for shp build update command Signed-off-by: mohit-bhandari45 --- pkg/shp/cmd/build/update.go | 100 ++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/pkg/shp/cmd/build/update.go b/pkg/shp/cmd/build/update.go index 24487cb09..deda29907 100644 --- a/pkg/shp/cmd/build/update.go +++ b/pkg/shp/cmd/build/update.go @@ -6,6 +6,8 @@ import ( buildv1beta1 "github.com/shipwright-io/build/pkg/apis/build/v1beta1" "github.com/spf13/cobra" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" "github.com/shipwright-io/cli/pkg/shp/cmd/runner" @@ -58,3 +60,101 @@ func (c *UpdateCommand) Validate() error { } return nil } + +// Run executes the update of an existing Build instance +func (c *UpdateCommand) Run(params *params.Params, ioStreams *genericclioptions.IOStreams) error { + clientset, err := params.ShipwrightClientSet() + if err != nil { + return err + } + + ns := params.Namespace() + build, err := clientset.ShipwrightV1beta1().Builds(ns).Get(c.cmd.Context(), c.name, metav1.GetOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + fmt.Fprintf(ioStreams.Out, "Build '%s' not found in namespace '%s'.\n", c.name, ns) + return nil + } + return err + } + + // Update Git Source URL if flag was explicitly provided + if c.cmd.Flags().Changed(flags.SourceGitURLFlag) && c.buildSpec.Source != nil && c.buildSpec.Source.Git != nil { + if build.Spec.Source == nil { + build.Spec.Source = &buildv1beta1.Source{} + } + if build.Spec.Source.Git == nil { + build.Spec.Source.Git = &buildv1beta1.Git{} + } + build.Spec.Source.Git.URL = c.buildSpec.Source.Git.URL + build.Spec.Source.Type = buildv1beta1.GitType + } + + // Update Git Revision if flag was explicitly provided + if c.cmd.Flags().Changed(flags.SourceGitRevisionFlag) && c.buildSpec.Source != nil && c.buildSpec.Source.Git != nil { + if build.Spec.Source == nil { + build.Spec.Source = &buildv1beta1.Source{} + } + if build.Spec.Source.Git == nil { + build.Spec.Source.Git = &buildv1beta1.Git{} + } + build.Spec.Source.Git.Revision = c.buildSpec.Source.Git.Revision + } + + // Update ContextDir if flag was explicitly provided + if c.cmd.Flags().Changed(flags.SourceContextDirFlag) && c.buildSpec.Source != nil { + if build.Spec.Source == nil { + build.Spec.Source = &buildv1beta1.Source{} + } + build.Spec.Source.ContextDir = c.buildSpec.Source.ContextDir + } + + // Update Strategy Name if flag was explicitly provided + if c.cmd.Flags().Changed(flags.StrategyNameFlag) { + build.Spec.Strategy.Name = c.buildSpec.Strategy.Name + } + + // Update Strategy Kind if flag was explicitly provided + if c.cmd.Flags().Changed(flags.StrategyKindFlag) { + build.Spec.Strategy.Kind = c.buildSpec.Strategy.Kind + } + + // Update Output Image if flag was explicitly provided + if c.cmd.Flags().Changed(flags.OutputImageFlag) { + build.Spec.Output.Image = c.buildSpec.Output.Image + } + + // Update dockerfile param if changed + if c.dockerfile != nil && *c.dockerfile != "" && c.cmd.Flags().Changed(flags.DockerfileFlag) { + updateOrAppendParam(&build.Spec.ParamValues, "dockerfile", *c.dockerfile) + } + + // Update builder-image param if changed + if c.builderImage != nil && *c.builderImage != "" && c.cmd.Flags().Changed(flags.BuilderImageFlag) { + updateOrAppendParam(&build.Spec.ParamValues, "builder-image", *c.builderImage) + } + + flags.SanitizeBuildSpec(&build.Spec) + + if _, err := clientset.ShipwrightV1beta1().Builds(ns).Update(c.cmd.Context(), build, metav1.UpdateOptions{}); err != nil { + return err + } + + fmt.Fprintf(ioStreams.Out, "Updated build %q\n", c.name) + return nil +} + +func updateOrAppendParam(paramValues *[]buildv1beta1.ParamValue, name string, val string) { + for i, p := range *paramValues { + if p.Name == name { + v := val + (*paramValues)[i].SingleValue = &buildv1beta1.SingleValue{Value: &v} + return + } + } + v := val + *paramValues = append(*paramValues, buildv1beta1.ParamValue{ + Name: name, + SingleValue: &buildv1beta1.SingleValue{Value: &v}, + }) +} From 4447a7e7cbb6d0edba7bf60721bf401563cbeec1 Mon Sep 17 00:00:00 2001 From: mohit-bhandari45 Date: Fri, 11 Sep 2026 12:37:02 +0530 Subject: [PATCH 4/4] test(build): add unit tests for shp build update command Signed-off-by: mohit-bhandari45 --- pkg/shp/cmd/build/update_test.go | 116 +++++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 pkg/shp/cmd/build/update_test.go diff --git a/pkg/shp/cmd/build/update_test.go b/pkg/shp/cmd/build/update_test.go new file mode 100644 index 000000000..2d9e52f46 --- /dev/null +++ b/pkg/shp/cmd/build/update_test.go @@ -0,0 +1,116 @@ +package build // nolint:revive + +import ( + "strings" + "testing" + "time" + + buildv1beta1 "github.com/shipwright-io/build/pkg/apis/build/v1beta1" + shpfake "github.com/shipwright-io/build/pkg/client/clientset/versioned/fake" + "github.com/shipwright-io/cli/pkg/shp/flags" + "github.com/shipwright-io/cli/pkg/shp/params" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + kclientsetfake "k8s.io/client-go/kubernetes/fake" +) + +func TestBuildUpdate_Success(t *testing.T) { + oldRevision := "v1.0" + testBuild := &buildv1beta1.Build{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-build", + Namespace: metav1.NamespaceDefault, + }, + Spec: buildv1beta1.BuildSpec{ + Source: &buildv1beta1.Source{ + Git: &buildv1beta1.Git{ + URL: "https://github.com/shipwright-io/old-repo", + Revision: &oldRevision, + }, + }, + Output: buildv1beta1.Image{ + Image: "quay.io/myuser/old-app:v1.0", + }, + }, + } + + shpClientset := shpfake.NewSimpleClientset(testBuild) + k8sClientset := kclientsetfake.NewSimpleClientset() + + cmd := updateCmd() + configFlags := genericclioptions.NewConfigFlags(true) + timeout := 10 * time.Second + p := params.NewParamsForTest(k8sClientset, shpClientset, nil, configFlags, metav1.NamespaceDefault, &timeout, &timeout) + + ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() + + if err := cmd.Complete(p, &ioStreams, []string{"my-build"}); err != nil { + t.Fatalf("unexpected error in Complete: %v", err) + } + + // Set updated flag values + if err := cmd.Cmd().Flags().Set(flags.SourceGitURLFlag, "https://github.com/shipwright-io/new-repo"); err != nil { + t.Fatalf("failed to set source-git-url flag: %v", err) + } + if err := cmd.Cmd().Flags().Set(flags.SourceGitRevisionFlag, "v2.0"); err != nil { + t.Fatalf("failed to set source-git-revision flag: %v", err) + } + if err := cmd.Cmd().Flags().Set(flags.OutputImageFlag, "quay.io/myuser/new-app:v2.0"); err != nil { + t.Fatalf("failed to set output-image flag: %v", err) + } + + if err := cmd.Run(p, &ioStreams); err != nil { + t.Fatalf("unexpected error in Run: %v", err) + } + + output := out.String() + expectedMsg := `Updated build "my-build"` + if !strings.Contains(output, expectedMsg) { + t.Errorf("expected output to contain %q, but got:\n%s", expectedMsg, output) + } + + // Verify object fields were updated in client + updatedBuild, err := shpClientset.ShipwrightV1beta1().Builds(metav1.NamespaceDefault).Get(cmd.Cmd().Context(), "my-build", metav1.GetOptions{}) + if err != nil { + t.Fatalf("failed to get updated build: %v", err) + } + + if updatedBuild.Spec.Source.Git.URL != "https://github.com/shipwright-io/new-repo" { + t.Errorf("expected updated URL 'https://github.com/shipwright-io/new-repo', got %q", updatedBuild.Spec.Source.Git.URL) + } + + if *updatedBuild.Spec.Source.Git.Revision != "v2.0" { + t.Errorf("expected updated revision 'v2.0', got %q", *updatedBuild.Spec.Source.Git.Revision) + } + + if updatedBuild.Spec.Output.Image != "quay.io/myuser/new-app:v2.0" { + t.Errorf("expected updated output image 'quay.io/myuser/new-app:v2.0', got %q", updatedBuild.Spec.Output.Image) + } +} + +func TestBuildUpdate_NotFound(t *testing.T) { + shpClientset := shpfake.NewSimpleClientset() + k8sClientset := kclientsetfake.NewSimpleClientset() + + cmd := updateCmd() + configFlags := genericclioptions.NewConfigFlags(true) + timeout := 10 * time.Second + p := params.NewParamsForTest(k8sClientset, shpClientset, nil, configFlags, metav1.NamespaceDefault, &timeout, &timeout) + + ioStreams, _, out, _ := genericclioptions.NewTestIOStreams() + + if err := cmd.Complete(p, &ioStreams, []string{"nonexistent-build"}); err != nil { + t.Fatalf("unexpected error in Complete: %v", err) + } + + if err := cmd.Run(p, &ioStreams); err != nil { + t.Fatalf("unexpected error in Run: %v", err) + } + + output := out.String() + expectedMsg := "Build 'nonexistent-build' not found in namespace 'default'." + if !strings.Contains(output, expectedMsg) { + t.Errorf("expected output to contain %q, but got:\n%s", expectedMsg, output) + } +}