diff --git a/artifactory/utils/yarn/configget.go b/artifactory/utils/yarn/configget.go index 48d14c3b1..ab529c5c5 100644 --- a/artifactory/utils/yarn/configget.go +++ b/artifactory/utils/yarn/configget.go @@ -6,7 +6,7 @@ import ( "strings" ) -// This method runs "yarn config set" command and sets the yarn configuration. +// This method runs "yarn config get" command and sets the yarn configuration. func ConfigGet(key, executablePath string, jsonOutput bool) (string, error) { var flags []string = nil if jsonOutput { diff --git a/artifactory/utils/yarn/versionVerify.go b/artifactory/utils/yarn/versionVerify.go new file mode 100644 index 000000000..acebfc922 --- /dev/null +++ b/artifactory/utils/yarn/versionVerify.go @@ -0,0 +1,39 @@ +package yarn + +import ( + gofrogcmd "github.com/jfrog/gofrog/io" + "github.com/jfrog/gofrog/version" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "strings" +) + +const unsupportedYarnVersion = "4.0.0" + +func IsInstalledYarnVersionSupported(executablePath string) error { + versionGetCmdConfig := getVersionCmdConfig(executablePath) + output, err := gofrogcmd.RunCmdOutput(versionGetCmdConfig) + if err != nil { + return errorutils.CheckError(err) + } + yarnVersion := strings.TrimSpace(output) + return IsVersionSupported(yarnVersion) +} + +func IsVersionSupported(versionStr string) error { + yarnVersion := version.NewVersion(versionStr) + if yarnVersion.Compare(unsupportedYarnVersion) <= 0 { + return errorutils.CheckErrorf("Yarn version 4 is not supported. The current version is: " + versionStr + + ". Please downgrade to a compatible version to continue") + } + return nil +} + +func getVersionCmdConfig(executablePath string) *YarnConfig { + return &YarnConfig{ + Executable: executablePath, + Command: []string{"--version"}, + CommandFlags: nil, + StrWriter: nil, + ErrWriter: nil, + } +} diff --git a/artifactory/utils/yarn/versionVerify_test.go b/artifactory/utils/yarn/versionVerify_test.go new file mode 100644 index 000000000..0160e291f --- /dev/null +++ b/artifactory/utils/yarn/versionVerify_test.go @@ -0,0 +1,26 @@ +package yarn + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestIsVersionSupported(t *testing.T) { + tests := []struct { + versionStr string + expectErr bool + }{ + {"3.9.0", false}, + {"4.0.0", true}, + {"4.1.0", true}, + } + + for _, test := range tests { + err := IsVersionSupported(test.versionStr) + if test.expectErr { + assert.Error(t, err, "Expected an error for version: %s", test.versionStr) + } else { + assert.NoError(t, err, "Did not expect an error for version: %s", test.versionStr) + } + } +} diff --git a/common/cliutils/spec.go b/common/cliutils/spec.go new file mode 100644 index 000000000..35122e163 --- /dev/null +++ b/common/cliutils/spec.go @@ -0,0 +1,81 @@ +package cliutils + +import ( + speccore "github.com/jfrog/jfrog-cli-core/v2/common/spec" + "github.com/jfrog/jfrog-cli-core/v2/plugins/components" + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + "strings" +) + +func GetSpec(c *components.Context, isDownload, overrideFieldsIfSet bool) (specFiles *speccore.SpecFiles, err error) { + specFiles, err = speccore.CreateSpecFromFile(c.GetStringFlagValue("spec"), coreutils.SpecVarsStringToMap(c.GetStringFlagValue("spec-vars"))) + if err != nil { + return nil, err + } + if isDownload { + trimPatternPrefix(specFiles) + } + if overrideFieldsIfSet { + overrideSpecFields(c, specFiles) + } + return +} + +func overrideSpecFields(c *components.Context, specFiles *speccore.SpecFiles) { + for i := 0; i < len(specFiles.Files); i++ { + OverrideFieldsIfSet(specFiles.Get(i), c) + } +} + +func trimPatternPrefix(specFiles *speccore.SpecFiles) { + for i := 0; i < len(specFiles.Files); i++ { + specFiles.Get(i).Pattern = strings.TrimPrefix(specFiles.Get(i).Pattern, "/") + } +} + +func OverrideFieldsIfSet(spec *speccore.File, c *components.Context) { + overrideArrayIfSet(&spec.Exclusions, c, "exclusions") + overrideArrayIfSet(&spec.SortBy, c, "sort-by") + overrideIntIfSet(&spec.Offset, c, "offset") + overrideIntIfSet(&spec.Limit, c, "limit") + overrideStringIfSet(&spec.SortOrder, c, "sort-order") + overrideStringIfSet(&spec.Props, c, "props") + overrideStringIfSet(&spec.TargetProps, c, "target-props") + overrideStringIfSet(&spec.ExcludeProps, c, "exclude-props") + overrideStringIfSet(&spec.Build, c, "build") + overrideStringIfSet(&spec.Project, c, "project") + overrideStringIfSet(&spec.ExcludeArtifacts, c, "exclude-artifacts") + overrideStringIfSet(&spec.IncludeDeps, c, "include-deps") + overrideStringIfSet(&spec.Bundle, c, "bundle") + overrideStringIfSet(&spec.Recursive, c, "recursive") + overrideStringIfSet(&spec.Flat, c, "flat") + overrideStringIfSet(&spec.Explode, c, "explode") + overrideStringIfSet(&spec.BypassArchiveInspection, c, "bypass-archive-inspection") + overrideStringIfSet(&spec.Regexp, c, "regexp") + overrideStringIfSet(&spec.IncludeDirs, c, "include-dirs") + overrideStringIfSet(&spec.ValidateSymlinks, c, "validate-symlinks") + overrideStringIfSet(&spec.Symlinks, c, "symlinks") + overrideStringIfSet(&spec.Transitive, c, "transitive") + overrideStringIfSet(&spec.PublicGpgKey, c, "gpg-key") +} + +// If `fieldName` exist in the cli args, read it to `field` as a string. +func overrideStringIfSet(field *string, c *components.Context, fieldName string) { + if c.IsFlagSet(fieldName) { + *field = c.GetStringFlagValue(fieldName) + } +} + +// If `fieldName` exist in the cli args, read it to `field` as an array split by `;`. +func overrideArrayIfSet(field *[]string, c *components.Context, fieldName string) { + if c.IsFlagSet(fieldName) { + *field = append([]string{}, strings.Split(c.GetStringFlagValue(fieldName), ";")...) + } +} + +// If `fieldName` exist in the cli args, read it to `field` as a int. +func overrideIntIfSet(field *int, c *components.Context, fieldName string) { + if c.IsFlagSet(fieldName) { + *field, _ = c.GetIntFlagValue(fieldName) + } +} diff --git a/common/cliutils/summary/summary.go b/common/cliutils/summary/summary.go new file mode 100644 index 000000000..62411a629 --- /dev/null +++ b/common/cliutils/summary/summary.go @@ -0,0 +1,128 @@ +package summary + +import ( + "encoding/json" + clientutils "github.com/jfrog/jfrog-client-go/utils" + "github.com/jfrog/jfrog-client-go/utils/errorutils" + "github.com/jfrog/jfrog-client-go/utils/log" + "strings" +) + +type StatusType int + +const ( + Success StatusType = iota + Failure +) + +var StatusTypes = []string{ + "success", + "failure", +} + +func (statusType StatusType) MarshalJSON() ([]byte, error) { + return json.Marshal(StatusTypes[statusType]) +} + +func (statusType *StatusType) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + switch strings.ToLower(s) { + default: + *statusType = Failure + case "success": + *statusType = Success + + } + return nil +} + +func NewBuildInfoSummary(success, failed int, sha256 string, err error) *BuildInfoSummary { + summaryReport := GetSummaryReport(success, failed, false, err) + buildInfoSummary := BuildInfoSummary{Summary: *summaryReport, Sha256Array: []Sha256{}} + if success == 1 { + buildInfoSummary.AddSha256(sha256) + } + return &buildInfoSummary +} + +func (summary *Summary) Marshal() ([]byte, error) { + return json.Marshal(summary) +} + +func (bis *BuildInfoSummary) Marshal() ([]byte, error) { + return json.Marshal(bis) +} + +type Summary struct { + Status StatusType `json:"status"` + Totals *Totals `json:"totals"` +} + +type Totals struct { + Success int `json:"success"` + Failure int `json:"failure"` +} + +type BuildInfoSummary struct { + Summary + Sha256Array []Sha256 `json:"files"` +} + +type Sha256 struct { + Sha256Str string `json:"sha256"` +} + +func (bis *BuildInfoSummary) AddSha256(sha256Str string) { + sha256 := Sha256{Sha256Str: sha256Str} + bis.Sha256Array = append(bis.Sha256Array, sha256) +} + +func GetSummaryReport(success, failed int, failNoOp bool, err error) *Summary { + summary := &Summary{Totals: &Totals{}} + if err != nil || failed > 0 || (success == 0 && failNoOp) { + summary.Status = Failure + } else { + summary.Status = Success + } + summary.Totals.Success = success + summary.Totals.Failure = failed + return summary +} + +func PrintBuildInfoSummaryReport(succeeded bool, sha256 string, originalErr error) error { + success, failed := 1, 0 + if !succeeded { + success, failed = 0, 1 + } + buildInfoSummary, mErr := CreateBuildInfoSummaryReportString(success, failed, sha256, originalErr) + if mErr != nil { + return summaryPrintError(mErr, originalErr) + } + log.Output(buildInfoSummary) + return summaryPrintError(mErr, originalErr) +} + +func CreateBuildInfoSummaryReportString(success, failed int, sha256 string, err error) (string, error) { + buildInfoSummary := NewBuildInfoSummary(success, failed, sha256, err) + buildInfoSummaryContent, mErr := buildInfoSummary.Marshal() + if errorutils.CheckError(mErr) != nil { + return "", mErr + } + return clientutils.IndentJson(buildInfoSummaryContent), mErr +} + +// Print summary report. +// a given non-nil error will pass through and be returned as is if no other errors are raised. +// In case of a nil error, the current function error will be returned. +func summaryPrintError(summaryError, originalError error) error { + if originalError != nil { + if summaryError != nil { + log.Error(summaryError) + } + return originalError + } + return summaryError +} diff --git a/go.mod b/go.mod index 8097b3dee..2c369accc 100644 --- a/go.mod +++ b/go.mod @@ -114,7 +114,7 @@ require ( sigs.k8s.io/yaml v1.4.0 // indirect ) -// replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20250126110945-81abbdde452f +replace github.com/jfrog/jfrog-client-go => github.com/jfrog/jfrog-client-go v1.28.1-0.20250221062042-87cb5136765e // replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20241121100855-e7a75ceee2bd diff --git a/go.sum b/go.sum index 8ad81c86e..7ce4dd48c 100644 --- a/go.sum +++ b/go.sum @@ -112,8 +112,8 @@ github.com/jfrog/build-info-go v1.10.9 h1:mdJ+wlLw2ReFsqC7rifJVlRYLEqYk38uXDYAOZ github.com/jfrog/build-info-go v1.10.9/go.mod h1:JcISnovFXKx3wWf3p1fcMmlPdt6adxScXvoJN4WXqIE= github.com/jfrog/gofrog v1.7.6 h1:QmfAiRzVyaI7JYGsB7cxfAJePAZTzFz0gRWZSE27c6s= github.com/jfrog/gofrog v1.7.6/go.mod h1:ntr1txqNOZtHplmaNd7rS4f8jpA5Apx8em70oYEe7+4= -github.com/jfrog/jfrog-client-go v1.50.0 h1:t7v/zpLkPomHR6ZjVbPQ1WPQJd9IFKESK9Tt6phZz3k= -github.com/jfrog/jfrog-client-go v1.50.0/go.mod h1:xHxwKBjPSUBd/FyCWgusfHmSWKUZTkfOZkTmntC2F5Y= +github.com/jfrog/jfrog-client-go v1.28.1-0.20250221062042-87cb5136765e h1:SGKkXdFJtc5Rb32jh5E55SCLrHXtOTvc9YKy6QwWbzY= +github.com/jfrog/jfrog-client-go v1.28.1-0.20250221062042-87cb5136765e/go.mod h1:hDoUcW6LZme83YNFbdSRImV+di1x0GP+Nlw7fctkwtE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= diff --git a/lifecycle/common.go b/lifecycle/common.go index 5e1b2d0a2..5ec4b4131 100644 --- a/lifecycle/common.go +++ b/lifecycle/common.go @@ -21,6 +21,18 @@ type releaseBundleCmd struct { } func (rbc *releaseBundleCmd) getPrerequisites() (servicesManager *lifecycle.LifecycleServicesManager, + rbDetails services.ReleaseBundleDetails, queryParams services.CommonOptionalQueryParams, err error) { + return rbc.initPrerequisites() +} + +func (rbp *ReleaseBundlePromoteCommand) getPromotionPrerequisites() (servicesManager *lifecycle.LifecycleServicesManager, + rbDetails services.ReleaseBundleDetails, queryParams services.CommonOptionalQueryParams, err error) { + servicesManager, rbDetails, queryParams, err = rbp.initPrerequisites() + queryParams.PromotionType = rbp.promotionType + return servicesManager, rbDetails, queryParams, err +} + +func (rbc *releaseBundleCmd) initPrerequisites() (servicesManager *lifecycle.LifecycleServicesManager, rbDetails services.ReleaseBundleDetails, queryParams services.CommonOptionalQueryParams, err error) { servicesManager, err = utils.CreateLifecycleServiceManager(rbc.serverDetails, false) if err != nil { @@ -34,6 +46,7 @@ func (rbc *releaseBundleCmd) getPrerequisites() (servicesManager *lifecycle.Life ProjectKey: rbc.rbProjectKey, Async: !rbc.sync, } + return } diff --git a/lifecycle/common_test.go b/lifecycle/common_test.go new file mode 100644 index 000000000..8eb58b95c --- /dev/null +++ b/lifecycle/common_test.go @@ -0,0 +1,69 @@ +package lifecycle + +import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/config" + "github.com/jfrog/jfrog-client-go/lifecycle/services" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetPrerequisites_Success(t *testing.T) { + serverDetails := &config.ServerDetails{} + rbCmd := &releaseBundleCmd{ + serverDetails: serverDetails, + releaseBundleName: "testRelease", + releaseBundleVersion: "1.0.0", + sync: true, + rbProjectKey: "project1", + } + + expectedQueryParams := services.CommonOptionalQueryParams{ + ProjectKey: rbCmd.rbProjectKey, + Async: false, + } + + expectedRbDetails := services.ReleaseBundleDetails{ + ReleaseBundleName: rbCmd.releaseBundleName, + ReleaseBundleVersion: rbCmd.releaseBundleVersion, + } + + servicesManager, rbDetails, queryParams, err := rbCmd.getPrerequisites() + + assert.NoError(t, err) + assert.NotNil(t, servicesManager, "Expected servicesManager to be initialized") + assert.Equal(t, expectedRbDetails, rbDetails, "ReleaseBundleDetails does not match expected values") + assert.Equal(t, expectedQueryParams, queryParams, "QueryParams do not match expected values") + +} + +func TestGetPromotionPrerequisites_Success(t *testing.T) { + serverDetails := &config.ServerDetails{} + rbp := &ReleaseBundlePromoteCommand{ + promotionType: "move", + releaseBundleCmd: releaseBundleCmd{ + serverDetails: serverDetails, + releaseBundleName: "testRelease", + releaseBundleVersion: "1.0.0", + sync: true, + rbProjectKey: "project1", + }, + } + + expectedQueryParams := services.CommonOptionalQueryParams{ + ProjectKey: rbp.rbProjectKey, + Async: false, + PromotionType: rbp.promotionType, + } + + expectedRbDetails := services.ReleaseBundleDetails{ + ReleaseBundleName: rbp.releaseBundleName, + ReleaseBundleVersion: rbp.releaseBundleVersion, + } + + servicesManager, rbDetails, queryParams, err := rbp.getPromotionPrerequisites() + + assert.NoError(t, err) + assert.NotNil(t, servicesManager, "Expected servicesManager to be initialized") + assert.Equal(t, expectedRbDetails, rbDetails, "ReleaseBundleDetails do not match expected values") // Replace _ with appropriate variable. + assert.Equal(t, expectedQueryParams, queryParams, "QueryParams do not match expected values") +} diff --git a/lifecycle/promote.go b/lifecycle/promote.go index fb078ff36..285104d98 100644 --- a/lifecycle/promote.go +++ b/lifecycle/promote.go @@ -14,6 +14,7 @@ type ReleaseBundlePromoteCommand struct { environment string includeReposPatterns []string excludeReposPatterns []string + promotionType string } func NewReleaseBundlePromoteCommand() *ReleaseBundlePromoteCommand { @@ -65,6 +66,11 @@ func (rbp *ReleaseBundlePromoteCommand) SetExcludeReposPatterns(excludeReposPatt return rbp } +func (rbp *ReleaseBundlePromoteCommand) SetPromotionType(promotionType string) *ReleaseBundlePromoteCommand { + rbp.promotionType = promotionType + return rbp +} + func (rbp *ReleaseBundlePromoteCommand) CommandName() string { return "rb_promote" } @@ -78,7 +84,8 @@ func (rbp *ReleaseBundlePromoteCommand) Run() error { return err } - servicesManager, rbDetails, queryParams, err := rbp.getPrerequisites() + servicesManager, rbDetails, queryParams, err := rbp.getPromotionPrerequisites() + if err != nil { return err } diff --git a/plugins/common/utils.go b/plugins/common/utils.go index 06250f6ce..1f7655c9b 100644 --- a/plugins/common/utils.go +++ b/plugins/common/utils.go @@ -1,6 +1,8 @@ package common import ( + "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" + clientutils "github.com/jfrog/jfrog-client-go/utils" "sort" "strconv" "strings" @@ -100,3 +102,25 @@ func buildAndSortFlags(keys []string, flagsMap map[string]components.Flag) (flag sort.Slice(flags, func(i, j int) bool { return flags[i].GetName() < flags[j].GetName() }) return } + +// This function indicates whether the command should be executed without +// confirmation warning or not. +// If the --quiet option was sent, it is used to determine whether to prompt the confirmation or not. +// If not, the command will prompt the confirmation, unless the CI environment variable was set to true. +func GetQuietValue(c *components.Context) bool { + if c.IsFlagSet("quiet") { + return c.GetBoolFlagValue("quiet") + } + + return getCiValue() +} + +// Return true if the CI environment variable was set to true. +func getCiValue() bool { + var ci bool + var err error + if ci, err = clientutils.GetBoolEnvValue(coreutils.CI, false); err != nil { + return false + } + return ci +} diff --git a/plugins/components/commandcomp.go b/plugins/components/commandcomp.go index 361f9533c..ca1efb766 100644 --- a/plugins/components/commandcomp.go +++ b/plugins/components/commandcomp.go @@ -171,6 +171,18 @@ func SetHiddenStrFlag() StringFlagOption { } } +func SetMandatoryFalse() StringFlagOption { + return func(f *StringFlag) { + f.Mandatory = false + } +} + +func WithBoolDefaultValueFalse() BoolFlagOption { + return func(f *BoolFlag) { + f.DefaultValue = false + } +} + type BoolFlag struct { BaseFlag DefaultValue bool @@ -201,3 +213,17 @@ func SetHiddenBoolFlag() BoolFlagOption { f.Hidden = true } } + +func (c *Context) WithDefaultIntFlagValue(flagName string, defValue int) (value int, err error) { + value = defValue + if c.IsFlagSet(flagName) { + var parsed int64 + parsed, err = strconv.ParseInt(c.GetStringFlagValue(flagName), 0, 64) + if err != nil { + err = fmt.Errorf("can't parse int flag '%s': %w", flagName, err) + return + } + value = int(parsed) + } + return +} diff --git a/utils/dependencies/utils.go b/utils/dependencies/utils.go index b683be8f3..b486f4952 100644 --- a/utils/dependencies/utils.go +++ b/utils/dependencies/utils.go @@ -20,7 +20,7 @@ import ( const ( ChecksumFileName = "checksum.sha2" - jarsDocumentation = "https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/cli-for-jfrog-artifactory/package-managers-integration#downloading-the-maven-and-gradle-extractor-jars" + jarsDocumentation = "https://docs.jfrog-applications.jfrog.io/jfrog-applications/jfrog-cli/binaries-management-with-jfrog-artifactory/package-managers-integration#downloading-the-maven-and-gradle-extractor-jars" ) // Download the relevant build-info-extractor jar.