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  }