Skip to content

Commit 67d0bc7

Browse files
authored
Merge branch 'main' into fix/npm-publish-nil-pointer-dereference
2 parents 74d2e38 + 0375923 commit 67d0bc7

19 files changed

+4960
-58
lines changed
Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
package flexpack
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"os"
8+
"path/filepath"
9+
"strconv"
10+
"strings"
11+
"time"
12+
13+
"github.com/jfrog/build-info-go/build"
14+
"github.com/jfrog/build-info-go/entities"
15+
"github.com/jfrog/build-info-go/flexpack"
16+
gradle "github.com/jfrog/build-info-go/flexpack/gradle"
17+
"github.com/jfrog/jfrog-cli-core/v2/artifactory/utils"
18+
buildUtils "github.com/jfrog/jfrog-cli-core/v2/common/build"
19+
"github.com/jfrog/jfrog-cli-core/v2/utils/config"
20+
"github.com/jfrog/jfrog-client-go/artifactory"
21+
"github.com/jfrog/jfrog-client-go/artifactory/services"
22+
servicesutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils"
23+
"github.com/jfrog/jfrog-client-go/utils/io/content"
24+
"github.com/jfrog/jfrog-client-go/utils/log"
25+
)
26+
27+
func CollectGradleBuildInfoWithFlexPack(workingDir, buildName, buildNumber string, tasks []string, buildConfiguration *buildUtils.BuildConfiguration, serverDetails *config.ServerDetails) error {
28+
if workingDir == "" {
29+
return fmt.Errorf("working directory is required")
30+
}
31+
if buildName == "" || buildNumber == "" {
32+
return fmt.Errorf("build name and build number are required")
33+
}
34+
35+
absWorkingDir, err := filepath.Abs(workingDir)
36+
if err != nil {
37+
return fmt.Errorf("failed to resolve absolute path for working directory")
38+
}
39+
workingDir = absWorkingDir
40+
config := flexpack.GradleConfig{
41+
WorkingDirectory: workingDir,
42+
IncludeTestDependencies: true,
43+
}
44+
45+
gradleFlex, err := gradle.NewGradleFlexPack(config)
46+
if err != nil {
47+
log.Debug("failed to create Gradle FlexPack: " + err.Error())
48+
return fmt.Errorf("could not initialize Gradle FlexPack")
49+
}
50+
51+
isPublishCommand := wasPublishCommand(tasks)
52+
gradleFlex.SetWasPublishCommand(isPublishCommand)
53+
54+
buildInfo, err := gradleFlex.CollectBuildInfo(buildName, buildNumber)
55+
if err != nil {
56+
return fmt.Errorf("failed to collect build info with FlexPack")
57+
}
58+
59+
projectKey := ""
60+
if buildConfiguration != nil {
61+
projectKey = buildConfiguration.GetProject()
62+
}
63+
64+
if err := saveGradleFlexPackBuildInfo(buildInfo, projectKey); err != nil {
65+
return fmt.Errorf("failed to save build info for jfrog-cli compatibility")
66+
} else {
67+
log.Info("Build info saved locally. Use 'jf rt bp " + buildName + " " + buildNumber + "' to publish it to Artifactory.")
68+
}
69+
70+
if isPublishCommand {
71+
if err := setGradleBuildPropertiesOnArtifacts(workingDir, buildName, buildNumber, projectKey, buildInfo, serverDetails); err != nil {
72+
log.Warn("Failed to set build properties on deployed artifacts")
73+
}
74+
}
75+
log.Info("Gradle build completed successfully")
76+
return nil
77+
}
78+
79+
func wasPublishCommand(tasks []string) bool {
80+
for _, task := range tasks {
81+
// Handle tasks with project paths (e.g., ":subproject:publish")
82+
if idx := strings.LastIndex(task, ":"); idx != -1 {
83+
task = task[idx+1:]
84+
}
85+
if task == gradleTaskPublish {
86+
return true
87+
}
88+
89+
if strings.HasPrefix(task, gradleTaskPublish) {
90+
toIdx := strings.Index(task, "To")
91+
if toIdx != -1 {
92+
afterTo := task[toIdx+2:]
93+
// Exclude local publishing tasks like "publishToMavenLocal" or "publishAllPublicationsToLocal"
94+
if len(afterTo) > 0 && !strings.HasSuffix(task, "Local") {
95+
return true
96+
}
97+
}
98+
}
99+
}
100+
return false
101+
}
102+
103+
func saveGradleFlexPackBuildInfo(buildInfo *entities.BuildInfo, projectKey string) error {
104+
service := build.NewBuildInfoService()
105+
// Pass the project key to organize build info under the correct project (same as non-FlexPack flow)
106+
buildInstance, err := service.GetOrCreateBuildWithProject(buildInfo.Name, buildInfo.Number, projectKey)
107+
if err != nil {
108+
return fmt.Errorf("failed to create build: %w", err)
109+
}
110+
return buildInstance.SaveBuildInfo(buildInfo)
111+
}
112+
113+
func setGradleBuildPropertiesOnArtifacts(workingDir, buildName, buildNumber, projectKey string, buildInfo *entities.BuildInfo, serverDetails *config.ServerDetails) error {
114+
if serverDetails == nil {
115+
log.Warn("No server details configured, skipping build properties")
116+
return nil
117+
}
118+
119+
servicesManager, err := utils.CreateServiceManager(serverDetails, -1, 0, false)
120+
if err != nil {
121+
return fmt.Errorf("failed to create services manager: %w", err)
122+
}
123+
124+
artifacts := collectArtifactsFromBuildInfo(buildInfo, workingDir)
125+
if len(artifacts) == 0 {
126+
log.Warn("No artifacts found to set build properties")
127+
return nil
128+
}
129+
130+
artifacts = resolveArtifactsByChecksum(servicesManager, artifacts)
131+
buildProps, err := buildUtils.CreateBuildProperties(buildName, buildNumber, projectKey)
132+
if err != nil {
133+
// Fallback to manual creation if CreateBuildProperties fails
134+
log.Debug(fmt.Sprintf("CreateBuildProperties failed, using fallback: %s", err.Error()))
135+
timestamp := strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10)
136+
buildProps = fmt.Sprintf("build.name=%s;build.number=%s;build.timestamp=%s", buildName, buildNumber, timestamp)
137+
}
138+
139+
if projectKey != "" {
140+
buildProps += fmt.Sprintf(";build.project=%s", projectKey)
141+
}
142+
143+
writer, err := content.NewContentWriter(content.DefaultKey, true, false)
144+
if err != nil {
145+
return fmt.Errorf("failed to create content writer: %w", err)
146+
}
147+
148+
// Write all artifacts to the writer
149+
for _, art := range artifacts {
150+
writer.Write(art)
151+
}
152+
153+
if closeErr := writer.Close(); closeErr != nil {
154+
// Clean up temp file on error
155+
if writerFilePath := writer.GetFilePath(); writerFilePath != "" {
156+
if removeErr := os.Remove(writerFilePath); removeErr != nil {
157+
log.Debug(fmt.Sprintf("Failed to remove temp file after write error: %s", removeErr))
158+
}
159+
}
160+
return fmt.Errorf("failed to close content writer: %w", closeErr)
161+
}
162+
writerFilePath := writer.GetFilePath()
163+
defer func() {
164+
if writerFilePath != "" {
165+
if removeErr := os.Remove(writerFilePath); removeErr != nil {
166+
log.Debug(fmt.Sprintf("Failed to remove temp file: %s", removeErr))
167+
}
168+
}
169+
}()
170+
171+
reader := content.NewContentReader(writerFilePath, content.DefaultKey)
172+
defer func() {
173+
if closeErr := reader.Close(); closeErr != nil {
174+
log.Debug(fmt.Sprintf("Failed to close reader: %s", closeErr))
175+
}
176+
}()
177+
178+
propsParams := services.PropsParams{
179+
Reader: reader,
180+
Props: buildProps,
181+
}
182+
183+
if _, err = servicesManager.SetProps(propsParams); err != nil {
184+
return fmt.Errorf("failed to set properties on artifacts: %w", err)
185+
}
186+
187+
log.Info("Successfully set build properties on deployed Gradle artifacts")
188+
return nil
189+
}
190+
191+
func collectArtifactsFromBuildInfo(buildInfo *entities.BuildInfo, workingDir string) []servicesutils.ResultItem {
192+
// Always return empty slice instead of nil for consistent behavior
193+
if buildInfo == nil {
194+
return []servicesutils.ResultItem{}
195+
}
196+
result := make([]servicesutils.ResultItem, 0)
197+
198+
// Cache per (module dir, version) to avoid repeated lookups and wrong reuse.
199+
moduleRepoCache := make(map[string]string)
200+
201+
for _, module := range buildInfo.Modules {
202+
// Resolve repo once per module (OriginalDeploymentRepo is not populated for Gradle FlexPack).
203+
repo := resolveRepoForModule(module, workingDir, moduleRepoCache)
204+
if repo == "" {
205+
continue
206+
}
207+
208+
for _, art := range module.Artifacts {
209+
if art.Name == "" {
210+
continue
211+
}
212+
213+
itemPath := art.Path
214+
if strings.HasSuffix(itemPath, "/"+art.Name) {
215+
itemPath = strings.TrimSuffix(itemPath, "/"+art.Name)
216+
}
217+
result = append(result, servicesutils.ResultItem{
218+
Repo: repo,
219+
Path: itemPath,
220+
Name: art.Name,
221+
Sha256: art.Sha256,
222+
})
223+
}
224+
}
225+
return result
226+
}
227+
228+
func resolveArtifactsByChecksum(servicesManager artifactory.ArtifactoryServicesManager, artifacts []servicesutils.ResultItem) []servicesutils.ResultItem {
229+
if servicesManager == nil || len(artifacts) == 0 {
230+
return artifacts
231+
}
232+
233+
cache := make(map[string][]servicesutils.ResultItem)
234+
resolved := make([]servicesutils.ResultItem, 0, len(artifacts))
235+
236+
for _, art := range artifacts {
237+
if art.Sha256 == "" {
238+
resolved = append(resolved, art)
239+
continue
240+
}
241+
242+
if cached, ok := cache[art.Sha256]; ok {
243+
if len(cached) == 0 {
244+
resolved = append(resolved, art)
245+
} else {
246+
resolved = append(resolved, cached...)
247+
}
248+
continue
249+
}
250+
251+
results, err := searchArtifactsBySha256(servicesManager, art.Sha256, art.Repo, art.Path)
252+
if err != nil {
253+
log.Debug(fmt.Sprintf("Checksum AQL search failed for %s/%s/%s: %s", art.Repo, art.Path, art.Name, err))
254+
cache[art.Sha256] = nil
255+
resolved = append(resolved, art)
256+
continue
257+
}
258+
if len(results) == 0 {
259+
cache[art.Sha256] = nil
260+
resolved = append(resolved, art)
261+
continue
262+
}
263+
264+
cache[art.Sha256] = results
265+
resolved = append(resolved, results...)
266+
}
267+
return resolved
268+
}
269+
270+
func searchArtifactsBySha256(servicesManager artifactory.ArtifactoryServicesManager, sha256, repo, pathHint string) ([]servicesutils.ResultItem, error) {
271+
if sha256 == "" {
272+
return []servicesutils.ResultItem{}, nil
273+
}
274+
275+
criteria := []string{fmt.Sprintf(`"sha256": { "$eq": "%s" }`, sha256)}
276+
if repo != "" {
277+
criteria = append(criteria, fmt.Sprintf(`"repo": { "$eq": "%s" }`, repo))
278+
}
279+
if pathHint != "" {
280+
criteria = append(criteria, fmt.Sprintf(`"path": { "$match": "%s*" }`, strings.TrimSuffix(pathHint, "/")))
281+
}
282+
283+
aql := fmt.Sprintf(`items.find({ %s }).include("name", "repo", "path", "sha256", "type", "modified", "size")`, strings.Join(criteria, ", "))
284+
285+
reader, err := servicesManager.Aql(aql)
286+
if err != nil {
287+
return nil, err
288+
}
289+
defer func() {
290+
if reader != nil {
291+
_ = reader.Close()
292+
}
293+
}()
294+
295+
raw, err := io.ReadAll(reader)
296+
if err != nil {
297+
return nil, err
298+
}
299+
300+
var parsed servicesutils.AqlSearchResult
301+
if err := json.Unmarshal(raw, &parsed); err != nil {
302+
return nil, err
303+
}
304+
305+
if parsed.Results == nil {
306+
return []servicesutils.ResultItem{}, nil
307+
}
308+
return parsed.Results, nil
309+
}
310+
311+
func resolveRepoForModule(module entities.Module, workingDir string, moduleRepoCache map[string]string) string {
312+
version := inferModuleVersion(module.Id)
313+
moduleWorkingDir := resolveModuleDir(module, workingDir)
314+
315+
cacheKey := fmt.Sprintf("%s|%s", moduleWorkingDir, version)
316+
rootCacheKey := fmt.Sprintf("%s|%s", workingDir, version)
317+
318+
if cached, ok := moduleRepoCache[cacheKey]; ok {
319+
return cached
320+
}
321+
322+
repo, err := getGradleDeployRepository(moduleWorkingDir, workingDir, version)
323+
if err != nil && moduleWorkingDir != workingDir {
324+
log.Debug(fmt.Sprintf("Repo not found in module dir %s, trying root: %v", moduleWorkingDir, err))
325+
if cached, ok := moduleRepoCache[rootCacheKey]; ok && cached != "" {
326+
moduleRepoCache[cacheKey] = cached
327+
return cached
328+
}
329+
repo, err = getGradleDeployRepository(workingDir, workingDir, version)
330+
if err == nil {
331+
moduleRepoCache[rootCacheKey] = repo
332+
}
333+
}
334+
if err != nil {
335+
log.Warn("failed to resolve Gradle deploy repository for module " + module.Id)
336+
}
337+
if repo == "" {
338+
log.Warn("Gradle deploy repository not found for module " + module.Id + ", skipping artifacts without repo")
339+
}
340+
moduleRepoCache[cacheKey] = repo
341+
return repo
342+
}
343+
344+
func inferModuleVersion(id string) string {
345+
if parts := strings.Split(id, ":"); len(parts) >= 3 {
346+
return parts[len(parts)-1]
347+
}
348+
return ""
349+
}
350+
351+
func resolveModuleDir(module entities.Module, workingDir string) string {
352+
moduleDir := workingDir
353+
354+
if moduleName := extractModuleName(module.Properties); moduleName != "" {
355+
relPath := strings.ReplaceAll(moduleName, ":", string(filepath.Separator))
356+
candidate := filepath.Join(workingDir, relPath)
357+
cleanCandidate := filepath.Clean(candidate)
358+
359+
if rel, err := filepath.Rel(workingDir, cleanCandidate); err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
360+
if stat, err := os.Stat(cleanCandidate); err == nil && stat.IsDir() {
361+
return cleanCandidate
362+
}
363+
log.Debug(fmt.Sprintf("Module directory %s from moduleName '%s' not found, using root %s", cleanCandidate, moduleName, workingDir))
364+
} else {
365+
log.Debug(fmt.Sprintf("Ignoring moduleName '%s' due to path traversal, using root %s", moduleName, workingDir))
366+
}
367+
}
368+
return moduleDir
369+
}
370+
371+
func extractModuleName(props interface{}) string {
372+
switch val := props.(type) {
373+
case map[string]string:
374+
return val["moduleName"]
375+
case map[string]interface{}:
376+
if name, ok := val["moduleName"]; ok {
377+
if nameStr, ok := name.(string); ok {
378+
return nameStr
379+
}
380+
}
381+
}
382+
return ""
383+
}
384+
385+
// ValidateWorkingDirectory checks if the working directory is valid.
386+
func ValidateWorkingDirectory(workingDir string) error {
387+
if workingDir == "" {
388+
return fmt.Errorf("working directory cannot be empty")
389+
}
390+
info, err := os.Stat(workingDir)
391+
if err != nil {
392+
return fmt.Errorf("invalid working directory: %s - %w", workingDir, err)
393+
}
394+
if !info.IsDir() {
395+
return fmt.Errorf("working directory is not a directory: %s", workingDir)
396+
}
397+
return nil
398+
}

0 commit comments

Comments
 (0)