github.com/jfrog/jfrog-cli-core/v2@v2.51.0/artifactory/commands/buildinfo/addgit.go (about) 1 package buildinfo 2 3 import ( 4 "errors" 5 "io" 6 "os" 7 "os/exec" 8 "strconv" 9 10 buildinfo "github.com/jfrog/build-info-go/entities" 11 gofrogcmd "github.com/jfrog/gofrog/io" 12 "github.com/jfrog/jfrog-cli-core/v2/artifactory/utils" 13 "github.com/jfrog/jfrog-cli-core/v2/common/build" 14 "github.com/jfrog/jfrog-cli-core/v2/common/project" 15 utilsconfig "github.com/jfrog/jfrog-cli-core/v2/utils/config" 16 "github.com/jfrog/jfrog-client-go/artifactory/services" 17 artclientutils "github.com/jfrog/jfrog-client-go/artifactory/services/utils" 18 clientutils "github.com/jfrog/jfrog-client-go/utils" 19 20 "github.com/forPelevin/gomoji" 21 "github.com/jfrog/jfrog-client-go/utils/errorutils" 22 "github.com/jfrog/jfrog-client-go/utils/io/fileutils" 23 "github.com/jfrog/jfrog-client-go/utils/log" 24 "github.com/spf13/viper" 25 ) 26 27 const ( 28 GitLogLimit = 100 29 ConfigIssuesPrefix = "issues." 30 ConfigParseValueError = "Failed parsing %s from configuration file: %s" 31 MissingConfigurationError = "Configuration file must contain: %s" 32 ) 33 34 type BuildAddGitCommand struct { 35 buildConfiguration *build.BuildConfiguration 36 dotGitPath string 37 configFilePath string 38 serverId string 39 issuesConfig *IssuesConfiguration 40 } 41 42 func NewBuildAddGitCommand() *BuildAddGitCommand { 43 return &BuildAddGitCommand{} 44 } 45 46 func (config *BuildAddGitCommand) SetIssuesConfig(issuesConfig *IssuesConfiguration) *BuildAddGitCommand { 47 config.issuesConfig = issuesConfig 48 return config 49 } 50 51 func (config *BuildAddGitCommand) SetConfigFilePath(configFilePath string) *BuildAddGitCommand { 52 config.configFilePath = configFilePath 53 return config 54 } 55 56 func (config *BuildAddGitCommand) SetDotGitPath(dotGitPath string) *BuildAddGitCommand { 57 config.dotGitPath = dotGitPath 58 return config 59 } 60 61 func (config *BuildAddGitCommand) SetBuildConfiguration(buildConfiguration *build.BuildConfiguration) *BuildAddGitCommand { 62 config.buildConfiguration = buildConfiguration 63 return config 64 } 65 66 func (config *BuildAddGitCommand) SetServerId(serverId string) *BuildAddGitCommand { 67 config.serverId = serverId 68 return config 69 } 70 71 func (config *BuildAddGitCommand) Run() error { 72 log.Info("Reading the git branch, revision and remote URL and adding them to the build-info.") 73 buildName, err := config.buildConfiguration.GetBuildName() 74 if err != nil { 75 return err 76 } 77 buildNumber, err := config.buildConfiguration.GetBuildNumber() 78 if err != nil { 79 return err 80 } 81 err = build.SaveBuildGeneralDetails(buildName, buildNumber, config.buildConfiguration.GetProject()) 82 if err != nil { 83 return err 84 } 85 86 // Find .git if it wasn't provided in the command. 87 if config.dotGitPath == "" { 88 var exists bool 89 config.dotGitPath, exists, err = fileutils.FindUpstream(".git", fileutils.Any) 90 if err != nil { 91 return err 92 } 93 if !exists { 94 return errorutils.CheckErrorf("Could not find .git") 95 } 96 } 97 98 // Collect URL, branch and revision into GitManager. 99 gitManager := clientutils.NewGitManager(config.dotGitPath) 100 err = gitManager.ReadConfig() 101 if err != nil { 102 return err 103 } 104 105 // Collect issues if required. 106 var issues []buildinfo.AffectedIssue 107 if config.configFilePath != "" { 108 issues, err = config.collectBuildIssues(gitManager.GetUrl()) 109 if err != nil { 110 return err 111 } 112 } 113 114 // Populate partials with VCS info. 115 populateFunc := func(partial *buildinfo.Partial) { 116 partial.VcsList = append(partial.VcsList, buildinfo.Vcs{ 117 Url: gitManager.GetUrl(), 118 Revision: gitManager.GetRevision(), 119 Branch: gitManager.GetBranch(), 120 Message: gomoji.RemoveEmojis(gitManager.GetMessage()), 121 }) 122 123 if config.configFilePath != "" { 124 partial.Issues = &buildinfo.Issues{ 125 Tracker: &buildinfo.Tracker{Name: config.issuesConfig.TrackerName, Version: ""}, 126 AggregateBuildIssues: config.issuesConfig.Aggregate, 127 AggregationBuildStatus: config.issuesConfig.AggregationStatus, 128 AffectedIssues: issues, 129 } 130 } 131 } 132 err = build.SavePartialBuildInfo(buildName, buildNumber, config.buildConfiguration.GetProject(), populateFunc) 133 if err != nil { 134 return err 135 } 136 137 // Done. 138 log.Debug("Collected VCS details for", buildName+"/"+buildNumber+".") 139 return nil 140 } 141 142 // Priorities for selecting server: 143 // 1. 'server-id' flag. 144 // 2. 'serverID' in config file. 145 // 3. Default server. 146 func (config *BuildAddGitCommand) ServerDetails() (*utilsconfig.ServerDetails, error) { 147 var serverId string 148 if config.serverId != "" { 149 serverId = config.serverId 150 } else if config.configFilePath != "" { 151 // Get the server ID from the conf file. 152 var vConfig *viper.Viper 153 vConfig, err := project.ReadConfigFile(config.configFilePath, project.YAML) 154 if err != nil { 155 return nil, err 156 } 157 serverId = vConfig.GetString(ConfigIssuesPrefix + "serverID") 158 } 159 return utilsconfig.GetSpecificConfig(serverId, true, false) 160 } 161 162 func (config *BuildAddGitCommand) CommandName() string { 163 return "rt_build_add_git" 164 } 165 166 func (config *BuildAddGitCommand) collectBuildIssues(vcsUrl string) ([]buildinfo.AffectedIssue, error) { 167 log.Info("Collecting build issues from VCS...") 168 169 // Check that git exists in path. 170 _, err := exec.LookPath("git") 171 if err != nil { 172 return nil, errorutils.CheckError(err) 173 } 174 175 // Initialize issues-configuration. 176 config.issuesConfig = new(IssuesConfiguration) 177 178 // Create config's IssuesConfigurations from the provided spec file. 179 err = config.createIssuesConfigs() 180 if err != nil { 181 return nil, err 182 } 183 184 // Get latest build's VCS revision from Artifactory. 185 lastVcsRevision, err := config.getLatestVcsRevision(vcsUrl) 186 if err != nil { 187 return nil, err 188 } 189 190 // Run issues collection. 191 return config.DoCollect(config.issuesConfig, lastVcsRevision) 192 } 193 194 func (config *BuildAddGitCommand) DoCollect(issuesConfig *IssuesConfiguration, lastVcsRevision string) (foundIssues []buildinfo.AffectedIssue, err error) { 195 logRegExp, err := createLogRegExpHandler(issuesConfig, &foundIssues) 196 if err != nil { 197 return 198 } 199 200 errRegExp, err := createErrRegExpHandler(lastVcsRevision) 201 if err != nil { 202 return 203 } 204 205 // Get log with limit, starting from the latest commit. 206 logCmd := &LogCmd{logLimit: issuesConfig.LogLimit, lastVcsRevision: lastVcsRevision} 207 208 // Change working dir to where .git is. 209 wd, err := os.Getwd() 210 if errorutils.CheckError(err) != nil { 211 return 212 } 213 defer func() { 214 err = errors.Join(err, errorutils.CheckError(os.Chdir(wd))) 215 }() 216 err = os.Chdir(config.dotGitPath) 217 if errorutils.CheckError(err) != nil { 218 return 219 } 220 221 // Run git command. 222 _, _, exitOk, err := gofrogcmd.RunCmdWithOutputParser(logCmd, false, logRegExp, errRegExp) 223 if errorutils.CheckError(err) != nil { 224 var revisionRangeError RevisionRangeError 225 if errors.As(err, &revisionRangeError) { 226 // Revision not found in range. Ignore and don't collect new issues. 227 log.Info(err.Error()) 228 return []buildinfo.AffectedIssue{}, nil 229 } 230 return 231 } 232 if !exitOk { 233 // May happen when trying to run git log for non-existing revision. 234 err = errorutils.CheckErrorf("failed executing git log command") 235 } 236 return 237 } 238 239 // Creates a regexp handler to parse and fetch issues from the output of the git log command. 240 func createLogRegExpHandler(issuesConfig *IssuesConfiguration, foundIssues *[]buildinfo.AffectedIssue) (*gofrogcmd.CmdOutputPattern, error) { 241 // Create regex pattern. 242 issueRegexp, err := clientutils.GetRegExp(issuesConfig.Regexp) 243 if err != nil { 244 return nil, err 245 } 246 247 // Create handler with exec function. 248 logRegExp := gofrogcmd.CmdOutputPattern{ 249 RegExp: issueRegexp, 250 ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) { 251 // Reached here - means no error occurred. 252 253 // Check for out of bound results. 254 if len(pattern.MatchedResults)-1 < issuesConfig.KeyGroupIndex || len(pattern.MatchedResults)-1 < issuesConfig.SummaryGroupIndex { 255 return "", errors.New("unexpected result while parsing issues from git log. Make sure that the regular expression used to find issues, includes two capturing groups, for the issue ID and the summary") 256 } 257 // Create found Affected Issue. 258 foundIssue := buildinfo.AffectedIssue{Key: pattern.MatchedResults[issuesConfig.KeyGroupIndex], Summary: pattern.MatchedResults[issuesConfig.SummaryGroupIndex], Aggregated: false} 259 if issuesConfig.TrackerUrl != "" { 260 foundIssue.Url = issuesConfig.TrackerUrl + pattern.MatchedResults[issuesConfig.KeyGroupIndex] 261 } 262 *foundIssues = append(*foundIssues, foundIssue) 263 log.Debug("Found issue: " + pattern.MatchedResults[issuesConfig.KeyGroupIndex]) 264 return "", nil 265 }, 266 } 267 return &logRegExp, nil 268 } 269 270 // Error to be thrown when revision could not be found in the git revision range. 271 type RevisionRangeError struct { 272 ErrorMsg string 273 } 274 275 func (err RevisionRangeError) Error() string { 276 return err.ErrorMsg 277 } 278 279 // Creates a regexp handler to handle the event of revision missing in the git revision range. 280 func createErrRegExpHandler(lastVcsRevision string) (*gofrogcmd.CmdOutputPattern, error) { 281 // Create regex pattern. 282 invalidRangeExp, err := clientutils.GetRegExp(`fatal: Invalid revision range [a-fA-F0-9]+\.\.`) 283 if err != nil { 284 return nil, err 285 } 286 287 // Create handler with exec function. 288 errRegExp := gofrogcmd.CmdOutputPattern{ 289 RegExp: invalidRangeExp, 290 ExecFunc: func(pattern *gofrogcmd.CmdOutputPattern) (string, error) { 291 // Revision could not be found in the revision range, probably due to a squash / revert. Ignore and don't collect new issues. 292 errMsg := "Revision: '" + lastVcsRevision + "' that was fetched from latest build info does not exist in the git revision range. No new issues are added." 293 return "", RevisionRangeError{ErrorMsg: errMsg} 294 }, 295 } 296 return &errRegExp, nil 297 } 298 299 func (config *BuildAddGitCommand) createIssuesConfigs() (err error) { 300 // Read file's data. 301 err = config.issuesConfig.populateIssuesConfigsFromSpec(config.configFilePath) 302 if err != nil { 303 return 304 } 305 306 // Use 'server-id' flag if provided. 307 if config.serverId != "" { 308 config.issuesConfig.ServerID = config.serverId 309 } 310 311 // Build ServerDetails from provided serverID. 312 err = config.issuesConfig.setServerDetails() 313 if err != nil { 314 return 315 } 316 317 // Add '/' suffix to URL if required. 318 if config.issuesConfig.TrackerUrl != "" { 319 // Url should end with '/' 320 config.issuesConfig.TrackerUrl = clientutils.AddTrailingSlashIfNeeded(config.issuesConfig.TrackerUrl) 321 } 322 323 return 324 } 325 326 func (config *BuildAddGitCommand) getLatestVcsRevision(vcsUrl string) (string, error) { 327 // Get latest build's build-info from Artifactory 328 buildInfo, err := config.getLatestBuildInfo(config.issuesConfig) 329 if err != nil { 330 return "", err 331 } 332 333 // Get previous VCS Revision from BuildInfo. 334 lastVcsRevision := "" 335 for _, vcs := range buildInfo.VcsList { 336 if vcs.Url == vcsUrl { 337 lastVcsRevision = vcs.Revision 338 break 339 } 340 } 341 342 return lastVcsRevision, nil 343 } 344 345 // Returns build info, or empty build info struct if not found. 346 func (config *BuildAddGitCommand) getLatestBuildInfo(issuesConfig *IssuesConfiguration) (*buildinfo.BuildInfo, error) { 347 // Create services manager to get build-info from Artifactory. 348 sm, err := utils.CreateServiceManager(issuesConfig.ServerDetails, -1, 0, false) 349 if err != nil { 350 return nil, err 351 } 352 353 // Get latest build-info from Artifactory. 354 buildName, err := config.buildConfiguration.GetBuildName() 355 if err != nil { 356 return nil, err 357 } 358 buildInfoParams := services.BuildInfoParams{BuildName: buildName, BuildNumber: artclientutils.LatestBuildNumberKey} 359 publishedBuildInfo, found, err := sm.GetBuildInfo(buildInfoParams) 360 if err != nil { 361 return nil, err 362 } 363 if !found { 364 return &buildinfo.BuildInfo{}, nil 365 } 366 367 return &publishedBuildInfo.BuildInfo, nil 368 } 369 370 func (ic *IssuesConfiguration) populateIssuesConfigsFromSpec(configFilePath string) (err error) { 371 var vConfig *viper.Viper 372 vConfig, err = project.ReadConfigFile(configFilePath, project.YAML) 373 if err != nil { 374 return err 375 } 376 377 // Validate that the config contains issues. 378 if !vConfig.IsSet("issues") { 379 return errorutils.CheckErrorf(MissingConfigurationError, "issues") 380 } 381 382 // Get server-id. 383 if vConfig.IsSet(ConfigIssuesPrefix + "serverID") { 384 ic.ServerID = vConfig.GetString(ConfigIssuesPrefix + "serverID") 385 } 386 387 // Set log limit. 388 ic.LogLimit = GitLogLimit 389 390 // Get tracker data 391 if !vConfig.IsSet(ConfigIssuesPrefix + "trackerName") { 392 return errorutils.CheckErrorf(MissingConfigurationError, ConfigIssuesPrefix+"trackerName") 393 } 394 ic.TrackerName = vConfig.GetString(ConfigIssuesPrefix + "trackerName") 395 396 // Get issues pattern 397 if !vConfig.IsSet(ConfigIssuesPrefix + "regexp") { 398 return errorutils.CheckErrorf(MissingConfigurationError, ConfigIssuesPrefix+"regexp") 399 } 400 ic.Regexp = vConfig.GetString(ConfigIssuesPrefix + "regexp") 401 402 // Get issues base url 403 if vConfig.IsSet(ConfigIssuesPrefix + "trackerUrl") { 404 ic.TrackerUrl = vConfig.GetString(ConfigIssuesPrefix + "trackerUrl") 405 } 406 407 // Get issues key group index 408 if !vConfig.IsSet(ConfigIssuesPrefix + "keyGroupIndex") { 409 return errorutils.CheckErrorf(MissingConfigurationError, ConfigIssuesPrefix+"keyGroupIndex") 410 } 411 ic.KeyGroupIndex, err = strconv.Atoi(vConfig.GetString(ConfigIssuesPrefix + "keyGroupIndex")) 412 if err != nil { 413 return errorutils.CheckErrorf(ConfigParseValueError, ConfigIssuesPrefix+"keyGroupIndex", err.Error()) 414 } 415 416 // Get issues summary group index 417 if !vConfig.IsSet(ConfigIssuesPrefix + "summaryGroupIndex") { 418 return errorutils.CheckErrorf(MissingConfigurationError, ConfigIssuesPrefix+"summaryGroupIndex") 419 } 420 ic.SummaryGroupIndex, err = strconv.Atoi(vConfig.GetString(ConfigIssuesPrefix + "summaryGroupIndex")) 421 if err != nil { 422 return errorutils.CheckErrorf(ConfigParseValueError, ConfigIssuesPrefix+"summaryGroupIndex", err.Error()) 423 } 424 425 // Get aggregation aggregate 426 ic.Aggregate = false 427 if vConfig.IsSet(ConfigIssuesPrefix + "aggregate") { 428 ic.Aggregate, err = strconv.ParseBool(vConfig.GetString(ConfigIssuesPrefix + "aggregate")) 429 if err != nil { 430 return errorutils.CheckErrorf(ConfigParseValueError, ConfigIssuesPrefix+"aggregate", err.Error()) 431 } 432 } 433 434 // Get aggregation status 435 if vConfig.IsSet(ConfigIssuesPrefix + "aggregationStatus") { 436 ic.AggregationStatus = vConfig.GetString(ConfigIssuesPrefix + "aggregationStatus") 437 } 438 439 return nil 440 } 441 442 func (ic *IssuesConfiguration) setServerDetails() error { 443 // If no server-id provided, use default server. 444 serverDetails, err := utilsconfig.GetSpecificConfig(ic.ServerID, true, false) 445 if err != nil { 446 return err 447 } 448 ic.ServerDetails = serverDetails 449 return nil 450 } 451 452 type IssuesConfiguration struct { 453 ServerDetails *utilsconfig.ServerDetails 454 Regexp string 455 LogLimit int 456 TrackerUrl string 457 TrackerName string 458 KeyGroupIndex int 459 SummaryGroupIndex int 460 Aggregate bool 461 AggregationStatus string 462 ServerID string 463 } 464 465 type LogCmd struct { 466 logLimit int 467 lastVcsRevision string 468 } 469 470 func (logCmd *LogCmd) GetCmd() *exec.Cmd { 471 var cmd []string 472 cmd = append(cmd, "git") 473 cmd = append(cmd, "log", "--pretty=format:%s", "-"+strconv.Itoa(logCmd.logLimit)) 474 if logCmd.lastVcsRevision != "" { 475 cmd = append(cmd, logCmd.lastVcsRevision+"..") 476 } 477 return exec.Command(cmd[0], cmd[1:]...) 478 } 479 480 func (logCmd *LogCmd) GetEnv() map[string]string { 481 return map[string]string{} 482 } 483 484 func (logCmd *LogCmd) GetStdWriter() io.WriteCloser { 485 return nil 486 } 487 488 func (logCmd *LogCmd) GetErrWriter() io.WriteCloser { 489 return nil 490 }