Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/user/gen-docs/kyma_app_push.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ kyma app push [flags]
# The application will be built using Cloud Native Buildpacks:
kyma app push --name my-app --code-path .

# Push with a custom image tag (e.g. a Git commit SHA for CI/CD traceability):
kyma app push --name my-app --code-path . --build-tag abc1234
kyma app push --name my-app --dockerfile ./Dockerfile --build-tag $GITHUB_SHA

# Push an application based on a Dockerfile located in the current directory:
kyma app push --name my-app --dockerfile ./Dockerfile --dockerfile-context .

Expand Down Expand Up @@ -65,6 +69,7 @@ kyma app push [flags]
## Flags

```text
--build-tag string Custom tag for the built image (e.g. a Git commit SHA). Applies only to --code-path and --dockerfile builds.
--code-path string Path to the application source code directory
--container-port int Port on which the application is exposed
--dockerfile string Path to the Dockerfile
Expand Down
34 changes: 31 additions & 3 deletions internal/cmd/app/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package app
import (
"fmt"
"os"
"regexp"
"time"

"github.com/kyma-project/cli.v3/internal/clierror"
Expand All @@ -20,13 +21,16 @@ import (
"github.com/spf13/cobra"
)

var buildTagRegexp = regexp.MustCompile(`^[a-zA-Z0-9_][a-zA-Z0-9_.-]{0,127}$`)

type appPushConfig struct {
*cmdcommon.KymaConfig

name string
namespace string
image string
imagePullSecretName string
buildTag string
dockerfilePath string
dockerfileSrcContext string
dockerfileArgs types.Map
Expand Down Expand Up @@ -59,6 +63,10 @@ func NewAppPushCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
# The application will be built using Cloud Native Buildpacks:
kyma app push --name my-app --code-path .

# Push with a custom image tag (e.g. a Git commit SHA for CI/CD traceability):
kyma app push --name my-app --code-path . --build-tag abc1234
kyma app push --name my-app --dockerfile ./Dockerfile --build-tag $GITHUB_SHA

# Push an application based on a Dockerfile located in the current directory:
kyma app push --name my-app --dockerfile ./Dockerfile --dockerfile-context .

Expand Down Expand Up @@ -110,6 +118,7 @@ func NewAppPushCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
flags.MarkExactlyOneRequired("image", "dockerfile", "code-path"),
flags.MarkExclusive("dockerfile-context", "image", "code-path"),
flags.MarkExclusive("dockerfile-build-arg", "image", "code-path"),
flags.MarkExclusive("build-tag", "image"),
flags.MarkPrerequisites("expose", "container-port"),
flags.MarkPrerequisites("image-pull-secret", "image"),
))
Expand All @@ -132,6 +141,7 @@ func NewAppPushCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
// image flags
cmd.Flags().StringVar(&config.image, "image", "", "Name of the image to deploy")
cmd.Flags().StringVar(&config.imagePullSecretName, "image-pull-secret", "", "Name of the Kubernetes Secret with credentials to pull the image")
cmd.Flags().StringVar(&config.buildTag, "build-tag", "", "Custom tag for the built image (e.g. a Git commit SHA). Applies only to --code-path and --dockerfile builds.")

// dockerfile flags
cmd.Flags().StringVar(&config.dockerfilePath, "dockerfile", "", "Path to the Dockerfile")
Expand Down Expand Up @@ -178,6 +188,17 @@ func (apc *appPushConfig) complete() clierror.Error {
}
}

if apc.buildTag != "" {
Copy link
Copy Markdown
Contributor

@musztardem musztardem May 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This lives in the complete method which as I understand is responsible for populating config data based on some logic. We don't populate the buildTag here but rather check if it's complaint with regex so I would move this piece to the validate method

if !buildTagRegexp.MatchString(apc.buildTag) {
return clierror.New(
fmt.Sprintf("invalid image tag %q", apc.buildTag),
"tag must start with a letter, digit, or underscore",
"tag may only contain letters, digits, underscores, dots, and hyphens",
"tag must be at most 128 characters",
)
}
}

return nil
}

Expand Down Expand Up @@ -317,7 +338,7 @@ func buildAndImportImage(client kube.Client, cfg *appPushConfig, registryConfig
out.Msgln("Building image\n")
imageName, err := buildImage(cfg)
if err != nil {
return "", clierror.Wrap(err, clierror.New("failed to build image from Dockerfile"))
return "", clierror.Wrap(err, clierror.New("failed to build image"))
}

pushFunc := registry.NewPushWithPortforwardFunc(
Expand Down Expand Up @@ -352,9 +373,16 @@ func buildAndImportImage(client kube.Client, cfg *appPushConfig, registryConfig
return pushedImage, nil
}

// resolveImageTag returns imageTag if non-empty, otherwise a timestamp-based tag.
func resolveImageTag(imageTag string) string {
if imageTag != "" {
return imageTag
}
return time.Now().Format("2006-01-02_15-04-05")
}

func buildImage(cfg *appPushConfig) (string, error) {
imageTag := time.Now().Format("2006-01-02_15-04-05")
imageName := fmt.Sprintf("%s:%s", cfg.name, imageTag)
imageName := fmt.Sprintf("%s:%s", cfg.name, resolveImageTag(cfg.buildTag))

var err error
if cfg.packAppPath != "" {
Expand Down
108 changes: 108 additions & 0 deletions internal/cmd/app/push_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package app

import (
"testing"

"github.com/stretchr/testify/require"
)

func Test_appPushConfig_complete_buildTag(t *testing.T) {
tests := []struct {
name string
buildTag string
wantErr bool
}{
{
name: "empty tag skips validation - no error",
buildTag: "",
wantErr: false,
},
{
name: "valid commit sha",
buildTag: "abc1234def5678",
wantErr: false,
},
{
name: "valid semver",
buildTag: "1.0.0",
wantErr: false,
},
{
name: "valid underscore prefix",
buildTag: "_build",
wantErr: false,
},
{
name: "valid single char",
buildTag: "1",
wantErr: false,
},
{
name: "valid with dots and dashes",
buildTag: "v1.2.3-beta.1",
wantErr: false,
},
{
name: "invalid: contains space",
buildTag: "my tag",
wantErr: true,
},
{
name: "invalid: contains colon",
buildTag: "my:tag",
wantErr: true,
},
{
name: "invalid: contains slash",
buildTag: "my/tag",
wantErr: true,
},
{
name: "invalid: contains @",
buildTag: "my@tag",
wantErr: true,
},
{
name: "invalid: starts with dot",
buildTag: ".mytag",
wantErr: true,
},
{
name: "invalid: starts with dash",
buildTag: "-mytag",
wantErr: true,
},
{
name: "invalid: exceeds 128 chars",
buildTag: "abbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cfg := &appPushConfig{
buildTag: tt.buildTag,
}
err := cfg.complete()
if tt.wantErr {
require.NotNil(t, err, "expected validation error for buildTag=%q", tt.buildTag)
} else {
require.Nil(t, err, "expected no error for buildTag=%q", tt.buildTag)
}
})
}
}

func Test_resolveImageTag(t *testing.T) {
t.Run("provided tag is used in image name", func(t *testing.T) {
imageTag := "abc1234"
resolvedTag := resolveImageTag(imageTag)
require.Equal(t, "abc1234", resolvedTag)
})

t.Run("empty tag resolves to timestamp format", func(t *testing.T) {
resolvedTag := resolveImageTag("")
require.Regexp(t, `^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}$`, resolvedTag)
})
}
Loading