diff --git a/buildtools/cli.go b/buildtools/cli.go index f792880c7..18be55f9c 100644 --- a/buildtools/cli.go +++ b/buildtools/cli.go @@ -14,6 +14,7 @@ import ( "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/container/strategies" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/python" "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/setup" + artutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/ioutils" "github.com/jfrog/jfrog-cli-security/utils/techutils" @@ -558,8 +559,13 @@ func MvnCmd(c *cli.Context) (err error) { return err } - // FlexPack bypasses all config file requirements - if os.Getenv("JFROG_RUN_NATIVE") == "true" { + configFilePath, configExists, err := project.GetProjectConfFilePath(project.Maven) + if err != nil { + return err + } + + // FlexPack bypasses all config file requirements (only when no config exists) + if artutils.ShouldRunNative(configFilePath) && !configExists { log.Debug("Routing to Maven native implementation") // Extract build configuration for FlexPack args := cliutils.ExtractCommand(c) @@ -572,9 +578,11 @@ func MvnCmd(c *cli.Context) (err error) { return commands.Exec(mvnCmd) } - configFilePath, err := getProjectConfigPathOrThrow(project.Maven, "mvn", "mvn-config") - if err != nil { - return err + // If config file is missing and not in native mode, return the standard missing-config error. + if !configExists { + if configFilePath, err = getProjectConfigPathOrThrow(project.Maven, "mvn", "mvn-config"); err != nil { + return err + } } if c.NArg() < 1 { @@ -629,10 +637,63 @@ func GradleCmd(c *cli.Context) (err error) { return err } - configFilePath, err := getProjectConfigPathOrThrow(project.Gradle, "gradle", "gradle-config") + resolveServer := func(args []string) ([]string, *coreConfig.ServerDetails, error) { + cleanedArgs, serverID, err := coreutils.ExtractServerIdFromCommand(args) + if err != nil { + return nil, nil, fmt.Errorf("failed to extract server ID: %w", err) + } + + if serverID == "" { + serverDetails, err := coreConfig.GetDefaultServerConf() + if err != nil { + return cleanedArgs, nil, err + } + if serverDetails == nil { + return cleanedArgs, nil, fmt.Errorf("no default server configuration found. Please configure a server using 'jfrog config add' or specify a server using --server-id") + } + return cleanedArgs, serverDetails, nil + } + + serverDetails, err := coreConfig.GetSpecificConfig(serverID, true, true) + if err != nil { + return nil, nil, fmt.Errorf("failed to get server configuration for ID '%s': %w", serverID, err) + } + return cleanedArgs, serverDetails, nil + } + + configFilePath, configExists, err := project.GetProjectConfFilePath(project.Gradle) if err != nil { return err } + nativeMode := artutils.ShouldRunNative(configFilePath) + + // FlexPack native mode for Gradle (bypasses config file requirements) + if nativeMode && !configExists { + log.Debug("Routing to Gradle FlexPack implementation") + if c.NArg() < 1 { + return cliutils.WrongNumberOfArgumentsHandler(c) + } + args := cliutils.ExtractCommand(c) + args, serverDetails, err := resolveServer(args) + if err != nil { + return err + } + filteredGradleArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) + if err != nil { + return err + } + + // Create Gradle command with FlexPack (no config file needed) + gradleCmd := gradle.NewGradleCommand().SetConfiguration(buildConfiguration).SetTasks(filteredGradleArgs).SetConfigPath("").SetServerDetails(serverDetails) + return commands.Exec(gradleCmd) + } + + // If config file is missing and not in native mode, return the standard missing-config error. + if !configExists { + if configFilePath, err = getProjectConfigPathOrThrow(project.Gradle, "gradle", "gradle-config"); err != nil { + return err + } + } // Found a config file. Continue as native command. if c.NArg() < 1 { @@ -1508,7 +1569,7 @@ func pythonCmd(c *cli.Context, projectType project.ProjectType) error { } // FlexPack native mode for Poetry (bypasses config file requirements) - if os.Getenv("JFROG_RUN_NATIVE") == "true" && projectType == project.Poetry { + if artutils.ShouldRunNative("") && projectType == project.Poetry { log.Debug("Routing to Poetry native implementation") args := cliutils.ExtractCommand(c) filteredArgs, buildConfiguration, err := build.ExtractBuildDetailsFromArgs(args) diff --git a/go.mod b/go.mod index 11588d174..e7982dd47 100644 --- a/go.mod +++ b/go.mod @@ -282,9 +282,9 @@ replace github.com/docker/docker => github.com/docker/docker v27.5.1+incompatibl replace github.com/gfleury/go-bitbucket-v1 => github.com/gfleury/go-bitbucket-v1 v0.0.0-20230825095122-9bc1711434ab -//replace github.com/jfrog/jfrog-cli-artifactory => github.com/naveenku-jfrog/jfrog-cli-artifactory v0.0.0-20251210184507-0c8d138690cb +replace github.com/jfrog/jfrog-cli-artifactory => github.com/nitinp19/jfrog-cli-artifactory v0.0.0-20251218130310-3bde47e0f6e2 -//replace github.com/jfrog/build-info-go => github.com/jfrog/build-info-go v1.8.9-0.20251006061821-8b1be6a65215 +replace github.com/jfrog/build-info-go => github.com/nitinp19/build-info-go v1.12.3-0.20251218125859-b4b775c8b360 // replace github.com/jfrog/jfrog-cli-core/v2 => github.com/reshmifrog/jfrog-cli-core/v2 v2.58.5-0.20251110095124-ab9a4102ec5a diff --git a/go.sum b/go.sum index 49469e87f..c1b24cf19 100644 --- a/go.sum +++ b/go.sum @@ -1204,8 +1204,6 @@ github.com/jellydator/ttlcache/v3 v3.4.0 h1:YS4P125qQS0tNhtL6aeYkheEaB/m8HCqdMMP github.com/jellydator/ttlcache/v3 v3.4.0/go.mod h1:Hw9EgjymziQD3yGsQdf1FqFdpp7YjFMd4Srg5EJlgD4= github.com/jfrog/archiver/v3 v3.6.1 h1:LOxnkw9pOn45DzCbZNFV6K0+6dCsQ0L8mR3ZcujO5eI= github.com/jfrog/archiver/v3 v3.6.1/go.mod h1:VgR+3WZS4N+i9FaDwLZbq+jeU4B4zctXL+gL4EMzfLw= -github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9 h1:CL7lp7Y7srwQ1vy1btX66t4wbztzEGQbqi/9tdEz7xk= -github.com/jfrog/build-info-go v1.12.5-0.20251209171349-eb030db986f9/go.mod h1:9W4U440fdTHwW1HiB/R0VQvz/5q8ZHsms9MWcq+JrdY= github.com/jfrog/froggit-go v1.20.6 h1:Xp7+LlEh0m1KGrQstb+u0aGfjRUtv1eh9xQBV3571jQ= github.com/jfrog/froggit-go v1.20.6/go.mod h1:obSG1SlsWjktkuqmKtpq7MNTTL63e0ot+ucTnlOMV88= github.com/jfrog/go-mockhttp v0.3.1 h1:/wac8v4GMZx62viZmv4wazB5GNKs+GxawuS1u3maJH8= @@ -1216,8 +1214,6 @@ github.com/jfrog/jfrog-apps-config v1.0.1 h1:mtv6k7g8A8BVhlHGlSveapqf4mJfonwvXYL github.com/jfrog/jfrog-apps-config v1.0.1/go.mod h1:8AIIr1oY9JuH5dylz2S6f8Ym2MaadPLR6noCBO4C22w= github.com/jfrog/jfrog-cli-application v1.0.2-0.20251210075951-519050602a7f h1:YHwPNF13Gmt65J618kEGn/4SGewXqyYuAdcyYWYdrBA= github.com/jfrog/jfrog-cli-application v1.0.2-0.20251210075951-519050602a7f/go.mod h1:xum2HquWO5uExa/A7MQs3TgJJVEeoqTR+6Z4mfBr1Xw= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251218044417-5113b260e416 h1:bCCsZ/2oVgcxeWVgUG8K0bE1I7JFZ+O6CISzwwEfrV8= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20251218044417-5113b260e416/go.mod h1:7cCaRhXorlbyXZgiW5bplCExFxlnROaG21K12d8inpQ= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5 h1:GYE67ubwl+ZRw3CcXFUi49EwwQp6k+qS8sX0QuHDHO8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20251210085744-f8481d179ac5/go.mod h1:BMoGi2rG0udCCeaghqlNgiW3fTmT+TNnfTnBoWFYgcg= github.com/jfrog/jfrog-cli-evidence v0.8.3-0.20251204144808-73fa744851c0 h1:8S1vE1PeVtrzWkKL0N39cX6XLLNV0It+f6xjRKjw7Ug= @@ -1327,6 +1323,10 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nitinp19/build-info-go v1.12.3-0.20251218125859-b4b775c8b360 h1:9XxSgWkh9gr9GALi0NHDlfU7kpVUtLbsYhsUG59/N5E= +github.com/nitinp19/build-info-go v1.12.3-0.20251218125859-b4b775c8b360/go.mod h1:9W4U440fdTHwW1HiB/R0VQvz/5q8ZHsms9MWcq+JrdY= +github.com/nitinp19/jfrog-cli-artifactory v0.0.0-20251218130310-3bde47e0f6e2 h1:HjiGdPDAl0Q79ZMUmA7YpBOkAZylVa3MvpFm24VWlcM= +github.com/nitinp19/jfrog-cli-artifactory v0.0.0-20251218130310-3bde47e0f6e2/go.mod h1:iMc/mGpwoCYc+9+0GwfpnNKH+RpOrUb7FSLeckVjFZQ= github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc= github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= diff --git a/gradle_test.go b/gradle_test.go index 84a060c15..5275bb70e 100644 --- a/gradle_test.go +++ b/gradle_test.go @@ -3,11 +3,6 @@ package main import ( "errors" "fmt" - "github.com/jfrog/gofrog/io" - "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/gradle" - coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" - "github.com/jfrog/jfrog-client-go/http/httpclient" - "github.com/stretchr/testify/require" "net/http" "os" "os/exec" @@ -15,6 +10,13 @@ import ( "strings" "testing" + "github.com/jfrog/gofrog/io" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/commands/gradle" + coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-client-go/http/httpclient" + "github.com/stretchr/testify/require" + urfavecli "github.com/urfave/cli" + "github.com/jfrog/jfrog-cli/utils/cliutils" "github.com/jfrog/jfrog-client-go/utils/log" @@ -134,7 +136,8 @@ func TestGradleBuildWithServerID(t *testing.T) { return } buildInfo := publishedBuildInfo.BuildInfo - validateBuildInfo(buildInfo, t, 0, 1, gradleModuleId, buildinfo.Gradle) + // Expect 1 dependency (junit:4.7) since the project has source code using JUnit + validateBuildInfo(buildInfo, t, 1, 1, gradleModuleId, buildinfo.Gradle) cleanGradleTest(t) } @@ -183,7 +186,8 @@ func TestGradleBuildWithServerIDAndDetailedSummary(t *testing.T) { return } buildInfo := publishedBuildInfo.BuildInfo - validateBuildInfo(buildInfo, t, 0, 1, gradleModuleId, buildinfo.Gradle) + // Expect 1 dependency (junit:4.7) since the project has source code using JUnit + validateBuildInfo(buildInfo, t, 1, 1, gradleModuleId, buildinfo.Gradle) cleanGradleTest(t) } @@ -254,14 +258,360 @@ func TestSetupGradleCommand(t *testing.T) { } } +// TestGradleBuildWithFlexPack tests Gradle build with JFROG_RUN_NATIVE=true (FlexPack mode) +func TestGradleBuildWithFlexPack(t *testing.T) { + initGradleTest(t) + + // Check if Gradle is available in the environment + if _, err := exec.LookPath("gradle"); err != nil { + t.Skip("Gradle not found in PATH, skipping Gradle FlexPack test") + } + + buildGradlePath := createGradleProject(t, "gradleproject") + oldHomeDir := changeWD(t, filepath.Dir(buildGradlePath)) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + // Set environment for native FlexPack implementation + setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallBack() + + // Windows compatibility + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + + // Run gradle without config file to trigger FlexPack mode + err := runJfrogCliWithoutAssertion("gradle", "clean", "build", "-b"+buildGradlePath) + assert.NoError(t, err) + + cleanGradleTest(t) +} + +// TestGradleBuildWithFlexPackBuildInfo tests Gradle build info collection with JFROG_RUN_NATIVE=true +func TestGradleBuildWithFlexPackBuildInfo(t *testing.T) { + initGradleTest(t) + + // Check if Gradle is available in the environment + if _, err := exec.LookPath("gradle"); err != nil { + t.Skip("Gradle not found in PATH, skipping Gradle FlexPack build info test") + } + + buildGradlePath := createGradleProject(t, "gradleproject") + oldHomeDir := changeWD(t, filepath.Dir(buildGradlePath)) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + buildName := tests.GradleBuildName + "-flexpack" + buildNumber := "1" + + // Set environment for native FlexPack implementation + setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallBack() + + // Windows compatibility + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + + // Run gradle with build info (FlexPack mode - no config file) + err := runJfrogCliWithoutAssertion("gradle", "clean", "build", "-b"+buildGradlePath, "--build-name="+buildName, "--build-number="+buildNumber) + assert.NoError(t, err) + + // Publish build info + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + // Validate build info was created with FlexPack dependencies + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found") + return + } + + // Validate build info structure + buildInfo := publishedBuildInfo.BuildInfo + assert.NotEmpty(t, buildInfo.Modules, "Build info should have modules") + if len(buildInfo.Modules) > 0 { + module := buildInfo.Modules[0] + assert.Equal(t, buildinfo.Gradle, module.Type, "Module type should be Gradle") + assert.NotEmpty(t, module.Id, "Module should have ID") + + // FlexPack should collect dependencies with scopes and checksums. + assert.NotEmpty(t, module.Dependencies, "FlexPack build info should include dependencies") + for _, dep := range module.Dependencies { + assert.NotEmpty(t, dep.Id, "Dependency should have ID") + // FlexPack should provide checksums + hasChecksum := dep.Sha1 != "" || dep.Sha256 != "" || dep.Md5 != "" + assert.True(t, hasChecksum, "Dependency %s should have at least one checksum", dep.Id) + // FlexPack should provide scopes (Gradle configurations) + assert.NotEmpty(t, dep.Scopes, "Dependency %s should have scopes/configurations", dep.Id) + } + } + + cleanGradleTest(t) +} + +// TestGradleBuildWithFlexPackEnvVars tests build info collection using JFROG_CLI_BUILD_NAME and JFROG_CLI_BUILD_NUMBER +func TestGradleBuildWithFlexPackEnvVars(t *testing.T) { + initGradleTest(t) + + // Check if Gradle is available in the environment + if _, err := exec.LookPath("gradle"); err != nil { + t.Skip("Gradle not found in PATH, skipping Gradle FlexPack env vars test") + } + + buildGradlePath := createGradleProject(t, "gradleproject") + oldHomeDir := changeWD(t, filepath.Dir(buildGradlePath)) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + buildName := tests.GradleBuildName + "-flexpack-env" + buildNumber := "123" + + // Set environment for native FlexPack implementation + setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallBack() + + // Set build name and number via environment variables + setBuildNameCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_CLI_BUILD_NAME", buildName) + defer setBuildNameCallback() + setBuildNumberCallback := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_CLI_BUILD_NUMBER", buildNumber) + defer setBuildNumberCallback() + + // Windows compatibility + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + + // Run gradle without explicit build name/number flags - should use env vars + err := runJfrogCliWithoutAssertion("gradle", "clean", "build", "-b"+buildGradlePath) + assert.NoError(t, err) + + // Publish build info + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + // Validate build info was created with the env var values + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found with env var build name/number") + return + } + + // Validate build info has the correct build name and number + assert.Equal(t, buildName, publishedBuildInfo.BuildInfo.Name, "Build name should match env var") + assert.Equal(t, buildNumber, publishedBuildInfo.BuildInfo.Number, "Build number should match env var") + + cleanGradleTest(t) +} + +// TestGradleBuildWithFlexPackInvalidArgs tests 'jf gradle build' with invalid Gradle arguments +func TestGradleBuildWithFlexPackInvalidArgs(t *testing.T) { + initGradleTest(t) + + // Check if Gradle is available in the environment + if _, err := exec.LookPath("gradle"); err != nil { + t.Skip("Gradle not found in PATH, skipping Gradle FlexPack invalid args test") + } + + // The CLI uses urfave/cli, which may call os.Exit(1) for ExitError (e.g., external tool exit code). + origOsExiter := urfavecli.OsExiter + urfavecli.OsExiter = func(code int) {} + defer func() { urfavecli.OsExiter = origOsExiter }() + + buildGradlePath := createGradleProject(t, "gradleproject") + oldHomeDir := changeWD(t, filepath.Dir(buildGradlePath)) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + // Set environment for native FlexPack implementation + setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallBack() + + // Windows compatibility + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + + // Run gradle with invalid task name - should fail + err := runJfrogCliWithoutAssertion("gradle", "nonExistentTask", "-b"+buildGradlePath) + assert.Error(t, err, "Gradle should fail with invalid task name") + + // Run gradle with invalid option + err = runJfrogCliWithoutAssertion("gradle", "build", "--invalid-option-xyz", "-b"+buildGradlePath) + assert.Error(t, err, "Gradle should fail with invalid option") + + // This test intentionally fails before any deployment/build-info publish. + clientTestUtils.UnSetEnvAndAssert(t, coreutils.HomeDir) + tests.CleanFileSystem() +} + +// TestGradleBuildWithFlexPackFallback verifies that gradle falls back to traditional approach +// when JFROG_RUN_NATIVE is not set (covered by existing traditional tests, this is explicit verification) +func TestGradleBuildWithFlexPackFallback(t *testing.T) { + initGradleTest(t) + + // Explicitly ensure JFROG_RUN_NATIVE is NOT set + _ = os.Unsetenv("JFROG_RUN_NATIVE") + + buildGradlePath := createGradleProject(t, "gradleproject") + configFilePath := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "buildspecs", tests.GradleConfig) + destPath := filepath.Join(filepath.Dir(buildGradlePath), ".jfrog", "projects") + createConfigFile(destPath, configFilePath, t) + + // Create the search spec before changing the working directory. + // tests.GetTestResourcesPath() is relative ("testdata"), so creating the spec after chdir may fail on CI. + searchSpec, err := tests.CreateSpec(tests.SearchAllGradle) + require.NoError(t, err) + + oldHomeDir := changeWD(t, filepath.Dir(buildGradlePath)) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + buildName := tests.GradleBuildName + "-fallback" + buildNumber := "1" + + // Windows compatibility + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + + // Run gradle with config file (traditional approach) + runJfrogCli(t, "gradle", "clean", "artifactoryPublish", "-b"+buildGradlePath, "--build-name="+buildName, "--build-number="+buildNumber) + + // Validate artifacts were deployed (traditional approach deploys to Artifactory) + inttestutils.VerifyExistInArtifactory(tests.GetGradleDeployedArtifacts(), searchSpec, serverDetails, t) + + cleanGradleTest(t) +} + +// TestGradleHelpCommand verifies 'jf gradle --help' displays correct usage instructions +func TestGradleHelpCommand(t *testing.T) { + initGradleTest(t) + + // Run gradle help command + jfrogCli := coretests.NewJfrogCli(execMain, "jfrog", "") + err := jfrogCli.Exec("gradle", "--help") + // Help command should succeed + assert.NoError(t, err, "gradle --help should succeed") + + cleanGradleTest(t) +} + +// TestGradleBuildWithFlexPackKotlinDSL tests build info collection for Kotlin DSL (build.gradle.kts) +// Note: This test requires a Kotlin DSL project to be available +func TestGradleBuildWithFlexPackKotlinDSL(t *testing.T) { + initGradleTest(t) + + // Check if Gradle is available in the environment + if _, err := exec.LookPath("gradle"); err != nil { + t.Skip("Gradle not found in PATH, skipping Gradle FlexPack Kotlin DSL test") + } + + // Check if Kotlin DSL project exists + kotlinProjectPath := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "gradle", "kotlinproject") + if _, err := os.Stat(kotlinProjectPath); os.IsNotExist(err) { + t.Skip("Kotlin DSL project not found, skipping Kotlin DSL test") + } + + // Create gradle project with Kotlin DSL + buildGradlePath := createGradleProjectKotlin(t, "kotlinproject") + oldHomeDir := changeWD(t, filepath.Dir(buildGradlePath)) + defer clientTestUtils.ChangeDirAndAssert(t, oldHomeDir) + + buildName := tests.GradleBuildName + "-flexpack-kotlin" + buildNumber := "1" + + // Set environment for native FlexPack implementation + setEnvCallBack := clientTestUtils.SetEnvWithCallbackAndAssert(t, "JFROG_RUN_NATIVE", "true") + defer setEnvCallBack() + + // Windows compatibility + buildGradlePath = strings.ReplaceAll(buildGradlePath, `\`, "/") + + // Run gradle build + err := runJfrogCliWithoutAssertion("gradle", "clean", "build", "-b"+buildGradlePath, "--build-name="+buildName, "--build-number="+buildNumber) + assert.NoError(t, err) + + // Publish build info + assert.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + // Validate build info was created + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + if err != nil { + assert.NoError(t, err) + return + } + if !found { + assert.True(t, found, "build info was expected to be found") + return + } + + // Validate build info structure + buildInfo := publishedBuildInfo.BuildInfo + assert.NotEmpty(t, buildInfo.Modules, "Build info should have modules") + if len(buildInfo.Modules) > 0 { + module := buildInfo.Modules[0] + assert.Equal(t, buildinfo.Gradle, module.Type, "Module type should be Gradle") + } + + cleanGradleTest(t) +} + func createGradleProject(t *testing.T, projectName string) string { - srcBuildFile := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "gradle", projectName, "build.gradle") - buildGradlePath, err := tests.ReplaceTemplateVariables(srcBuildFile, "") + // Copy the entire project directory including source files + projectSrc := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "gradle", projectName) + projectTarget := filepath.Join(tests.Temp, projectName) + // Use absolute paths to avoid issues when tests change working directory and pass relative -b flags. + // (e.g. running from tmp/gradleproject and passing -btmp/gradleproject/build.gradle results in duplicated paths) + var err error + projectTarget, err = filepath.Abs(projectTarget) + assert.NoError(t, err) + err = io.CopyDir(projectSrc, projectTarget, true, nil) + assert.NoError(t, err) + + // Replace template variables in build.gradle + srcBuildFile := filepath.Join(projectTarget, "build.gradle") + buildGradlePath, err := tests.ReplaceTemplateVariables(srcBuildFile, projectTarget) + assert.NoError(t, err) + buildGradlePath, err = filepath.Abs(buildGradlePath) + assert.NoError(t, err) + assert.FileExists(t, buildGradlePath) + // Gradle accepts forward slashes on all platforms, and this avoids Windows path edge-cases. + buildGradlePath = filepath.ToSlash(buildGradlePath) + + // Replace template variables in settings.gradle + srcSettingsFile := filepath.Join(projectTarget, "settings.gradle") + settingsPath, err := tests.ReplaceTemplateVariables(srcSettingsFile, projectTarget) + assert.NoError(t, err) + settingsPath, err = filepath.Abs(settingsPath) + assert.NoError(t, err) + assert.FileExists(t, settingsPath) + + return buildGradlePath +} + +// createGradleProjectKotlin creates a Kotlin DSL gradle project for testing +func createGradleProjectKotlin(t *testing.T, projectName string) string { + // Copy the entire project directory including source files + projectSrc := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "gradle", projectName) + projectTarget := filepath.Join(tests.Temp, projectName) + // Use absolute paths to avoid issues when tests change working directory and pass relative -b flags. + var err error + projectTarget, err = filepath.Abs(projectTarget) + assert.NoError(t, err) + err = io.CopyDir(projectSrc, projectTarget, true, nil) + assert.NoError(t, err) + + // Replace template variables in build.gradle.kts + srcBuildFile := filepath.Join(projectTarget, "build.gradle.kts") + buildGradlePath, err := tests.ReplaceTemplateVariables(srcBuildFile, projectTarget) + assert.NoError(t, err) + buildGradlePath, err = filepath.Abs(buildGradlePath) assert.NoError(t, err) + assert.FileExists(t, buildGradlePath) + // Gradle accepts forward slashes on all platforms, and this avoids Windows path edge-cases. + buildGradlePath = filepath.ToSlash(buildGradlePath) - srcSettingsFile := filepath.Join(filepath.FromSlash(tests.GetTestResourcesPath()), "gradle", projectName, "settings.gradle") - _, err = tests.ReplaceTemplateVariables(srcSettingsFile, "") + // Replace template variables in settings.gradle.kts + srcSettingsFile := filepath.Join(projectTarget, "settings.gradle.kts") + settingsPath, err := tests.ReplaceTemplateVariables(srcSettingsFile, projectTarget) + assert.NoError(t, err) + settingsPath, err = filepath.Abs(settingsPath) assert.NoError(t, err) + assert.FileExists(t, settingsPath) return buildGradlePath } @@ -270,6 +620,8 @@ func initGradleTest(t *testing.T) { if !*tests.TestGradle { t.Skip("Skipping Gradle test. To run Gradle test add the '-test.gradle=true' option.") } + // Ensure clean state - unset native flag so traditional tests run correctly + _ = os.Unsetenv("JFROG_RUN_NATIVE") createJfrogHomeConfig(t, true) } diff --git a/inttestutils/artifactory.go b/inttestutils/artifactory.go index 29d6a5481..1448a4c99 100644 --- a/inttestutils/artifactory.go +++ b/inttestutils/artifactory.go @@ -9,6 +9,7 @@ import ( "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli/utils/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) // Verify the input slice exist in Artifactory @@ -17,23 +18,39 @@ import ( // serverDetails - Target Artifactory server details // t - Tests object func VerifyExistInArtifactory(expected []string, specFile string, serverDetails *config.ServerDetails, t *testing.T) { - results, _ := SearchInArtifactory(specFile, serverDetails, t) + results, err := SearchInArtifactory(specFile, serverDetails, t) + require.NoError(t, err) tests.CompareExpectedVsActual(expected, results, t) } func SearchInArtifactory(specFile string, serverDetails *config.ServerDetails, t *testing.T) ([]utils.SearchResult, error) { - searchSpec, _ := spec.CreateSpecFromFile(specFile, nil) + searchSpec, err := spec.CreateSpecFromFile(specFile, nil) + if err != nil { + return nil, err + } + if searchSpec == nil { + return nil, assert.AnError + } searchCmd := generic.NewSearchCommand() searchCmd.SetServerDetails(serverDetails).SetSpec(searchSpec) reader, err := searchCmd.Search() - assert.NoError(t, err) + if err != nil { + return nil, err + } var resultItems []utils.SearchResult readerNoDate, err := utils.SearchResultNoDate(reader) - assert.NoError(t, err) + if err != nil { + _ = reader.Close() + return nil, err + } for searchResult := new(utils.SearchResult); readerNoDate.NextRecord(searchResult) == nil; searchResult = new(utils.SearchResult) { resultItems = append(resultItems, *searchResult) } - assert.NoError(t, reader.Close(), "Couldn't close reader") - assert.NoError(t, reader.GetError(), "Couldn't get reader error") - return resultItems, err + if cerr := reader.Close(); cerr != nil { + return resultItems, cerr + } + if rerr := reader.GetError(); rerr != nil { + return resultItems, rerr + } + return resultItems, nil } diff --git a/main_test.go b/main_test.go index 4d396585e..3921882b6 100644 --- a/main_test.go +++ b/main_test.go @@ -263,8 +263,10 @@ func createConfigFile(inDir, configFilePath string, t *testing.T) { if _, err := os.Stat(inDir); os.IsNotExist(err) { assert.NoError(t, os.MkdirAll(inDir, 0o777)) } - _, err := tests.ReplaceTemplateVariables(configFilePath, inDir) + createdPath, err := tests.ReplaceTemplateVariables(configFilePath, inDir) assert.NoError(t, err) + // Fail early with a descriptive error if the config file wasn't actually created. + assert.FileExists(t, createdPath) } // Validate that all CLI commands' aliases are unique, and that two commands don't use the same alias. diff --git a/testdata/gradle/gradleproject/build.gradle b/testdata/gradle/gradleproject/build.gradle index eeac79246..cd34354f7 100644 --- a/testdata/gradle/gradleproject/build.gradle +++ b/testdata/gradle/gradleproject/build.gradle @@ -1,6 +1,26 @@ apply plugin: 'groovy' apply plugin: 'idea' +repositories { + mavenCentral() +} + +// Gradle 8+ blocks http:// repositories unless allowInsecureProtocol=true is set. +// The JFrog Gradle integration (traditional extractor flow) may add an Artifactory resolver repo using http://localhost in tests. +// This block is safe across Gradle versions because it checks for the property before setting it. +allprojects { + repositories.all { repo -> + try { + def urlStr = repo.respondsTo('getUrl') && repo.url != null ? repo.url.toString() : "" + if (urlStr.startsWith("http://") && repo.hasProperty("allowInsecureProtocol")) { + repo.allowInsecureProtocol = true + } + } catch (Throwable ignored) { + // Best-effort; ignore repositories that don't support URL/allowInsecureProtocol. + } + } +} + dependencies { implementation "junit:junit:4.7" } diff --git a/testdata/gradle/gradleproject/src/main/java/example/Example.java b/testdata/gradle/gradleproject/src/main/java/example/Example.java new file mode 100644 index 000000000..9827ad7d5 --- /dev/null +++ b/testdata/gradle/gradleproject/src/main/java/example/Example.java @@ -0,0 +1,19 @@ +package example; + +/** + * Simple example class for the gradle test project. + * Note: We intentionally don't use JUnit here because dependency resolution + * may not work when the JFrog Gradle plugin configures the resolver to use + * the test Artifactory repository (which may not have JUnit cached). + */ +public class Example { + public boolean isTrue() { + return true; + } + + public static void main(String[] args) { + Example example = new Example(); + System.out.println("Example.isTrue() = " + example.isTrue()); + } +} + diff --git a/testdata/gradle/projectwithplugin/build.gradle b/testdata/gradle/projectwithplugin/build.gradle index d06c9fa99..796fcb9b3 100644 --- a/testdata/gradle/projectwithplugin/build.gradle +++ b/testdata/gradle/projectwithplugin/build.gradle @@ -11,6 +11,22 @@ apply plugin: 'idea' apply plugin: 'com.jfrog.artifactory' apply plugin: 'maven-publish' +// Gradle 8+ blocks http:// repositories unless allowInsecureProtocol=true is set. +// The JFrog Gradle integration / Artifactory plugin may add an Artifactory resolver repo using http://localhost in tests. +// This block is safe across Gradle versions because it checks for the property before setting it. +allprojects { + repositories.all { repo -> + try { + def urlStr = repo.respondsTo('getUrl') && repo.url != null ? repo.url.toString() : "" + if (urlStr.startsWith("http://") && repo.hasProperty("allowInsecureProtocol")) { + repo.allowInsecureProtocol = true + } + } catch (Throwable ignored) { + // Best-effort; ignore repositories that don't support URL/allowInsecureProtocol. + } + } +} + version = 1.0 task initProject(description: 'Initialize project directory structure.') { doLast { diff --git a/utils/buildinfo/buildinfo.go b/utils/buildinfo/buildinfo.go index 1dedee1fb..4952c9a55 100644 --- a/utils/buildinfo/buildinfo.go +++ b/utils/buildinfo/buildinfo.go @@ -12,6 +12,7 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/build-info-go/flexpack" "github.com/jfrog/gofrog/crypto" + artutils "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build" "github.com/jfrog/jfrog-cli-core/v2/common/project" @@ -302,7 +303,7 @@ func extractRepositoryConfigForProject(projectType project.ProjectType) (*projec log.Debug("Extracting repository config for project type") // In native mode, completely ignore YAML and infer from pyproject.toml - if os.Getenv("JFROG_RUN_NATIVE") == "true" { + if artutils.ShouldRunNative("") { log.Info("Native mode enabled: inferring Poetry config from pyproject.toml") return inferPoetryConfigFromToml(projectType) } diff --git a/utils/cliutils/commandsflags.go b/utils/cliutils/commandsflags.go index 68ffdd03c..15dbf09bf 100644 --- a/utils/cliutils/commandsflags.go +++ b/utils/cliutils/commandsflags.go @@ -1860,7 +1860,7 @@ var commandFlags = map[string][]string{ BuildName, BuildNumber, deploymentThreads, InsecureTls, Project, detailedSummary, xrayScan, xrOutput, }, Gradle: { - BuildName, BuildNumber, deploymentThreads, Project, detailedSummary, xrayScan, xrOutput, + BuildName, BuildNumber, deploymentThreads, Project, serverId, detailedSummary, xrayScan, xrOutput, }, Docker: { BuildName, BuildNumber, module, Project,