github.com/bndr/gojenkins@v1.1.0/build.go (about) 1 // Copyright 2015 Vadim Kravcenko 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"): you may 4 // not use this file except in compliance with the License. You may obtain 5 // a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12 // License for the specific language governing permissions and limitations 13 // under the License. 14 15 package gojenkins 16 17 import ( 18 "bytes" 19 "context" 20 "errors" 21 "net/url" 22 "regexp" 23 "strconv" 24 "time" 25 ) 26 27 type Build struct { 28 Raw *BuildResponse 29 Job *Job 30 Jenkins *Jenkins 31 Base string 32 Depth int 33 } 34 35 type parameter struct { 36 Name string 37 Value string 38 } 39 40 type branch struct { 41 SHA1 string 42 Name string 43 } 44 45 type BuildRevision struct { 46 SHA1 string `json:"SHA1"` 47 Branch []branch `json:"branch"` 48 } 49 50 type Builds struct { 51 BuildNumber int64 `json:"buildNumber"` 52 BuildResult interface{} `json:"buildResult"` 53 Marked BuildRevision `json:"marked"` 54 Revision BuildRevision `json:"revision"` 55 } 56 57 type Culprit struct { 58 AbsoluteUrl string 59 FullName string 60 } 61 62 type generalObj struct { 63 Parameters []parameter `json:"parameters"` 64 Causes []map[string]interface{} `json:"causes"` 65 BuildsByBranchName map[string]Builds `json:"buildsByBranchName"` 66 LastBuiltRevision BuildRevision `json:"lastBuiltRevision"` 67 RemoteUrls []string `json:"remoteUrls"` 68 ScmName string `json:"scmName"` 69 MercurialNodeName string `json:"mercurialNodeName"` 70 MercurialRevisionNumber string `json:"mercurialRevisionNumber"` 71 Subdir interface{} `json:"subdir"` 72 TotalCount int64 73 UrlName string 74 } 75 76 type TestResult struct { 77 Duration float64 `json:"duration"` 78 Empty bool `json:"empty"` 79 FailCount int64 `json:"failCount"` 80 PassCount int64 `json:"passCount"` 81 SkipCount int64 `json:"skipCount"` 82 Suites []struct { 83 Cases []struct { 84 Age int64 `json:"age"` 85 ClassName string `json:"className"` 86 Duration float64 `json:"duration"` 87 ErrorDetails interface{} `json:"errorDetails"` 88 ErrorStackTrace interface{} `json:"errorStackTrace"` 89 FailedSince int64 `json:"failedSince"` 90 Name string `json:"name"` 91 Skipped bool `json:"skipped"` 92 SkippedMessage interface{} `json:"skippedMessage"` 93 Status string `json:"status"` 94 Stderr interface{} `json:"stderr"` 95 Stdout interface{} `json:"stdout"` 96 } `json:"cases"` 97 Duration float64 `json:"duration"` 98 ID interface{} `json:"id"` 99 Name string `json:"name"` 100 Stderr interface{} `json:"stderr"` 101 Stdout interface{} `json:"stdout"` 102 Timestamp interface{} `json:"timestamp"` 103 } `json:"suites"` 104 } 105 106 type BuildResponse struct { 107 Actions []generalObj 108 Artifacts []struct { 109 DisplayPath string `json:"displayPath"` 110 FileName string `json:"fileName"` 111 RelativePath string `json:"relativePath"` 112 } `json:"artifacts"` 113 Building bool `json:"building"` 114 BuiltOn string `json:"builtOn"` 115 ChangeSet struct { 116 Items []struct { 117 AffectedPaths []string `json:"affectedPaths"` 118 Author struct { 119 AbsoluteUrl string `json:"absoluteUrl"` 120 FullName string `json:"fullName"` 121 } `json:"author"` 122 Comment string `json:"comment"` 123 CommitID string `json:"commitId"` 124 Date string `json:"date"` 125 ID string `json:"id"` 126 Msg string `json:"msg"` 127 Paths []struct { 128 EditType string `json:"editType"` 129 File string `json:"file"` 130 } `json:"paths"` 131 Timestamp int64 `json:"timestamp"` 132 } `json:"items"` 133 Kind string `json:"kind"` 134 Revisions []struct { 135 Module string 136 Revision int 137 } `json:"revision"` 138 } `json:"changeSet"` 139 ChangeSets []struct { 140 Items []struct { 141 AffectedPaths []string `json:"affectedPaths"` 142 Author struct { 143 AbsoluteUrl string `json:"absoluteUrl"` 144 FullName string `json:"fullName"` 145 } `json:"author"` 146 Comment string `json:"comment"` 147 CommitID string `json:"commitId"` 148 Date string `json:"date"` 149 ID string `json:"id"` 150 Msg string `json:"msg"` 151 Paths []struct { 152 EditType string `json:"editType"` 153 File string `json:"file"` 154 } `json:"paths"` 155 Timestamp int64 `json:"timestamp"` 156 } `json:"items"` 157 Kind string `json:"kind"` 158 Revisions []struct { 159 Module string 160 Revision int 161 } `json:"revision"` 162 } `json:"changeSets"` 163 Culprits []Culprit `json:"culprits"` 164 Description interface{} `json:"description"` 165 Duration float64 `json:"duration"` 166 EstimatedDuration float64 `json:"estimatedDuration"` 167 Executor interface{} `json:"executor"` 168 DisplayName string `json:"displayName"` 169 FullDisplayName string `json:"fullDisplayName"` 170 ID string `json:"id"` 171 KeepLog bool `json:"keepLog"` 172 Number int64 `json:"number"` 173 QueueID int64 `json:"queueId"` 174 Result string `json:"result"` 175 Timestamp int64 `json:"timestamp"` 176 URL string `json:"url"` 177 MavenArtifacts interface{} `json:"mavenArtifacts"` 178 MavenVersionUsed string `json:"mavenVersionUsed"` 179 FingerPrint []FingerPrintResponse 180 Runs []struct { 181 Number int64 182 URL string 183 } `json:"runs"` 184 } 185 186 type consoleResponse struct { 187 Content string 188 Offset int64 189 HasMoreText bool 190 } 191 192 // Builds 193 func (b *Build) Info() *BuildResponse { 194 return b.Raw 195 } 196 197 func (b *Build) GetActions() []generalObj { 198 return b.Raw.Actions 199 } 200 201 func (b *Build) GetUrl() string { 202 return b.Raw.URL 203 } 204 205 func (b *Build) GetBuildNumber() int64 { 206 return b.Raw.Number 207 } 208 func (b *Build) GetResult() string { 209 return b.Raw.Result 210 } 211 212 func (b *Build) GetArtifacts() []Artifact { 213 artifacts := make([]Artifact, len(b.Raw.Artifacts)) 214 for i, artifact := range b.Raw.Artifacts { 215 artifacts[i] = Artifact{ 216 Jenkins: b.Jenkins, 217 Build: b, 218 FileName: artifact.FileName, 219 Path: b.Base + "/artifact/" + artifact.RelativePath, 220 } 221 } 222 return artifacts 223 } 224 225 func (b *Build) GetCulprits() []Culprit { 226 return b.Raw.Culprits 227 } 228 229 func (b *Build) Stop(ctx context.Context) (bool, error) { 230 if b.IsRunning(ctx) { 231 response, err := b.Jenkins.Requester.Post(ctx, b.Base+"/stop", nil, nil, nil) 232 if err != nil { 233 return false, err 234 } 235 return response.StatusCode == 200, nil 236 } 237 return true, nil 238 } 239 240 func (b *Build) GetConsoleOutput(ctx context.Context) string { 241 url := b.Base + "/consoleText" 242 var content string 243 b.Jenkins.Requester.GetXML(ctx, url, &content, nil) 244 return content 245 } 246 247 func (b *Build) GetConsoleOutputFromIndex(ctx context.Context, startID int64) (consoleResponse, error) { 248 strstart := strconv.FormatInt(startID, 10) 249 url := b.Base + "/logText/progressiveText" 250 251 var console consoleResponse 252 253 querymap := make(map[string]string) 254 querymap["start"] = strstart 255 rsp, err := b.Jenkins.Requester.Get(ctx, url, &console.Content, querymap) 256 if err != nil { 257 return console, err 258 } 259 260 textSize := rsp.Header.Get("X-Text-Size") 261 console.HasMoreText = len(rsp.Header.Get("X-More-Data")) != 0 262 console.Offset, err = strconv.ParseInt(textSize, 10, 64) 263 if err != nil { 264 return console, err 265 } 266 267 return console, err 268 } 269 270 func (b *Build) GetCauses(ctx context.Context) ([]map[string]interface{}, error) { 271 _, err := b.Poll(ctx) 272 if err != nil { 273 return nil, err 274 } 275 for _, a := range b.Raw.Actions { 276 if a.Causes != nil { 277 return a.Causes, nil 278 } 279 } 280 return nil, errors.New("No Causes") 281 } 282 283 func (b *Build) GetParameters() []parameter { 284 for _, a := range b.Raw.Actions { 285 if a.Parameters != nil { 286 return a.Parameters 287 } 288 } 289 return nil 290 } 291 292 func (b *Build) GetInjectedEnvVars(ctx context.Context) (map[string]string, error) { 293 var envVars struct { 294 EnvMap map[string]string `json:"envMap"` 295 } 296 endpoint := b.Base + "/injectedEnvVars" 297 _, err := b.Jenkins.Requester.GetJSON(ctx, endpoint, &envVars, nil) 298 if err != nil { 299 return envVars.EnvMap, err 300 } 301 return envVars.EnvMap, nil 302 } 303 304 func (b *Build) GetDownstreamBuilds(ctx context.Context) ([]*Build, error) { 305 result := make([]*Build, 0) 306 downstreamJobs, err := b.Job.GetDownstreamJobs(ctx) 307 if err != nil { 308 return nil, err 309 } 310 for _, job := range downstreamJobs { 311 allBuildIDs, err := job.GetAllBuildIds(ctx) 312 if err != nil { 313 return nil, err 314 } 315 for _, buildID := range allBuildIDs { 316 build, err := job.GetBuild(ctx, buildID.Number) 317 if err != nil { 318 return nil, err 319 } 320 upstreamBuild, err := build.GetUpstreamBuild(ctx) 321 // older build may no longer exist, so simply ignore these 322 // cannot compare only id, it can be from different job 323 if err == nil && b.GetUrl() == upstreamBuild.GetUrl() { 324 result = append(result, build) 325 break 326 } 327 } 328 } 329 return result, nil 330 } 331 332 func (b *Build) GetDownstreamJobNames(ctx context.Context) []string { 333 result := make([]string, 0) 334 downstreamJobs := b.Job.GetDownstreamJobsMetadata() 335 fingerprints := b.GetAllFingerPrints(ctx) 336 for _, fingerprint := range fingerprints { 337 for _, usage := range fingerprint.Raw.Usage { 338 for _, job := range downstreamJobs { 339 if job.Name == usage.Name { 340 result = append(result, job.Name) 341 } 342 } 343 } 344 } 345 return result 346 } 347 348 func (b *Build) GetAllFingerPrints(ctx context.Context) []*FingerPrint { 349 b.Poll(ctx) 350 result := make([]*FingerPrint, len(b.Raw.FingerPrint)) 351 for i, f := range b.Raw.FingerPrint { 352 result[i] = &FingerPrint{Jenkins: b.Jenkins, Base: "/fingerprint/", Id: f.Hash, Raw: &f} 353 } 354 return result 355 } 356 357 func (b *Build) GetUpstreamJob(ctx context.Context) (*Job, error) { 358 causes, err := b.GetCauses(ctx) 359 if err != nil { 360 return nil, err 361 } 362 363 for _, cause := range causes { 364 if job, ok := cause["upstreamProject"]; ok { 365 return b.Jenkins.GetJob(ctx, job.(string)) 366 } 367 } 368 return nil, errors.New("Unable to get Upstream Job") 369 } 370 371 func (b *Build) GetUpstreamBuildNumber(ctx context.Context) (int64, error) { 372 causes, err := b.GetCauses(ctx) 373 if err != nil { 374 return 0, err 375 } 376 for _, cause := range causes { 377 if build, ok := cause["upstreamBuild"]; ok { 378 switch t := build.(type) { 379 default: 380 return t.(int64), nil 381 case float64: 382 return int64(t), nil 383 } 384 } 385 } 386 return 0, nil 387 } 388 389 func (b *Build) GetUpstreamBuild(ctx context.Context) (*Build, error) { 390 job, err := b.GetUpstreamJob(ctx) 391 if err != nil { 392 return nil, err 393 } 394 if job != nil { 395 buildNumber, err := b.GetUpstreamBuildNumber(ctx) 396 if err == nil && buildNumber != 0 { 397 return job.GetBuild(ctx, buildNumber) 398 } 399 } 400 return nil, errors.New("Build not found") 401 } 402 403 func (b *Build) GetMatrixRuns(ctx context.Context) ([]*Build, error) { 404 _, err := b.Poll(ctx, 0) 405 if err != nil { 406 return nil, err 407 } 408 runs := b.Raw.Runs 409 result := make([]*Build, len(b.Raw.Runs)) 410 r, _ := regexp.Compile(`job/(.*?)/(.*?)/(\d+)/`) 411 412 for i, run := range runs { 413 result[i] = &Build{Jenkins: b.Jenkins, Job: b.Job, Raw: new(BuildResponse), Depth: 1, Base: "/" + r.FindString(run.URL)} 414 result[i].Poll(ctx) 415 } 416 return result, nil 417 } 418 419 func (b *Build) GetResultSet(ctx context.Context) (*TestResult, error) { 420 421 url := b.Base + "/testReport" 422 var report TestResult 423 424 _, err := b.Jenkins.Requester.GetJSON(ctx, url, &report, nil) 425 if err != nil { 426 return nil, err 427 } 428 429 return &report, nil 430 431 } 432 433 func (b *Build) GetTimestamp() time.Time { 434 msInt := int64(b.Raw.Timestamp) 435 return time.Unix(0, msInt*int64(time.Millisecond)) 436 } 437 438 func (b *Build) GetDuration() float64 { 439 return b.Raw.Duration 440 } 441 442 func (b *Build) GetRevision() string { 443 vcs := b.Raw.ChangeSet.Kind 444 445 if vcs == "git" || vcs == "hg" { 446 for _, a := range b.Raw.Actions { 447 if a.LastBuiltRevision.SHA1 != "" { 448 return a.LastBuiltRevision.SHA1 449 } 450 if a.MercurialRevisionNumber != "" { 451 return a.MercurialRevisionNumber 452 } 453 } 454 } else if vcs == "svn" { 455 return strconv.Itoa(b.Raw.ChangeSet.Revisions[0].Revision) 456 } 457 return "" 458 } 459 460 func (b *Build) GetRevisionBranch() string { 461 vcs := b.Raw.ChangeSet.Kind 462 if vcs == "git" { 463 for _, a := range b.Raw.Actions { 464 if len(a.LastBuiltRevision.Branch) > 0 && a.LastBuiltRevision.Branch[0].SHA1 != "" { 465 return a.LastBuiltRevision.Branch[0].SHA1 466 } 467 } 468 } else { 469 panic("Not implemented") 470 } 471 return "" 472 } 473 474 func (b *Build) IsGood(ctx context.Context) bool { 475 return (!b.IsRunning(ctx) && b.Raw.Result == STATUS_SUCCESS) 476 } 477 478 func (b *Build) IsRunning(ctx context.Context) bool { 479 _, err := b.Poll(ctx) 480 if err != nil { 481 return false 482 } 483 return b.Raw.Building 484 } 485 486 func (b *Build) SetDescription(ctx context.Context, description string) error { 487 data := url.Values{} 488 data.Set("description", description) 489 _, err := b.Jenkins.Requester.Post(ctx, b.Base+"/submitDescription", bytes.NewBufferString(data.Encode()), nil, nil) 490 return err 491 } 492 493 // Poll for current data. Optional parameter - depth. 494 // More about depth here: https://wiki.jenkins-ci.org/display/JENKINS/Remote+access+API 495 func (b *Build) Poll(ctx context.Context, options ...interface{}) (int, error) { 496 depth := "-1" 497 498 for _, o := range options { 499 switch v := o.(type) { 500 case string: 501 depth = v 502 case int: 503 depth = strconv.Itoa(v) 504 case int64: 505 depth = strconv.FormatInt(v, 10) 506 } 507 } 508 if depth == "-1" { 509 depth = strconv.Itoa(b.Depth) 510 } 511 512 qr := map[string]string{ 513 "depth": depth, 514 } 515 response, err := b.Jenkins.Requester.GetJSON(ctx, b.Base, b.Raw, qr) 516 if err != nil { 517 return 0, err 518 } 519 return response.StatusCode, nil 520 }