diff --git a/artifactory/commands/buildinfo/civcs_props.go b/artifactory/commands/buildinfo/civcs_props.go index 03f75aa4..33add0b0 100644 --- a/artifactory/commands/buildinfo/civcs_props.go +++ b/artifactory/commands/buildinfo/civcs_props.go @@ -42,28 +42,29 @@ func extractArtifactPathsWithWarnings(buildInfo *buildinfo.BuildInfo) ([]string, // constructArtifactPathWithFallback builds the full Artifactory path for an artifact. // Strategy: // 1. If OriginalDeploymentRepo is present: use OriginalDeploymentRepo + "/" + Path -// 2. If OriginalDeploymentRepo is missing: use Path directly (it may or may not work) +// 2. If OriginalDeploymentRepo is missing: prefix Path with "*/" so search resolves +// across repositories. At set-props time this is restricted to local repos when possible, +// with a warning logged (Gradle extractor often omits originalDeploymentRepo in build-info). // 3. If neither available: return empty string (caller should warn and skip) func constructArtifactPathWithFallback(artifact buildinfo.Artifact) string { // Primary: Use OriginalDeploymentRepo if available if artifact.OriginalDeploymentRepo != "" { if artifact.Path != "" { - return artifact.OriginalDeploymentRepo + "/" + artifact.Path + return artifact.OriginalDeploymentRepo + "/" + strings.TrimPrefix(artifact.Path, "/") } if artifact.Name != "" { return artifact.OriginalDeploymentRepo + "/" + artifact.Name } } - // Fallback: Use Path directly - it might be a complete path or might fail - // If it fails, setPropsOnArtifacts will warn and move on + // Fallback: path-only from build-info — use wildcard repo for search-based SetProps if artifact.Path != "" { - return artifact.Path + return "*/" + strings.TrimPrefix(artifact.Path, "/") } // Last resort: just the name (unlikely to work, but let it try) if artifact.Name != "" { - return artifact.Name + return "*/" + artifact.Name } // Nothing available @@ -84,6 +85,60 @@ func constructArtifactPath(artifact buildinfo.Artifact) string { return "" } +const wildcardRepoPrefix = "*/" + +// isWildcardRepoPath reports whether the path uses the repo-less fallback prefix. +func isWildcardRepoPath(path string) bool { + return strings.HasPrefix(path, wildcardRepoPrefix) +} + +// expandWildcardPathsToLocalRepos replaces "*/" patterns with "/" for each +// local repository. This avoids matching virtual repos and remote caches when OriginalDeploymentRepo +// is missing from build-info (common with the Gradle extractor). +func expandWildcardPathsToLocalRepos(servicesManager artifactory.ArtifactoryServicesManager, artifactPaths []string) ([]string, error) { + if !hasWildcardRepoPaths(artifactPaths) { + return artifactPaths, nil + } + repos, err := servicesManager.GetAllRepositoriesFiltered(services.RepositoriesFilterParams{RepoType: "local"}) + if err != nil { + return artifactPaths, err + } + if repos == nil || len(*repos) == 0 { + return artifactPaths, nil + } + expanded := make([]string, 0, len(artifactPaths)) + for _, artifactPath := range artifactPaths { + if !isWildcardRepoPath(artifactPath) { + expanded = append(expanded, artifactPath) + continue + } + artifactOnlyPath := strings.TrimPrefix(artifactPath, wildcardRepoPrefix) + for _, repo := range *repos { + expanded = append(expanded, repo.Key+"/"+artifactOnlyPath) + } + } + return expanded, nil +} + +func hasWildcardRepoPaths(artifactPaths []string) bool { + for _, path := range artifactPaths { + if isWildcardRepoPath(path) { + return true + } + } + return false +} + +func countWildcardRepoPaths(artifactPaths []string) int { + count := 0 + for _, path := range artifactPaths { + if isWildcardRepoPath(path) { + count++ + } + } + return count +} + // setPropsOnArtifacts sets properties on multiple artifacts using search-based resolution. // This uses the same search mechanism as 'jf rt set-props', which resolves virtual repository // paths to their underlying local repositories before setting properties. @@ -92,6 +147,19 @@ func setPropsOnArtifacts(servicesManager artifactory.ArtifactoryServicesManager, if len(artifactPaths) == 0 { return } + wildcardCount := countWildcardRepoPaths(artifactPaths) + if wildcardCount > 0 { + expanded, err := expandWildcardPathsToLocalRepos(servicesManager, artifactPaths) + switch { + case err != nil: + log.Warn("CI VCS:", wildcardCount, "artifact path(s) missing OriginalDeploymentRepo; searching across all repositories (failed to list local repos:", err, "). Artifacts at the same path in other repositories may be tagged unintentionally.") + case hasWildcardRepoPaths(expanded): + log.Warn("CI VCS:", wildcardCount, "artifact path(s) missing OriginalDeploymentRepo; searching across all repositories. Artifacts at the same path in other repositories may be tagged unintentionally.") + default: + artifactPaths = expanded + log.Warn("CI VCS:", wildcardCount, "artifact path(s) missing OriginalDeploymentRepo; property search restricted to local repositories. The same path in multiple local repos may still be tagged unintentionally.") + } + } var lastErr error for attempt := 0; attempt < maxRetries; attempt++ { if attempt > 0 { diff --git a/artifactory/commands/buildinfo/civcs_props_test.go b/artifactory/commands/buildinfo/civcs_props_test.go index 6443823b..30369c32 100644 --- a/artifactory/commands/buildinfo/civcs_props_test.go +++ b/artifactory/commands/buildinfo/civcs_props_test.go @@ -4,6 +4,8 @@ import ( "testing" buildinfo "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-client-go/artifactory" + "github.com/jfrog/jfrog-client-go/artifactory/services" "github.com/stretchr/testify/assert" ) @@ -80,19 +82,27 @@ func TestConstructArtifactPathWithFallback(t *testing.T) { expected: "my-repo/file.jar", }, { - name: "without OriginalDeploymentRepo - fallback to Path", + name: "without OriginalDeploymentRepo - wildcard repo search prefix", artifact: buildinfo.Artifact{ Path: "my-repo/path/to/file.jar", Name: "file.jar", }, - expected: "my-repo/path/to/file.jar", + expected: "*/my-repo/path/to/file.jar", + }, + { + name: "gradle extractor path without OriginalDeploymentRepo", + artifact: buildinfo.Artifact{ + Path: "minimal-example/1.0/minimal-example-1.0.jar", + Name: "minimal-example-1.0.jar", + }, + expected: "*/minimal-example/1.0/minimal-example-1.0.jar", }, { name: "without OriginalDeploymentRepo or Path - fallback to Name", artifact: buildinfo.Artifact{ Name: "file.jar", }, - expected: "file.jar", + expected: "*/file.jar", }, { name: "empty artifact", @@ -211,6 +221,61 @@ func TestCivcsExtractArtifactPathsWithWarnings(t *testing.T) { } } +func TestExpandWildcardPathsToLocalRepos(t *testing.T) { + t.Run("no wildcard paths", func(t *testing.T) { + paths := []string{"libs-release/com/example/foo.jar"} + expanded, err := expandWildcardPathsToLocalRepos(nil, paths) + assert.NoError(t, err) + assert.Equal(t, paths, expanded) + }) + + t.Run("expands wildcard paths to local repos", func(t *testing.T) { + mockSM := &mockReposServicesManager{ + repos: []services.RepositoryDetails{ + {Key: "libs-release"}, + {Key: "libs-snapshot-local"}, + }, + } + paths := []string{ + "libs-release/com/example/foo.jar", + "*/com/example/bar.jar", + } + expanded, err := expandWildcardPathsToLocalRepos(mockSM, paths) + assert.NoError(t, err) + assert.Equal(t, []string{ + "libs-release/com/example/foo.jar", + "libs-release/com/example/bar.jar", + "libs-snapshot-local/com/example/bar.jar", + }, expanded) + }) + + t.Run("returns error when listing repos fails", func(t *testing.T) { + mockSM := &mockReposServicesManager{err: assert.AnError} + paths := []string{"*/com/example/foo.jar"} + expanded, err := expandWildcardPathsToLocalRepos(mockSM, paths) + assert.Error(t, err) + assert.Equal(t, paths, expanded) + }) +} + +type mockReposServicesManager struct { + artifactory.EmptyArtifactoryServicesManager + repos []services.RepositoryDetails + err error +} + +func (m *mockReposServicesManager) GetAllRepositoriesFiltered(services.RepositoriesFilterParams) (*[]services.RepositoryDetails, error) { + if m.err != nil { + return nil, m.err + } + return &m.repos, nil +} + +func TestIsWildcardRepoPath(t *testing.T) { + assert.True(t, isWildcardRepoPath("*/com/example/foo.jar")) + assert.False(t, isWildcardRepoPath("libs-release/com/example/foo.jar")) +} + func TestCivcsIs404Error(t *testing.T) { tests := []struct { name string diff --git a/artifactory/commands/buildinfo/publish.go b/artifactory/commands/buildinfo/publish.go index 3dc8cf19..a4b040b3 100644 --- a/artifactory/commands/buildinfo/publish.go +++ b/artifactory/commands/buildinfo/publish.go @@ -4,12 +4,12 @@ import ( "errors" "fmt" "net/url" + "os" "strconv" "strings" "time" buildinfo "github.com/jfrog/build-info-go/entities" - "github.com/jfrog/build-info-go/utils/cienv" "github.com/jfrog/jfrog-cli-artifactory/artifactory/formats" "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" @@ -239,7 +239,7 @@ func (bpc *BuildPublishCommand) Run() error { // Set CI VCS properties on artifacts from build info. // This only runs if we're in a supported CI environment (GitHub Actions, GitLab CI, etc.) // Note: This never returns an error - it only logs warnings on failure - bpc.setCIVcsPropsOnArtifacts(servicesManager, buildInfo) + bpc.setVcsPropsOnArtifacts(servicesManager, buildInfo) majorVersion, err := utils.GetRtMajorVersion(servicesManager) if err != nil { @@ -359,52 +359,49 @@ func (bpc *BuildPublishCommand) getNextBuildNumber(buildName string, servicesMan return strconv.Itoa(latestBuildNumber), nil } -// setCIVcsPropsOnArtifacts sets CI VCS properties on all artifacts in the build info. +// setVcsPropsOnArtifacts sets VCS properties on all artifacts in the build info. // This method: -// - Only runs when in a supported CI environment (GitHub Actions, GitLab CI, etc.) +// - Merges CI and local git VCS props via mergeVcsPropsForBuildPublish // - Never fails the build publish - only logs warnings on errors // - Retries transient failures but not 404 errors -// - Does nothing if CI VCS props collection is disabled via JFROG_CLI_CI_VCS_PROPS_DISABLED -func (bpc *BuildPublishCommand) setCIVcsPropsOnArtifacts( +// - Does nothing if VCS props collection is disabled via JFROG_CLI_CI_VCS_PROPS_DISABLED +func (bpc *BuildPublishCommand) setVcsPropsOnArtifacts( servicesManager artifactory.ArtifactoryServicesManager, buildInfo *buildinfo.BuildInfo, ) { - // Check if CI VCS props collection is disabled if civcs.IsCIVcsPropsDisabled() { return } - // Check if running in a supported CI environment - // This requires CI=true AND a registered provider (GitHub, GitLab, etc.) - ciVcsInfo := cienv.GetCIVcsInfo() - if ciVcsInfo.IsEmpty() { - // Not in CI or no registered provider - silently skip - return + searchDir := bpc.dotGitPath + if searchDir == "" { + var err error + searchDir, err = os.Getwd() + if err != nil { + searchDir = "." + } } - log.Debug("CI VCS: Detected provider:", ciVcsInfo.Provider, ", org:", ciVcsInfo.Org, ", repo:", ciVcsInfo.Repo) - - // Build props string - props := civcs.BuildCIVcsPropsString(ciVcsInfo) + // Build props string (CI env + local git fallback). + props := civcs.GetCIVcsPropsString(searchDir) if props == "" { - log.Debug("CI VCS: Empty props string, skipping") + log.Debug("VCS: Empty props string, skipping") return } - // Extract artifact paths from build info (with warnings for missing repo paths) artifactPaths, skippedCount := extractArtifactPathsWithWarnings(buildInfo) - log.Debug("CI VCS: Extracted", len(artifactPaths), "artifact paths,", skippedCount, "skipped") + log.Debug("VCS: Extracted", len(artifactPaths), "artifact paths,", skippedCount, "skipped") if len(artifactPaths) == 0 && skippedCount == 0 { - log.Debug("CI VCS: No artifacts found in build info") + log.Debug("VCS: No artifacts found in build info") return } if len(artifactPaths) == 0 { // All artifacts were skipped due to missing repo paths - log.Debug("CI VCS: All artifacts skipped due to missing repo paths") + log.Debug("VCS: All artifacts skipped due to missing repo paths") return } - log.Debug("CI VCS: Setting properties on", len(artifactPaths), "artifacts with props:", props) + log.Debug("VCS: Setting properties on", len(artifactPaths), "artifacts with props:", props) // Set properties on all artifacts in a single batch call setPropsOnArtifacts(servicesManager, artifactPaths, props) - log.Debug("CI VCS: Property setting completed") + log.Debug("VCS: Property setting completed") } func (bpc *BuildPublishCommand) excludeDependenciesByScope(buildInfo *buildinfo.BuildInfo) { diff --git a/artifactory/commands/buildinfo/publish_test.go b/artifactory/commands/buildinfo/publish_test.go index 7e1d756a..b2fb84bf 100644 --- a/artifactory/commands/buildinfo/publish_test.go +++ b/artifactory/commands/buildinfo/publish_test.go @@ -67,7 +67,9 @@ func TestSetCIVcsPropsOnArtifacts(t *testing.T) { // 2. Mock services manager mockSM := new(mockServicesManager) - expectedProps := "vcs.provider=github;vcs.org=jfrog;vcs.repo=jfrog-cli" + // Use a non-git directory so only CI env props are collected (not local git url/revision/branch). + nonGitDir := t.TempDir() + expectedProps := civcs.GetCIVcsPropsString(nonGitDir) searchReader, cleanup := createTestSearchReader(t) defer cleanup() @@ -95,7 +97,8 @@ func TestSetCIVcsPropsOnArtifacts(t *testing.T) { // 4. Run command bpc := NewBuildPublishCommand() - bpc.setCIVcsPropsOnArtifacts(mockSM, bi) + bpc.SetDotGitPath(nonGitDir) + bpc.setVcsPropsOnArtifacts(mockSM, bi) // 5. Verify mockSM.AssertExpectations(t) @@ -234,7 +237,7 @@ func TestExtractArtifactPathsWithWarnings(t *testing.T) { }, }, }, - expectedPaths: []string{"com/example/file1.jar"}, + expectedPaths: []string{"*/com/example/file1.jar"}, expectedSkipped: 0, }, { @@ -249,7 +252,7 @@ func TestExtractArtifactPathsWithWarnings(t *testing.T) { }, }, }, - expectedPaths: []string{"libs-release/com/example/file1.jar", "com/example/file2.jar"}, + expectedPaths: []string{"libs-release/com/example/file1.jar", "*/com/example/file2.jar"}, expectedSkipped: 0, }, { diff --git a/artifactory/commands/conan/artifacts.go b/artifactory/commands/conan/artifacts.go index 8e323d05..f68d0dd8 100644 --- a/artifactory/commands/conan/artifacts.go +++ b/artifactory/commands/conan/artifacts.go @@ -8,6 +8,7 @@ import ( "time" "github.com/jfrog/build-info-go/entities" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-client-go/artifactory/services" @@ -16,6 +17,8 @@ import ( "github.com/jfrog/jfrog-client-go/utils/log" ) +var mergeVcsPropsForConan = civcs.MergeWithUserProps + // ConanPackageInfo holds parsed Conan package reference information. // Supports both Conan 2.x (name/version) and 1.x (name/version@user/channel) formats. type ConanPackageInfo struct { @@ -180,16 +183,21 @@ type BuildPropertySetter struct { buildName string buildNumber string projectKey string + searchDir string } // NewBuildPropertySetter creates a new build property setter. -func NewBuildPropertySetter(serverDetails *config.ServerDetails, targetRepo, buildName, buildNumber, projectKey string) *BuildPropertySetter { +func NewBuildPropertySetter(serverDetails *config.ServerDetails, targetRepo, buildName, buildNumber, projectKey, searchDir string) *BuildPropertySetter { + if searchDir == "" { + searchDir = "." + } return &BuildPropertySetter{ serverDetails: serverDetails, targetRepo: targetRepo, buildName: buildName, buildNumber: buildNumber, projectKey: projectKey, + searchDir: searchDir, } } @@ -282,7 +290,7 @@ func (bps *BuildPropertySetter) formatBuildProperties(timestamp string) string { props += fmt.Sprintf(";build.project=%s", bps.projectKey) } - return props + return mergeVcsPropsForConan(props, bps.searchDir) } // closeReader safely closes a content reader. diff --git a/artifactory/commands/conan/artifacts_test.go b/artifactory/commands/conan/artifacts_test.go index f4a081af..51098782 100644 --- a/artifactory/commands/conan/artifacts_test.go +++ b/artifactory/commands/conan/artifacts_test.go @@ -157,6 +157,7 @@ func TestBuildPropertySetter_FormatBuildProperties(t *testing.T) { buildNumber string projectKey string timestamp string + searchDir string expected string }{ { @@ -175,14 +176,35 @@ func TestBuildPropertySetter_FormatBuildProperties(t *testing.T) { timestamp: "9876543210", expected: "build.name=my-build;build.number=456;build.timestamp=9876543210;build.project=myproject", }, + { + name: "With search dir", + buildName: "my-build", + buildNumber: "789", + projectKey: "", + timestamp: "0123456789", + searchDir: "/conan/project", + expected: "build.name=my-build;build.number=789;build.timestamp=0123456789;vcs.revision=conan-sha", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.searchDir == "" { + t.Setenv("JFROG_CLI_CI_VCS_PROPS_DISABLED", "true") + } else { + t.Setenv("JFROG_CLI_CI_VCS_PROPS_DISABLED", "") + originalMerge := mergeVcsPropsForConan + mergeVcsPropsForConan = func(userProps, searchDir string) string { + assert.Equal(t, tt.searchDir, searchDir) + return userProps + ";vcs.revision=conan-sha" + } + t.Cleanup(func() { mergeVcsPropsForConan = originalMerge }) + } setter := &BuildPropertySetter{ buildName: tt.buildName, buildNumber: tt.buildNumber, projectKey: tt.projectKey, + searchDir: tt.searchDir, } result := setter.formatBuildProperties(tt.timestamp) assert.Equal(t, tt.expected, result) @@ -205,8 +227,9 @@ func TestNewBuildPropertySetter(t *testing.T) { buildNumber := "1" projectKey := "test-project" targetRepo := "conan-local" + searchDir := "/conan/project" - setter := NewBuildPropertySetter(nil, targetRepo, buildName, buildNumber, projectKey) + setter := NewBuildPropertySetter(nil, targetRepo, buildName, buildNumber, projectKey, searchDir) assert.NotNil(t, setter) assert.Equal(t, buildName, setter.buildName) diff --git a/artifactory/commands/conan/upload.go b/artifactory/commands/conan/upload.go index e307ea56..17563734 100644 --- a/artifactory/commands/conan/upload.go +++ b/artifactory/commands/conan/upload.go @@ -267,7 +267,7 @@ func (up *UploadProcessor) setBuildProperties(artifacts []entities.Artifact, tar projectKey := up.buildConfiguration.GetProject() - setter := NewBuildPropertySetter(up.serverDetails, targetRepo, buildName, buildNumber, projectKey) + setter := NewBuildPropertySetter(up.serverDetails, targetRepo, buildName, buildNumber, projectKey, up.workingDir) return setter.SetProperties(artifacts) } diff --git a/artifactory/commands/flexpack/gradle/gradle.go b/artifactory/commands/flexpack/gradle/gradle.go index 9c6c1052..f3b0ff64 100644 --- a/artifactory/commands/flexpack/gradle/gradle.go +++ b/artifactory/commands/flexpack/gradle/gradle.go @@ -14,6 +14,7 @@ import ( "github.com/jfrog/build-info-go/entities" "github.com/jfrog/build-info-go/flexpack" gradle "github.com/jfrog/build-info-go/flexpack/gradle" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "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/utils/config" @@ -146,6 +147,8 @@ func setGradleBuildPropertiesOnArtifacts(workingDir, buildName, buildNumber, pro buildProps += fmt.Sprintf(";build.project=%s", projectKey) } + buildProps = civcs.MergeWithUserProps(buildProps, workingDir) + writer, err := content.NewContentWriter(content.DefaultKey, true, false) if err != nil { return fmt.Errorf("failed to create content writer: %w", err) diff --git a/artifactory/commands/flexpack/maven.go b/artifactory/commands/flexpack/maven.go index eafd8f61..0010c610 100644 --- a/artifactory/commands/flexpack/maven.go +++ b/artifactory/commands/flexpack/maven.go @@ -15,6 +15,7 @@ import ( "github.com/jfrog/build-info-go/entities" "github.com/jfrog/build-info-go/flexpack" "github.com/jfrog/gofrog/crypto" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "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/utils/config" @@ -228,6 +229,8 @@ func setMavenBuildPropertiesOnArtifacts(workingDir, buildName, buildNumber strin buildProps += fmt.Sprintf(";build.project=%s", projectKey) } + buildProps = civcs.MergeWithUserProps(buildProps, workingDir) + // Set properties on each recent artifact individually for _, artifact := range recentArtifacts { // ResultItem has Repo, Path, and Name fields already separated diff --git a/artifactory/commands/nix/command.go b/artifactory/commands/nix/command.go index 6bea235d..c829e5b3 100644 --- a/artifactory/commands/nix/command.go +++ b/artifactory/commands/nix/command.go @@ -13,6 +13,7 @@ import ( "github.com/jfrog/build-info-go/entities" nixpkg "github.com/jfrog/build-info-go/flexpack/nix" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "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/utils/config" @@ -535,7 +536,7 @@ func (c *NixCommand) tagUploadedArtifacts() error { // 1. ONE SetProps for every file in every closure directory. timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10) props := fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp) - if err := c.setBuildPropertiesBatch(c.repo, dirPaths, "*", props); err != nil { + if err := c.setBuildPropertiesBatch(c.repo, dirPaths, "*", civcs.MergeWithUserProps(props, c.workingDir)); err != nil { log.Warn("Failed to set build properties on uploaded artifacts: " + err.Error()) } diff --git a/artifactory/commands/npm/npmpublish.go b/artifactory/commands/npm/npmpublish.go index 6aec78f3..3d27d4f1 100644 --- a/artifactory/commands/npm/npmpublish.go +++ b/artifactory/commands/npm/npmpublish.go @@ -7,6 +7,7 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" gofrogcmd "github.com/jfrog/gofrog/io" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" "github.com/jfrog/jfrog-cli-core/v2/utils/config" "github.com/jfrog/jfrog-client-go/artifactory/services" @@ -90,7 +91,7 @@ func (npu *npmPublish) publishPackage(executablePath, filePath string, serverDet propsParams := services.PropsParams{ Reader: searchReader, - Props: buildProps, + Props: civcs.MergeWithUserProps(buildProps, npu.workingDirectory), } _, err = servicesManager.SetProps(propsParams) if err != nil { diff --git a/artifactory/commands/pnpm/publish.go b/artifactory/commands/pnpm/publish.go index 8c5cc6ad..b5598c53 100644 --- a/artifactory/commands/pnpm/publish.go +++ b/artifactory/commands/pnpm/publish.go @@ -10,6 +10,7 @@ import ( "github.com/jfrog/build-info-go/entities" artCliUtils "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" artCoreUtils "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/utils/config" @@ -513,7 +514,7 @@ func (ppc *PnpmPublishCommand) saveBuildArtifacts(packages []pnpmPackResult, che // finalizePublishBuildInfo is the shared tail of both collectSinglePublishBuildInfo and // collectWorkspacePublishBuildInfo. It computes checksums, resolves repos (including -// virtual → default deployment), saves build artifacts, and tags build properties. +// virtual → default deployment), saves build artifacts, and tags published properties. func (ppc *PnpmPublishCommand) finalizePublishBuildInfo(packages []pnpmPackResult, published []publishedPackage) error { checksumResults := computeChecksums(packages) log.Debug(fmt.Sprintf("Checksums computed for %d/%d package(s).", len(checksumResults), len(packages))) @@ -525,14 +526,14 @@ func (ppc *PnpmPublishCommand) finalizePublishBuildInfo(packages []pnpmPackResul return err } - ppc.tagBuildProperties(published, publishRepos, fallbackRepos) + ppc.tagPublishedProperties(published, publishRepos, fallbackRepos) log.Info(fmt.Sprintf("pnpm publish finished successfully. %d package(s) published with build info.", len(packages))) return nil } // resolveVirtualRepos resolves any virtual repositories in publishRepos and fallbackRepos // to their default deployment repositories. This must be called before saveBuildArtifacts -// and tagBuildProperties so that both use the actual local repo where artifacts land. +// and tagPublishedProperties so that both use the actual local repo where artifacts land. func (ppc *PnpmPublishCommand) resolveVirtualRepos(publishRepos map[string]string, fallbackRepos *registryMap) { if ppc.serverDetails == nil { return @@ -569,30 +570,33 @@ func (ppc *PnpmPublishCommand) resolveVirtualRepos(publishRepos map[string]strin } } -// tagBuildProperties sets build.name, build.number, build.timestamp on published -// artifacts in Artifactory. Constructs the artifact path directly from the package -// name/version and registry config, avoiding a SearchFiles API call. -func (ppc *PnpmPublishCommand) tagBuildProperties(published []publishedPackage, publishRepos map[string]string, fallbackRepos registryMap) { +// tagPublishedProperties sets build and detected VCS properties on published artifacts +// in a single SetProps call. Constructs artifact paths from package name/version and +// registry config, avoiding a SearchFiles API call. +func (ppc *PnpmPublishCommand) tagPublishedProperties(published []publishedPackage, publishRepos map[string]string, fallbackRepos registryMap) { if ppc.serverDetails == nil { - log.Debug("No server details configured. Skipping build property tagging.") + log.Debug("No server details configured. Skipping property tagging.") return } - servicesManager, err := artCoreUtils.CreateServiceManager(ppc.serverDetails, -1, 0, false) - if err != nil { - log.Warn("Unable to create service manager for build property tagging:", err.Error()) - return - } - props, err := buildUtils.CreateBuildPropsFromConfiguration(ppc.buildConfiguration) if err != nil { log.Warn("Unable to create build properties:", err.Error()) return } + props = civcs.MergeWithUserProps(props, ppc.workingDirectory) if props == "" { - log.Debug("No build properties to set. Skipping.") + log.Debug("No properties to set on published artifacts. Skipping.") return } + items := ppc.buildPublishedResultItems(published, publishRepos, fallbackRepos) + if len(items) == 0 { + log.Debug("No artifacts to tag with build properties.") + return + } + ppc.setPropsOnPublishedItems(items, props) +} +func (ppc *PnpmPublishCommand) buildPublishedResultItems(published []publishedPackage, publishRepos map[string]string, fallbackRepos registryMap) []specutils.ResultItem { var items []specutils.ResultItem for _, pkg := range published { repo := resolvePublishRepo(pkg.Name, publishRepos, fallbackRepos) @@ -607,15 +611,27 @@ func (ppc *PnpmPublishCommand) tagBuildProperties(published []publishedPackage, Name: tarballName, }) } + return items +} - if len(items) == 0 { - log.Debug("No artifacts to tag with build properties.") +func (ppc *PnpmPublishCommand) setPropsOnPublishedItems(items []specutils.ResultItem, props string) { + if props == "" || len(items) == 0 { + log.Debug("No properties to set on published artifacts. Skipping.") + return + } + if ppc.serverDetails == nil { + log.Debug("No server details configured. Skipping property tagging.") + return + } + servicesManager, err := artCoreUtils.CreateServiceManager(ppc.serverDetails, -1, 0, false) + if err != nil { + log.Warn("Unable to create service manager for property tagging:", err.Error()) return } pathToFile, err := artCliUtils.WriteResultItemsToFile(items) if err != nil { - log.Warn("Unable to write result items for build property tagging:", err.Error()) + log.Warn("Unable to write result items for properties tagging:", err.Error()) return } defer func() { @@ -633,11 +649,11 @@ func (ppc *PnpmPublishCommand) tagBuildProperties(published []publishedPackage, _, err = servicesManager.SetProps(services.PropsParams{Reader: reader, Props: props, UseDebugLogs: true}) if err != nil { - log.Warn("Unable to set build properties on published artifacts:", err.Error(), - "\nThis may cause the build to not properly link with artifacts. You can add build properties manually.") + log.Warn("Unable to set properties on published artifacts:", err.Error(), + "\nThis may cause the build to not properly link with artifacts. You can add properties manually.") return } - log.Info(fmt.Sprintf("Build properties set on %d published artifact(s).", len(items))) + log.Info(fmt.Sprintf("Properties set on %d published artifact(s).", len(items))) } // buildNpmDeployPath constructs the Artifactory path and tarball name for an npm package. diff --git a/artifactory/commands/python/native_uv.go b/artifactory/commands/python/native_uv.go index 4c32a5df..9d34c160 100644 --- a/artifactory/commands/python/native_uv.go +++ b/artifactory/commands/python/native_uv.go @@ -17,6 +17,7 @@ import ( buildinfo "github.com/jfrog/build-info-go/entities" "github.com/jfrog/build-info-go/flexpack" "github.com/jfrog/gofrog/crypto" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build" coreConfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" @@ -761,7 +762,7 @@ func uvGetBuildInfo(workingDir string, buildConfiguration *buildUtils.BuildConfi bi.Modules[0].Artifacts = artifacts } } - if propErr := uvSetBuildProperties(sd, repoKey, buildName, buildNumber, buildConfiguration.GetProject(), bi); propErr != nil { + if propErr := uvSetBuildProperties(sd, repoKey, buildName, buildNumber, buildConfiguration.GetProject(), bi, workingDir); propErr != nil { log.Warn("Failed to set build properties on artifacts: " + propErr.Error()) } } @@ -1106,7 +1107,7 @@ func uvAddArtifactsToBuildInfo(bi *buildinfo.BuildInfo, serverDetails *coreConfi } // uvSetBuildProperties sets build.name / build.number properties on uploaded Python dist artifacts. -func uvSetBuildProperties(serverDetails *coreConfig.ServerDetails, targetRepo, buildName, buildNumber, project string, bi *buildinfo.BuildInfo) error { +func uvSetBuildProperties(serverDetails *coreConfig.ServerDetails, targetRepo, buildName, buildNumber, project string, bi *buildinfo.BuildInfo, searchDir string) error { servicesManager, err := utils.CreateServiceManager(serverDetails, -1, 0, false) if err != nil { return fmt.Errorf("failed to create services manager: %w", err) @@ -1119,6 +1120,7 @@ func uvSetBuildProperties(serverDetails *coreConfig.ServerDetails, targetRepo, b if err != nil { return fmt.Errorf("CreateBuildProperties failed: %w", err) } + buildProps = civcs.MergeWithUserProps(buildProps, searchDir) if len(bi.Modules) == 0 || len(bi.Modules[0].Artifacts) == 0 { return nil diff --git a/artifactory/commands/python/twine.go b/artifactory/commands/python/twine.go index 1ca2b6be..e3c39589 100644 --- a/artifactory/commands/python/twine.go +++ b/artifactory/commands/python/twine.go @@ -11,6 +11,7 @@ import ( "github.com/jfrog/build-info-go/build" "github.com/jfrog/build-info-go/utils/pythonutils" + "github.com/jfrog/jfrog-cli-artifactory/artifactory/utils/civcs" rtUtils "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/utils/config" @@ -46,6 +47,7 @@ type TwineCommand struct { commandName string args []string targetRepo string + workingDirectory string buildConfiguration *buildUtils.BuildConfiguration } @@ -79,6 +81,13 @@ func (tc *TwineCommand) SetArgs(args []string) *TwineCommand { } func (tc *TwineCommand) Run() (err error) { + if tc.workingDirectory == "" { + tc.workingDirectory, err = os.Getwd() + if err != nil { + log.Warn("Failed to get working directory: " + err.Error()) + tc.workingDirectory = "." + } + } // Assert no forbidden flags were provided. if tc.isRepoConfigFlagProvided() { return errorutils.CheckError(errors.New(tc.getRepoConfigFlagProvidedErr())) @@ -212,9 +221,10 @@ func (tc *TwineCommand) uploadAndCollectBuildInfo() error { return err } timestamp := strconv.FormatInt(buildInfo.GetBuildTimestamp().UnixNano()/int64(time.Millisecond), 10) + props := fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp) propsParams := services.PropsParams{ Reader: searchReader, - Props: fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp), + Props: civcs.MergeWithUserProps(props, tc.workingDirectory), } _, err = servicesManager.SetProps(propsParams) if err != nil {