diff --git a/artifactory_test.go b/artifactory_test.go index 9d5bd245b..3307be26f 100644 --- a/artifactory_test.go +++ b/artifactory_test.go @@ -6922,6 +6922,51 @@ func terraformPublishModulesAndBuildInfo(t *testing.T, trPublishArgs []string) { assert.Len(t, buildInfo.Modules[0].Artifacts, 3) } +func TestTerraformPublishWithLocalGitVcsProps(t *testing.T) { + initArtifactoryTest(t, terraformMinArtifactoryVersion) + defer cleanArtifactoryTest() + createJfrogHomeConfig(t, true) + + buildNumber := "local-git-1" + buildName := tests.RtBuildName1 + "-local-git" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + projectPath := prepareTerraformProject("terraformproject", t, true) + tests.CopyGitFixtureIntoProject(t, projectPath) + + wd, err := os.Getwd() + require.NoError(t, err) + awsDir := filepath.Join(projectPath, "aws") + chdirCallback := clientTestUtils.ChangeDirWithCallback(t, wd, awsDir) + defer chdirCallback() + + trPublishArgs := []string{ + "terraform", "publish", + "--namespace=namespace", "--provider=provider", "--tag=tag", + "--exclusions=*test*", + "--build-name=" + buildName, "--build-number=" + buildNumber, + "--module=my-tr-module-local-git", + } + require.NoError(t, platformCli.WithoutCredentials().Exec(trPublishArgs...)) + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.TerraformRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + func prepareTerraformProject(projectName string, t *testing.T, copyDirs bool) string { projectPath := filepath.Join(tests.GetTestResourcesPath(), "terraform", projectName) testdataTarget := filepath.Join(tests.Out, "terraformProject") diff --git a/docker_test.go b/docker_test.go index f0ac37a0a..2b9752f66 100644 --- a/docker_test.go +++ b/docker_test.go @@ -1574,6 +1574,55 @@ CMD ["echo", "Hello from CI VCS test"]`, baseImage) assert.Greater(t, artifactCount, 0, "No artifacts in build info") } +// TestDockerPushWithLocalGitVcsProps verifies local git VCS props on Docker artifacts +// when running build-publish with VCS collection enabled and no CI env. +func TestDockerPushWithLocalGitVcsProps(t *testing.T) { + cleanup := initDockerBuildTest(t) + defer cleanup() + + buildName := "docker-local-git-test" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + registryHost := *tests.ContainerRegistry + if parsedURL, err := url.Parse(registryHost); err == nil && parsedURL.Host != "" { + registryHost = parsedURL.Host + } + imageName := path.Join(registryHost, tests.OciLocalRepo, "test-local-git-docker") + imageTag := imageName + ":v1" + + workspace, err := filepath.Abs(tests.Out) + require.NoError(t, err) + require.NoError(t, fileutils.CreateDirIfNotExist(workspace)) + tests.CopyGitFixtureIntoProject(t, workspace) + + baseImage := path.Join(registryHost, tests.OciRemoteRepo, "alpine:latest") + dockerfileContent := fmt.Sprintf("FROM %s\nCMD [\"echo\", \"local git vcs test\"]", baseImage) + dockerfilePath := filepath.Join(workspace, "Dockerfile") + require.NoError(t, os.WriteFile(dockerfilePath, []byte(dockerfileContent), 0o644)) //#nosec G703 -- test code, path built from test workspace + + runJfrogCli(t, "rt", "bc", buildName, buildNumber) + runJfrogCli(t, "docker", "build", "-t", imageTag, "--push", "-f", dockerfilePath, + "--build-name="+buildName, "--build-number="+buildNumber, workspace) + runRt(t, "build-publish", buildName, buildNumber) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.OciLocalRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + // TestSetupDockerCommand verifies `jf setup docker --url ...` end-to-end. // // Guards RTECO-1352: configureContainer (in jfrog-cli-artifactory) used to read diff --git a/go.mod b/go.mod index 01570e815..04f4a2a95 100644 --- a/go.mod +++ b/go.mod @@ -242,6 +242,9 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect ) +// attiasas:expend_vsc_detection_for_container +replace github.com/jfrog/jfrog-cli-artifactory => github.com/attiasas/jfrog-cli-artifactory v0.0.0-20260618090105-893dad1b8438 + //replace github.com/gfleury/go-bitbucket-v1 => github.com/gfleury/go-bitbucket-v1 v0.0.0-20230825095122-9bc1711434ab //replace github.com/ktrysmt/go-bitbucket => github.com/ktrysmt/go-bitbucket v0.9.80 diff --git a/go.sum b/go.sum index 381622e55..c76f09cc1 100644 --- a/go.sum +++ b/go.sum @@ -67,6 +67,8 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= +github.com/attiasas/jfrog-cli-artifactory v0.0.0-20260618090105-893dad1b8438 h1:dhl2mW0l41Fq3QfVzq7MiGE8PTvt42pI2oiiYnwk9i4= +github.com/attiasas/jfrog-cli-artifactory v0.0.0-20260618090105-893dad1b8438/go.mod h1:VqV0Bed11HoBlugAEGa3RumbwnDVslEf0gKocTzLs9s= github.com/aws/aws-sdk-go-v2 v1.41.7 h1:DWpAJt66FmnnaRIOT/8ASTucrvuDPZASqhhLey6tLY8= github.com/aws/aws-sdk-go-v2 v1.41.7/go.mod h1:4LAfZOPHNVNQEckOACQx60Y8pSRjIkNZQz1w92xpMJc= github.com/aws/aws-sdk-go-v2/config v1.32.17 h1:FpL4/758/diKwqbytU0prpuiu60fgXKUWCpDJtApclU= @@ -406,8 +408,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.20260617073349-d68ee3120aa8 h1:FG+SfgPgrIuBHSos4sw4KNZq2MKxebbCZ6KZZRfaYcs= github.com/jfrog/jfrog-cli-application v1.0.2-0.20260617073349-d68ee3120aa8/go.mod h1:p8yLtbmCxxQucIbLZKnWu0F+EDtj6NLXbRQCEK/nb6o= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606 h1:hlc8XoqySjbrvKKjxswyXQ/q5I0Px9FcZpVZUTd+T3M= -github.com/jfrog/jfrog-cli-artifactory v0.8.1-0.20260618051529-1b76b6ad2606/go.mod h1:VqV0Bed11HoBlugAEGa3RumbwnDVslEf0gKocTzLs9s= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e h1:E3B8OyEkCsdEdGsZifTphBDUPrd00yKoemL9+l25Qj8= github.com/jfrog/jfrog-cli-core/v2 v2.60.1-0.20260615072209-8ccac4f0072e/go.mod h1:9R90mhbczGXwW5EGlDs7F08ejQU/xdoDhYHMvzBiqgE= github.com/jfrog/jfrog-cli-evidence v0.9.5-0.20260601141509-8df6c9a4bc9b h1:V0FxnU3xh29y8yJHWymm6rPr1MrjG1DdPQlr3ckImwk= diff --git a/helm_test.go b/helm_test.go index a472b2967..aa051ba33 100644 --- a/helm_test.go +++ b/helm_test.go @@ -958,6 +958,99 @@ func TestHelmBuildPublishWithCIVcsProps(t *testing.T) { assert.Greater(t, artifactCount, 0, "No artifacts were validated for CI VCS properties") } +// TestHelmPushWithLocalGitVcsProps verifies local git VCS props on Helm artifacts +// when running build-publish with VCS collection enabled and no CI env. +func TestHelmPushWithLocalGitVcsProps(t *testing.T) { + initHelmTest(t) + defer cleanHelmTest(t) + + buildName := tests.HelmBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + chartDir := createTestHelmChartWithDependencies(t, "test-chart-local-git", "0.2.0") + defer func() { + if err := os.RemoveAll(chartDir); err != nil { + t.Logf("Warning: Failed to remove test chart directory %s: %v", chartDir, err) + } + }() + tests.CopyGitFixtureIntoProject(t, chartDir) + + originalDir, err := os.Getwd() + require.NoError(t, err) + defer func() { + if err := os.Chdir(originalDir); err != nil { + t.Logf("Warning: Failed to change back to original directory: %v", err) + } + }() + require.NoError(t, os.Chdir(chartDir)) + + helmCmd := exec.Command("helm", "dependency", "update") + helmCmd.Dir = chartDir + require.NoError(t, helmCmd.Run(), "helm dependency update should succeed") + + jfrogCli := coreTests.NewJfrogCli(execMain, "jfrog", "") + require.NoError(t, jfrogCli.Exec("helm", "package", ".", + "--build-name="+buildName, "--build-number="+buildNumber), "helm package should succeed") + + chartFiles, err := filepath.Glob(filepath.Join(chartDir, "*.tgz")) + require.NoError(t, err) + require.NotEmpty(t, chartFiles, "Chart package file should be created") + chartFile := filepath.Base(chartFiles[0]) + + parsedURL, err := url.Parse(serverDetails.ArtifactoryUrl) + require.NoError(t, err) + registryHost := parsedURL.Host + registryURL := fmt.Sprintf("oci://%s/%s", registryHost, tests.HelmLocalRepo) + + if !isRepoExist(tests.HelmLocalRepo) { + t.Skipf("Repository %s does not exist. Skipping test.", tests.HelmLocalRepo) + } + + err = loginHelmRegistry(t, registryHost) + if err != nil { + errorMsg := strings.ToLower(err.Error()) + if strings.Contains(errorMsg, "account temporarily locked") { + t.Skip("Artifactory account is temporarily locked. Skipping test.") + } + if strings.Contains(errorMsg, "http response to https") || + strings.Contains(errorMsg, "tls: first record does not look like a tls handshake") { + t.Skip("Helm registry login failed due to HTTPS/HTTP mismatch. Skipping test.") + } + } + require.NoError(t, err, "helm registry login should succeed") + + err = jfrogCli.Exec("helm", "push", chartFile, registryURL, + "--build-name="+buildName, "--build-number="+buildNumber) + if err != nil { + errorMsg := strings.ToLower(err.Error()) + if strings.Contains(errorMsg, "404") || + strings.Contains(errorMsg, "not found") || + strings.Contains(errorMsg, "exit status 1") { + t.Skip("OCI registry API not accessible (404). Skipping test.") + } + } + require.NoError(t, err, "helm push should succeed") + + require.NoError(t, artifactoryCli.Exec("bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.HelmLocalRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + // InitHelmTests initializes Helm tests func InitHelmTests() { initArtifactoryCli() diff --git a/huggingface_test.go b/huggingface_test.go index f71ad8ac9..2007143aa 100644 --- a/huggingface_test.go +++ b/huggingface_test.go @@ -10,9 +10,11 @@ import ( "strings" "testing" + "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-cli-core/v2/utils/coreutils" coreTests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-cli/inttestutils" "github.com/jfrog/jfrog-cli/utils/tests" clientTestUtils "github.com/jfrog/jfrog-client-go/utils/tests" "github.com/stretchr/testify/assert" @@ -867,6 +869,53 @@ func InitHuggingFaceTests() { createRequiredRepos() } +func TestHuggingFaceUploadWithLocalGitVcsProps(t *testing.T) { + initHuggingFaceTest(t) + defer cleanHuggingFaceTest(t) + checkHuggingFaceHubAvailable(t) + + buildName := tests.HuggingFaceBuildName + "-local-git" + buildNumber := "1" + + cleanupEnv := tests.SetupLocalGitVcsEnv(t) + defer cleanupEnv() + + inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + defer inttestutils.DeleteBuild(serverDetails.ArtifactoryUrl, buildName, artHttpDetails) + + tempDir, err := os.MkdirTemp("", "hf-local-git-*") + require.NoError(t, err) + t.Cleanup(func() { _ = os.RemoveAll(tempDir) }) + tests.CopyGitFixtureIntoProject(t, tempDir) + + require.NoError(t, os.WriteFile(filepath.Join(tempDir, "config.json"), + []byte(`{"model_type": "local-git-vcs"}`), 0o644)) + require.NoError(t, os.WriteFile(filepath.Join(tempDir, "model.bin"), + []byte("model"), 0o644)) + + jfrogCli := coreTests.NewJfrogCli(execMain, "jfrog", "") + args := []string{ + "hf", "u", tempDir, "test-org/test-local-git-model", + "--repo-type=model", + "--build-name=" + buildName, + "--build-number=" + buildNumber, + "--repo-key=" + tests.HuggingFaceLocalRepo, + } + require.NoError(t, jfrogCli.Exec(args...)) + require.NoError(t, jfrogCli.Exec("rt", "bp", buildName, buildNumber)) + + publishedBuildInfo, found, err := tests.GetBuildInfo(serverDetails, buildName, buildNumber) + require.NoError(t, err) + require.True(t, found) + + serviceManager, err := utils.CreateServiceManager(serverDetails, 3, 1000, false) + require.NoError(t, err) + + count := tests.ValidateLocalGitVcsPropsOnBuildInfoArtifacts(t, serviceManager, publishedBuildInfo, tests.HuggingFaceLocalRepo, + tests.VcsFixtureMainURL, tests.VcsFixtureMainRevision, tests.VcsFixtureMainBranch) + assert.Greater(t, count, 0) +} + // CleanHuggingFaceTests cleans up after HuggingFace tests func CleanHuggingFaceTests() { deleteCreatedRepos() diff --git a/utils/tests/artifact_props.go b/utils/tests/artifact_props.go new file mode 100644 index 000000000..172e2aadf --- /dev/null +++ b/utils/tests/artifact_props.go @@ -0,0 +1,88 @@ +package tests + +import ( + "strings" + "testing" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// ArtifactFullPath builds the Artifactory item path for GetItemProps. +// When OriginalDeploymentRepo is empty (common with Gradle extractor build-info), +// defaultRepo is used as the repository prefix. +func ArtifactFullPath(a buildinfo.Artifact, defaultRepo string) string { + path := strings.TrimPrefix(a.Path, "/") + repo := a.OriginalDeploymentRepo + if repo == "" { + repo = defaultRepo + } + if repo != "" { + return repo + "/" + path + } + return path +} + +// ArtifactItemPath returns the Artifactory item path for GetItemProps. +// When Name is set and not already part of Path (e.g. UV stores Path as a directory), +// Name is appended as the filename segment. +func ArtifactItemPath(a buildinfo.Artifact, defaultRepo string) string { + fullPath := ArtifactFullPath(a, defaultRepo) + if a.Name == "" { + return fullPath + } + if strings.HasSuffix(fullPath, "/"+a.Name) || strings.HasSuffix(fullPath, a.Name) { + return fullPath + } + return fullPath + "/" + a.Name +} + +// ValidateLocalGitVcsPropsOnBuildInfoArtifacts fetches props for each build-info artifact +// and asserts local-git VCS fields. Returns the number of artifacts validated. +func ValidateLocalGitVcsPropsOnBuildInfoArtifacts( + t *testing.T, + serviceManager artifactory.ArtifactoryServicesManager, + publishedBuildInfo *buildinfo.PublishedBuildInfo, + defaultRepo string, + expectedURL, expectedRevision, expectedBranch string, +) int { + t.Helper() + require.NotNil(t, publishedBuildInfo) + + count := 0 + for _, module := range publishedBuildInfo.BuildInfo.Modules { + for _, artifact := range module.Artifacts { + fullPath := ArtifactItemPath(artifact, defaultRepo) + if fullPath == "" { + continue + } + + props, err := serviceManager.GetItemProps(fullPath) + require.NoError(t, err, "GetItemProps failed for %s", fullPath) + if props == nil { + assert.Fail(t, "Properties are nil for artifact: %s", fullPath) + continue + } + + assert.Contains(t, props.Properties, "vcs.url", "Missing vcs.url on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.url"], expectedURL, "Wrong vcs.url on %s", artifact.Name) + + assert.Contains(t, props.Properties, "vcs.revision", "Missing vcs.revision on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.revision"], expectedRevision, "Wrong vcs.revision on %s", artifact.Name) + + if expectedBranch != "" { + assert.Contains(t, props.Properties, "vcs.branch", "Missing vcs.branch on %s", artifact.Name) + assert.Contains(t, props.Properties["vcs.branch"], expectedBranch, "Wrong vcs.branch on %s", artifact.Name) + } + + // Local-git-only: provider/org/repo must NOT appear when CI is cleared + _, hasProvider := props.Properties["vcs.provider"] + assert.False(t, hasProvider, "vcs.provider should not be set on %s in local-git-only mode", artifact.Name) + + count++ + } + } + return count +} diff --git a/utils/tests/artifact_props_test.go b/utils/tests/artifact_props_test.go new file mode 100644 index 000000000..aaf8bbe01 --- /dev/null +++ b/utils/tests/artifact_props_test.go @@ -0,0 +1,57 @@ +package tests + +import ( + "testing" + + buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/stretchr/testify/assert" +) + +func TestArtifactFullPath(t *testing.T) { + t.Run("uses OriginalDeploymentRepo when set", func(t *testing.T) { + a := buildinfo.Artifact{OriginalDeploymentRepo: "cli-gradle-123", Path: "com/foo/1.0/foo.jar"} + assert.Equal(t, "cli-gradle-123/com/foo/1.0/foo.jar", ArtifactFullPath(a, "fallback-repo")) + }) + + t.Run("falls back to defaultRepo when OriginalDeploymentRepo empty", func(t *testing.T) { + a := buildinfo.Artifact{Path: "com/foo/1.0/foo.jar"} + assert.Equal(t, "cli-gradle-123/com/foo/1.0/foo.jar", ArtifactFullPath(a, "cli-gradle-123")) + }) + + t.Run("falls back to Path when repo empty and no default", func(t *testing.T) { + a := buildinfo.Artifact{Path: "com/foo/1.0/foo.jar"} + assert.Equal(t, "com/foo/1.0/foo.jar", ArtifactFullPath(a, "")) + }) + + t.Run("strips leading slash from Path", func(t *testing.T) { + a := buildinfo.Artifact{Path: "/minimal-example/1.0/minimal-example-1.0.jar"} + assert.Equal(t, "cli-gradle-123/minimal-example/1.0/minimal-example-1.0.jar", ArtifactFullPath(a, "cli-gradle-123")) + }) +} + +func TestValidateLocalGitVcsPropsOnBuildInfoArtifacts_UsesArtifactFullPath(t *testing.T) { + // Smoke-test ArtifactFullPath integration used by the helper (no Artifactory call). + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "", + Path: "/com/foo/1.0/foo.jar", + } + assert.Equal(t, "my-repo/com/foo/1.0/foo.jar", ArtifactFullPath(a, "my-repo")) +} + +func TestArtifactItemPath_AppendsNameForDirectoryPath(t *testing.T) { + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "uv-local", + Path: "my-pkg/0.1.0", + Name: "my_pkg-0.1.0-py3-none-any.whl", + } + assert.Equal(t, "uv-local/my-pkg/0.1.0/my_pkg-0.1.0-py3-none-any.whl", ArtifactItemPath(a, "")) +} + +func TestArtifactItemPath_DoesNotDoubleAppendName(t *testing.T) { + a := buildinfo.Artifact{ + OriginalDeploymentRepo: "mvn-local", + Path: "com/foo/1.0/foo.jar", + Name: "foo.jar", + } + assert.Equal(t, "mvn-local/com/foo/1.0/foo.jar", ArtifactItemPath(a, "")) +} diff --git a/utils/tests/utils.go b/utils/tests/utils.go index 4bacf7ffc..e8653d1cf 100644 --- a/utils/tests/utils.go +++ b/utils/tests/utils.go @@ -114,8 +114,8 @@ func init() { TestPip = flag.Bool("test.pip", false, "Test Pip") TestPipenv = flag.Bool("test.pipenv", false, "Test Pipenv") TestPoetry = flag.Bool("test.poetry", false, "Test Poetry") - TestUv = flag.Bool("test.uv", false, "Test UV") - TestNix = flag.Bool("test.nix", false, "Test Nix") + TestUv = flag.Bool("test.uv", false, "Test UV") + TestNix = flag.Bool("test.nix", false, "Test Nix") TestConan = flag.Bool("test.conan", false, "Test Conan") TestHelm = flag.Bool("test.helm", false, "Test Helm") TestHuggingFace = flag.Bool("test.huggingface", false, "Test HuggingFace") @@ -862,6 +862,31 @@ func SetupGitHubActionsEnv(t *testing.T) (cleanup func(), actualOrg, actualRepo return cleanup, actualOrg, actualRepo } +// SetupGitHubActionsEnvForLocalGitMerge enables CI VCS collection with provider/org/repo +// but clears url/revision/branch CI env vars so local git fallback is exercised. +func SetupGitHubActionsEnvForLocalGitMerge(t *testing.T) (cleanup func(), actualOrg, actualRepo string) { + t.Helper() + cleanupBase, actualOrg, actualRepo := SetupGitHubActionsEnv(t) + + var callbacks []func() + for _, key := range []string{ + "GITHUB_SERVER_URL", + "GITHUB_SHA", + "GITHUB_REF", + "GITHUB_REF_NAME", + "GITHUB_HEAD_REF", + } { + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) + } + + return func() { + for _, cb := range callbacks { + cb() + } + cleanupBase() + }, actualOrg, actualRepo +} + // ValidateCIVcsPropsOnArtifacts validates that CI VCS properties are set on artifacts. func ValidateCIVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedProvider, expectedOrg, expectedRepo string) { for _, item := range resultItems { @@ -960,3 +985,76 @@ func ValidateCIVcsPropsIfPresent(t *testing.T, resultItems []utils.ResultItem, e } } } + +// SetupLocalGitVcsEnv enables VCS property collection and clears CI detection +// so only local git fallback is exercised. +func SetupLocalGitVcsEnv(t *testing.T) (cleanup func()) { + t.Helper() + var callbacks []func() + + for _, key := range []string{ + "JFROG_CLI_CI_VCS_PROPS_DISABLED", // set to "" to enable + "CI", "GITHUB_ACTIONS", "GITHUB_WORKFLOW", "GITHUB_RUN_ID", + "GITHUB_REPOSITORY", "GITHUB_REPOSITORY_OWNER", + "GITHUB_SERVER_URL", "GITHUB_SHA", "GITHUB_REF", "GITHUB_REF_NAME", "GITHUB_HEAD_REF", + } { + callbacks = append(callbacks, tests.SetEnvWithCallbackAndAssert(t, key, "")) + } + + return func() { + for _, cb := range callbacks { + cb() + } + } +} + +// ValidateLocalGitVcsPropsOnArtifacts asserts vcs.url, vcs.revision, vcs.branch on every item. +func ValidateLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, expectedURL, expectedRevision, expectedBranch string) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) + if expectedBranch != "" { + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) + } + } +} + +func assertLocalGitProp(t *testing.T, itemName string, props map[string][]string, key, expected string) { + t.Helper() + vals, ok := props[key] + assert.True(t, ok, "Missing %s on %s", key, itemName) + assert.Contains(t, vals, expected, "Wrong %s on %s", key, itemName) +} + +// ValidateNoLocalGitVcsPropsOnArtifacts asserts url/revision/branch are absent. +func ValidateNoLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + _, hasURL := propertiesMap["vcs.url"] + _, hasRev := propertiesMap["vcs.revision"] + _, hasBranch := propertiesMap["vcs.branch"] + assert.False(t, hasURL, "vcs.url should not be set on %s", item.Name) + assert.False(t, hasRev, "vcs.revision should not be set on %s", item.Name) + assert.False(t, hasBranch, "vcs.branch should not be set on %s", item.Name) + } +} + +// ValidateCIAndLocalGitVcsPropsOnArtifacts asserts CI props plus local git props coexist. +func ValidateCIAndLocalGitVcsPropsOnArtifacts(t *testing.T, resultItems []utils.ResultItem, + expectedProvider, expectedOrg, expectedRepo, expectedURL, expectedRevision, expectedBranch string) { + t.Helper() + for _, item := range resultItems { + propertiesMap := ConvertPropertiesToMap(item.Properties) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.provider", expectedProvider) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.org", expectedOrg) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.repo", expectedRepo) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.url", expectedURL) + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.revision", expectedRevision) + if expectedBranch != "" { + assertLocalGitProp(t, item.Name, propertiesMap, "vcs.branch", expectedBranch) + } + } +} diff --git a/utils/tests/utils_test.go b/utils/tests/utils_test.go new file mode 100644 index 000000000..d384b9e8c --- /dev/null +++ b/utils/tests/utils_test.go @@ -0,0 +1,31 @@ +package tests + +import ( + "testing" + + "github.com/jfrog/build-info-go/utils/cienv" + "github.com/stretchr/testify/assert" +) + +func TestSetupGitHubActionsEnvForLocalGitMerge_ClearsUrlRevisionBranch(t *testing.T) { + t.Setenv("CI", "true") + t.Setenv("GITHUB_ACTIONS", "true") + t.Setenv("GITHUB_WORKFLOW", "wf") + t.Setenv("GITHUB_RUN_ID", "99") + t.Setenv("GITHUB_REPOSITORY_OWNER", "jfrog") + t.Setenv("GITHUB_REPOSITORY", "jfrog/jfrog-cli") + t.Setenv("GITHUB_SERVER_URL", "https://github.com") + t.Setenv("GITHUB_SHA", "abc123") + t.Setenv("GITHUB_REF", "refs/heads/feature") + + cleanup, _, _ := SetupGitHubActionsEnvForLocalGitMerge(t) + defer cleanup() + + info := cienv.GetCIVcsInfo() + assert.Equal(t, "github", info.Provider) + assert.Equal(t, "jfrog", info.Org) + assert.Equal(t, "jfrog-cli", info.Repo) + assert.Empty(t, info.Url) + assert.Empty(t, info.Revision) + assert.Empty(t, info.Branch) +} diff --git a/utils/tests/vcs_fixtures.go b/utils/tests/vcs_fixtures.go new file mode 100644 index 000000000..2b94d5cbf --- /dev/null +++ b/utils/tests/vcs_fixtures.go @@ -0,0 +1,92 @@ +package tests + +import ( + "os" + "path/filepath" + "runtime" + "testing" + + biutils "github.com/jfrog/build-info-go/utils" + coretests "github.com/jfrog/jfrog-cli-core/v2/utils/tests" + "github.com/jfrog/jfrog-client-go/utils/io/fileutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + VcsFixtureMainURL = "https://github.com/jfrog/jfrog-cli.git" + VcsFixtureMainRevision = "d63c5957ad6819f4c02a817abe757f210d35ff92" + VcsFixtureMainBranch = "master" + + VcsFixtureOtherURL = "https://github.com/jfrog/jfrog-client-go.git" + VcsFixtureOtherRevision = "ad99b6c068283878fde4d49423728f0bdc00544a" + VcsFixtureOtherBranch = "InnerGit" +) + +// testResourcesDir returns the absolute path to the repo's testdata/ directory. +// It is resolved from this source file's location, not os.Getwd(). +func testResourcesDir() string { + _, filename, _, ok := runtime.Caller(0) + if !ok { + abs, err := filepath.Abs(filepath.FromSlash(GetTestResourcesPath())) + if err != nil { + return filepath.FromSlash(GetTestResourcesPath()) + } + return abs + } + abs, err := filepath.Abs(filepath.Join(filepath.Dir(filename), "..", "..", "testdata")) + if err != nil { + return filepath.Join(filepath.Dir(filename), "..", "..", "testdata") + } + return abs +} + +func vcsFixtureSrcDir() string { + return filepath.Join(testResourcesDir(), "vcs") +} + +func vcsGitdataSrcDir() string { + return filepath.Join(vcsFixtureSrcDir(), "gitdata") +} + +// CopyVcsGitFixture copies testdata/vcs into destDir and renames gitdata -> .git. +// Returns the absolute path to destDir. +func CopyVcsGitFixture(t *testing.T, destDir string) string { + t.Helper() + src := vcsFixtureSrcDir() + assert.NoError(t, biutils.CopyDir(src, destDir, true, nil)) + if found, err := fileutils.IsDirExists(filepath.Join(destDir, "gitdata"), false); found { + assert.NoError(t, err) + coretests.RenamePath(filepath.Join(destDir, "gitdata"), filepath.Join(destDir, ".git"), t) + } + if found, err := fileutils.IsDirExists(filepath.Join(destDir, "OtherGit", "gitdata"), false); found { + assert.NoError(t, err) + coretests.RenamePath( + filepath.Join(destDir, "OtherGit", "gitdata"), + filepath.Join(destDir, "OtherGit", ".git"), + t, + ) + } + abs, err := filepath.Abs(destDir) + assert.NoError(t, err) + return abs +} + +// CopyGitFixtureIntoProject installs testdata/vcs/gitdata as projectDir/.git. +func CopyGitFixtureIntoProject(t *testing.T, projectDir string) { + t.Helper() + src := vcsGitdataSrcDir() + gitDir := filepath.Join(projectDir, ".git") + stagingDir := filepath.Join(projectDir, "gitdata-staging") + + if fileutils.IsPathExists(gitDir, false) { + require.NoError(t, os.RemoveAll(gitDir)) + } + require.NoError(t, os.RemoveAll(stagingDir)) + + require.NoError(t, biutils.CopyDir(src, stagingDir, true, nil)) + coretests.RenamePath(stagingDir, gitDir, t) + + require.FileExists(t, filepath.Join(gitDir, "HEAD")) + require.FileExists(t, filepath.Join(gitDir, "config")) +} diff --git a/utils/tests/vcs_fixtures_test.go b/utils/tests/vcs_fixtures_test.go new file mode 100644 index 000000000..a9968101a --- /dev/null +++ b/utils/tests/vcs_fixtures_test.go @@ -0,0 +1,27 @@ +package tests + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCopyGitFixtureIntoProject_WorksAfterChdir(t *testing.T) { + repoRoot, err := os.Getwd() + require.NoError(t, err) + + projectDir := t.TempDir() + subDir := filepath.Join(projectDir, "nested") + require.NoError(t, os.MkdirAll(subDir, 0o755)) + + // Simulate prepareGoProject leaving cwd inside the project tree. + require.NoError(t, os.Chdir(subDir)) + t.Cleanup(func() { _ = os.Chdir(repoRoot) }) + + CopyGitFixtureIntoProject(t, projectDir) + + require.FileExists(t, filepath.Join(projectDir, ".git", "HEAD")) + require.FileExists(t, filepath.Join(projectDir, ".git", "config")) +}