@@ -2,19 +2,28 @@ package entities
22
33import (
44 "errors"
5+ "fmt"
6+ "os"
57 "regexp"
68 "sort"
79 "strings"
810 "time"
911
1012 "github.com/jfrog/build-info-go/utils/compareutils"
13+ "github.com/jfrog/gofrog/log"
1114 "golang.org/x/exp/maps"
1215 "golang.org/x/exp/slices"
1316
1417 cdx "github.com/CycloneDX/cyclonedx-go"
1518 "github.com/jfrog/gofrog/stringutils"
1619)
1720
21+ // MergeArtifactsByNameEnv controls whether artifacts with the same name but different SHA1s
22+ // should be merged (keeping the newer one). This handles non-deterministic builds like Maven SNAPSHOTs.
23+ // Set to "FALSE" to disable name-based merging and only use SHA1 comparison.
24+ // Default: enabled (any value other than "FALSE")
25+ const MergeArtifactsByNameEnv = "JFROG_CLI_MERGE_BUILD_INFO_ARTIFACTS_BY_NAME"
26+
1827type ModuleType string
1928
2029const (
@@ -255,20 +264,70 @@ func mergeModules(merge *Module, into *Module) {
255264}
256265
257266func mergeArtifacts (mergeArtifacts * []Artifact , intoArtifacts * []Artifact ) {
258- for _ , mergeArtifact := range * mergeArtifacts {
267+ mergeByNameEnabled := strings .ToUpper (os .Getenv (MergeArtifactsByNameEnv )) != "FALSE"
268+ log .Debug (fmt .Sprintf ("Merge artifacts by name enabled: %v" , mergeByNameEnabled ))
269+
270+ for _ , newArtifact := range * mergeArtifacts {
259271 exists := false
260- for _ , artifact := range * intoArtifacts {
261- if mergeArtifact .Sha1 == artifact .Sha1 {
272+
273+ // PRIORITY 1: Check SHA1 - if checksums match, artifact already exists (skip it)
274+ for _ , existingArtifact := range * intoArtifacts {
275+ if newArtifact .Sha1 == existingArtifact .Sha1 {
262276 exists = true
263277 break
264278 }
265279 }
280+
281+ // PRIORITY 2: If SHA1 doesn't match, check by artifact name
282+ // This handles non-deterministic builds (Maven WAR with timestamps, etc.)
283+ // where rebuilding produces different checksums for the same logical artifact.
284+ if ! exists && mergeByNameEnabled {
285+ for i , existingArtifact := range * intoArtifacts {
286+ if newArtifact .Name == existingArtifact .Name && isSameLogicalArtifact (existingArtifact , newArtifact ) {
287+ // Same logical artifact but different checksum
288+ // Use incoming artifact (from later merge operation)
289+ log .Warn (fmt .Sprintf ("Artifact '%s' has different checksums (%s vs %s). Using incoming artifact." ,
290+ newArtifact .Name , existingArtifact .Sha1 , newArtifact .Sha1 ))
291+ (* intoArtifacts )[i ] = newArtifact
292+ exists = true
293+ break
294+ }
295+ }
296+ }
297+
298+ // No match found - add as new artifact
266299 if ! exists {
267- * intoArtifacts = append (* intoArtifacts , mergeArtifact )
300+ * intoArtifacts = append (* intoArtifacts , newArtifact )
268301 }
269302 }
270303}
271304
305+ // isSameLogicalArtifact checks if two artifacts represent the same logical artifact.
306+ // Returns true if they are in the same directory and same repository.
307+ func isSameLogicalArtifact (existing , new Artifact ) bool {
308+ // Check if paths are in the same directory
309+ if extractPathDir (existing .Path ) != extractPathDir (new .Path ) {
310+ return false
311+ }
312+
313+ // Check repo - reject if BOTH have repos AND they differ
314+ if existing .OriginalDeploymentRepo != "" && new .OriginalDeploymentRepo != "" &&
315+ existing .OriginalDeploymentRepo != new .OriginalDeploymentRepo {
316+ return false
317+ }
318+
319+ return true
320+ }
321+
322+ // extractPathDir extracts the directory portion from an artifact path
323+ func extractPathDir (path string ) string {
324+ lastSlash := strings .LastIndex (path , "/" )
325+ if lastSlash == - 1 {
326+ return ""
327+ }
328+ return path [:lastSlash ]
329+ }
330+
272331func mergeDependenciesLists (dependenciesToAdd , intoDependencies * []Dependency ) {
273332 for i , dependencyToAdd := range * dependenciesToAdd {
274333 exists := false
0 commit comments