github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/step/step_changelog.go (about)

     1  package step
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"sort"
    12  	"strings"
    13  	"text/template"
    14  	"time"
    15  
    16  	"github.com/jenkins-x/jx/v2/pkg/builds"
    17  
    18  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts/step"
    19  
    20  	"github.com/jenkins-x/jx/v2/pkg/dependencymatrix"
    21  
    22  	"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
    23  	"github.com/jenkins-x/jx/v2/pkg/kube/naming"
    24  
    25  	"github.com/pkg/errors"
    26  
    27  	"github.com/jenkins-x/jx/v2/pkg/users"
    28  
    29  	"github.com/ghodss/yaml"
    30  	jenkinsio "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io"
    31  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    32  	"github.com/jenkins-x/jx-logging/pkg/log"
    33  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    34  	"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
    35  	"github.com/jenkins-x/jx/v2/pkg/gits"
    36  	"github.com/jenkins-x/jx/v2/pkg/issues"
    37  	"github.com/jenkins-x/jx/v2/pkg/kube"
    38  	"github.com/jenkins-x/jx/v2/pkg/util"
    39  	"github.com/spf13/cobra"
    40  	"gopkg.in/src-d/go-git.v4/plumbing/object"
    41  
    42  	chgit "github.com/antham/chyle/chyle/git"
    43  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    44  )
    45  
    46  // StepChangelogOptions contains the command line flags
    47  type StepChangelogOptions struct {
    48  	step.StepOptions
    49  
    50  	PreviousRevision    string
    51  	PreviousDate        string
    52  	CurrentRevision     string
    53  	TemplatesDir        string
    54  	ReleaseYamlFile     string
    55  	CrdYamlFile         string
    56  	Dir                 string
    57  	Version             string
    58  	Build               string
    59  	Header              string
    60  	HeaderFile          string
    61  	Footer              string
    62  	FooterFile          string
    63  	OutputMarkdownFile  string
    64  	OverwriteCRD        bool
    65  	GenerateCRD         bool
    66  	GenerateReleaseYaml bool
    67  	UpdateRelease       bool
    68  	NoReleaseInDev      bool
    69  	IncludeMergeCommits bool
    70  	FailIfFindCommits   bool
    71  	State               StepChangelogState
    72  }
    73  
    74  type StepChangelogState struct {
    75  	GitInfo         *gits.GitRepository
    76  	GitProvider     gits.GitProvider
    77  	Tracker         issues.IssueProvider
    78  	FoundIssueNames map[string]bool
    79  	LoggedIssueKind bool
    80  	Release         *v1.Release
    81  }
    82  
    83  const (
    84  	ReleaseName = `{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}`
    85  
    86  	SpecName    = `{{ .Chart.Name }}`
    87  	SpecVersion = `{{ .Chart.Version }}`
    88  
    89  	ReleaseCrdYaml = `apiVersion: apiextensions.k8s.io/v1beta1
    90  kind: CustomResourceDefinition
    91  metadata:
    92    creationTimestamp: 2018-02-24T14:56:33Z
    93    name: releases.jenkins.io
    94    resourceVersion: "557150"
    95    selfLink: /apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/releases.jenkins.io
    96    uid: e77f4e08-1972-11e8-988e-42010a8401df
    97  spec:
    98    group: jenkins.io
    99    names:
   100      kind: Release
   101      listKind: ReleaseList
   102      plural: releases
   103      shortNames:
   104      - rel
   105      singular: release
   106      categories:
   107      - all
   108    scope: Namespaced
   109    version: v1`
   110  )
   111  
   112  var (
   113  	GitAccessDescription = `
   114  
   115  By default jx commands look for a file '~/.jx/gitAuth.yaml' to find the API tokens for Git servers. You can use 'jx create git token' to create a Git token.
   116  
   117  Alternatively if you are running this command inside a CI server you can use environment variables to specify the username and API token.
   118  e.g. define environment variables GIT_USERNAME and GIT_API_TOKEN
   119  `
   120  
   121  	StepChangelogLong = templates.LongDesc(`
   122  		Generates a Changelog for the latest tag
   123  
   124  		This command will generate a Changelog as markdown for the git commit range given. 
   125  		If you are using GitHub it will also update the GitHub Release with the changelog. You can disable that by passing'--update-release=false'
   126  
   127  		If you have just created a git tag this command will try default to the changes between the last tag and the previous one. You can always specify the exact Git references (tag/sha) directly via '--previous-rev' and '--rev'
   128  
   129  		The changelog is generated by parsing the git commits. It will also detect any text like 'fixes #123' to link to issue fixes. You can also use Conventional Commits notation: https://conventionalcommits.org/ to get a nicer formatted changelog. e.g. using commits like 'fix:(my feature) this my fix' or 'feat:(cheese) something'
   130  
   131  		This command also generates a Release Custom Resource Definition you can include in your helm chart to give metadata about the changelog of the application along with metadata about the release (git tag, url, commits, issues fixed etc). Including this metadata in a helm charts means we can do things like automatically comment on issues when they hit Staging or Production; or give detailed descriptions of what things have changed when using GitOps to update versions in an environment by referencing the fixed issues in the Pull Request.
   132  
   133  		You can opt out of the release YAML generation via the '--generate-yaml=false' option
   134  		
   135  		To update the release notes on GitHub / Gitea this command needs a git API token.
   136  
   137  `) + GitAccessDescription
   138  
   139  	StepChangelogExample = templates.Examples(`
   140  		# generate a changelog on the current source
   141  		jx step changelog
   142  
   143  		# specify the version to use
   144  		jx step changelog --version 1.2.3
   145  
   146  		# specify the version and a header template
   147  		jx step changelog --header-file docs/dev/changelog-header.md --version 1.2.3
   148  
   149  `)
   150  
   151  	GitHubIssueRegex = regexp.MustCompile(`(\#\d+)`)
   152  	JIRAIssueRegex   = regexp.MustCompile(`[A-Z][A-Z]+-(\d+)`)
   153  )
   154  
   155  func NewCmdStepChangelog(commonOpts *opts.CommonOptions) *cobra.Command {
   156  	options := StepChangelogOptions{
   157  		StepOptions: step.StepOptions{
   158  			CommonOptions: commonOpts,
   159  		},
   160  	}
   161  	cmd := &cobra.Command{
   162  		Use:     "changelog",
   163  		Short:   "Creates a changelog for a git tag",
   164  		Aliases: []string{"changes"},
   165  		Long:    StepChangelogLong,
   166  		Example: StepChangelogExample,
   167  		Run: func(cmd *cobra.Command, args []string) {
   168  			options.Cmd = cmd
   169  			options.Args = args
   170  			err := options.Run()
   171  			helper.CheckErr(err)
   172  		},
   173  	}
   174  
   175  	cmd.Flags().StringVarP(&options.PreviousRevision, "previous-rev", "p", "", "the previous tag revision")
   176  	cmd.Flags().StringVarP(&options.PreviousDate, "previous-date", "", "", "the previous date to find a revision in format 'MonthName dayNumber year'")
   177  	cmd.Flags().StringVarP(&options.CurrentRevision, "rev", "r", "", "the current tag revision")
   178  	cmd.Flags().StringVarP(&options.TemplatesDir, "templates-dir", "t", "", "the directory containing the helm chart templates to generate the resources")
   179  	cmd.Flags().StringVarP(&options.ReleaseYamlFile, "release-yaml-file", "", "release.yaml", "the name of the file to generate the Release YAML")
   180  	cmd.Flags().StringVarP(&options.CrdYamlFile, "crd-yaml-file", "", "release-crd.yaml", "the name of the file to generate the Release CustomResourceDefinition YAML")
   181  	cmd.Flags().StringVarP(&options.Version, "version", "v", "", "The version to release")
   182  	cmd.Flags().StringVarP(&options.Build, "build", "", "", "The Build number which is used to update the PipelineActivity. If not specified its defaulted from  the '$BUILD_NUMBER' environment variable")
   183  	cmd.Flags().StringVarP(&options.Dir, "dir", "", "", "The directory of the Git repository. Defaults to the current working directory")
   184  	cmd.Flags().StringVarP(&options.OutputMarkdownFile, "output-markdown", "", "", "The file to generate for the changelog output if not updating a Git provider release")
   185  	cmd.Flags().BoolVarP(&options.OverwriteCRD, "overwrite", "o", false, "overwrites the Release CRD YAML file if it exists")
   186  	cmd.Flags().BoolVarP(&options.GenerateCRD, "crd", "c", false, "Generate the CRD in the chart")
   187  	cmd.Flags().BoolVarP(&options.GenerateReleaseYaml, "generate-yaml", "y", true, "Generate the Release YAML in the local helm chart")
   188  	cmd.Flags().BoolVarP(&options.UpdateRelease, "update-release", "", true, "Should we update the release on the Git repository with the changelog")
   189  	cmd.Flags().BoolVarP(&options.NoReleaseInDev, "no-dev-release", "", false, "Disables the generation of Release CRDs in the development namespace to track releases being performed")
   190  	cmd.Flags().BoolVarP(&options.IncludeMergeCommits, "include-merge-commits", "", false, "Include merge commits when generating the changelog")
   191  	cmd.Flags().BoolVarP(&options.FailIfFindCommits, "fail-if-no-commits", "", false, "Do we want to fail the build if we don't find any commits to generate the changelog")
   192  
   193  	cmd.Flags().StringVarP(&options.Header, "header", "", "", "The changelog header in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/")
   194  	cmd.Flags().StringVarP(&options.HeaderFile, "header-file", "", "", "The file name of the changelog header in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/")
   195  	cmd.Flags().StringVarP(&options.Footer, "footer", "", "", "The changelog footer in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/")
   196  	cmd.Flags().StringVarP(&options.FooterFile, "footer-file", "", "", "The file name of the changelog footer in markdown for the changelog. Can use go template expressions on the ReleaseSpec object: https://golang.org/pkg/text/template/")
   197  
   198  	return cmd
   199  }
   200  
   201  func (o *StepChangelogOptions) Run() error {
   202  	// lets enable batch mode if we detect we are inside a pipeline
   203  	if !o.BatchMode && builds.GetBuildNumber() != "" {
   204  		log.Logger().Info("Using batch mode as inside a pipeline")
   205  		o.BatchMode = true
   206  	}
   207  
   208  	dir := o.Dir
   209  	var err error
   210  	if dir == "" {
   211  		dir, err = os.Getwd()
   212  		if err != nil {
   213  			return err
   214  		}
   215  	}
   216  
   217  	// Ensure we don't have a shallow checkout in git
   218  	err = gits.Unshallow(dir, o.Git())
   219  	if err != nil {
   220  		return errors.Wrapf(err, "error unshallowing git repo in %s", dir)
   221  	}
   222  	previousRev := o.PreviousRevision
   223  	if previousRev == "" {
   224  		previousDate := o.PreviousDate
   225  		if previousDate != "" {
   226  			previousRev, err = o.Git().GetRevisionBeforeDateText(dir, previousDate)
   227  			if err != nil {
   228  				return fmt.Errorf("Failed to find commits before date %s: %s", previousDate, err)
   229  			}
   230  		}
   231  	}
   232  	if previousRev == "" {
   233  		previousRev, _, err = o.Git().GetCommitPointedToByPreviousTag(dir)
   234  		if err != nil {
   235  			return err
   236  		}
   237  		if previousRev == "" {
   238  			// lets assume we are the first release
   239  			previousRev, err = o.Git().GetFirstCommitSha(dir)
   240  			if err != nil {
   241  				return errors.Wrap(err, "failed to find first commit after we found no previous releaes")
   242  			}
   243  			if previousRev == "" {
   244  				log.Logger().Info("no previous commit version found so change diff unavailable")
   245  				return nil
   246  			}
   247  		}
   248  	}
   249  	currentRev := o.CurrentRevision
   250  	if currentRev == "" {
   251  		currentRev, _, err = o.Git().GetCommitPointedToByLatestTag(dir)
   252  		if err != nil {
   253  			return err
   254  		}
   255  	}
   256  
   257  	templatesDir := o.TemplatesDir
   258  	if templatesDir == "" {
   259  		chartFile, err := o.FindHelmChart()
   260  		if err != nil {
   261  			return fmt.Errorf("Could not find helm chart %s", err)
   262  		}
   263  		path, _ := filepath.Split(chartFile)
   264  		templatesDir = filepath.Join(path, "templates")
   265  	}
   266  	err = os.MkdirAll(templatesDir, util.DefaultWritePermissions)
   267  	if err != nil {
   268  		return fmt.Errorf("Failed to create the templates directory %s due to %s", templatesDir, err)
   269  	}
   270  
   271  	log.Logger().Infof("Generating change log from git ref %s => %s", util.ColorInfo(previousRev), util.ColorInfo(currentRev))
   272  
   273  	gitDir, gitConfDir, err := o.Git().FindGitConfigDir(dir)
   274  	if err != nil {
   275  		return err
   276  	}
   277  	if gitDir == "" || gitConfDir == "" {
   278  		log.Logger().Warnf("No git directory could be found from dir %s", dir)
   279  		return nil
   280  	}
   281  
   282  	gitUrl, err := o.Git().DiscoverUpstreamGitURL(gitConfDir)
   283  	if err != nil {
   284  		return err
   285  	}
   286  	gitInfo, err := gits.ParseGitURL(gitUrl)
   287  	if err != nil {
   288  		return err
   289  	}
   290  	o.State.GitInfo = gitInfo
   291  
   292  	tracker, err := o.CreateIssueProvider(dir)
   293  	if err != nil {
   294  		return err
   295  	}
   296  	o.State.Tracker = tracker
   297  
   298  	authConfigSvc, err := o.GitAuthConfigService()
   299  	if err != nil {
   300  		return err
   301  	}
   302  	jxClient, devNs, err := o.JXClientAndDevNamespace()
   303  	if err != nil {
   304  		return err
   305  	}
   306  
   307  	gitKind, err := o.GitServerKind(gitInfo)
   308  	foundGitProvider := true
   309  	ghOwner, err := o.GetGitHubAppOwner(gitInfo)
   310  	if err != nil {
   311  		return err
   312  	}
   313  	gitProvider, err := o.State.GitInfo.CreateProvider(o.InCluster(), authConfigSvc, gitKind, ghOwner, o.Git(), o.BatchMode, o.GetIOFileHandles())
   314  	if err != nil {
   315  		foundGitProvider = false
   316  		log.Logger().Warnf("Could not create GitProvide so cannot update the release notes: %s", err)
   317  	}
   318  	o.State.GitProvider = gitProvider
   319  	o.State.FoundIssueNames = map[string]bool{}
   320  
   321  	commits, err := chgit.FetchCommits(gitDir, previousRev, currentRev)
   322  	if err != nil {
   323  		if o.FailIfFindCommits {
   324  			return err
   325  		}
   326  		log.Logger().Warnf("failed to find git commits between revision %s and %s due to: %s", previousRev, currentRev, err.Error())
   327  	}
   328  	if commits != nil {
   329  		commits1 := *commits
   330  		if len(commits1) > 0 {
   331  			if strings.HasPrefix(commits1[0].Message, "release ") {
   332  				// remove the release commit from the log
   333  				tmp := commits1[1:]
   334  				commits = &tmp
   335  			}
   336  		}
   337  		log.Logger().Debugf("Found commits:")
   338  		for _, commit := range *commits {
   339  			log.Logger().Debugf("  commit %s", commit.Hash)
   340  			log.Logger().Debugf("  Author: %s <%s>", commit.Author.Name, commit.Author.Email)
   341  			log.Logger().Debugf("  Date: %s", commit.Committer.When.Format(time.ANSIC))
   342  			log.Logger().Debugf("      %s\n\n\n", commit.Message)
   343  		}
   344  	}
   345  	version := o.Version
   346  	if version == "" {
   347  		version = SpecVersion
   348  	}
   349  
   350  	release := &v1.Release{
   351  		TypeMeta: metav1.TypeMeta{
   352  			Kind:       "Release",
   353  			APIVersion: jenkinsio.GroupAndVersion,
   354  		},
   355  		ObjectMeta: metav1.ObjectMeta{
   356  			Name: ReleaseName,
   357  			CreationTimestamp: metav1.Time{
   358  				Time: time.Now(),
   359  			},
   360  			//ResourceVersion:   "1",
   361  			DeletionTimestamp: &metav1.Time{},
   362  		},
   363  		Spec: v1.ReleaseSpec{
   364  			Name:          SpecName,
   365  			Version:       version,
   366  			GitOwner:      gitInfo.Organisation,
   367  			GitRepository: gitInfo.Name,
   368  			GitHTTPURL:    gitInfo.HttpsURL(),
   369  			GitCloneURL:   gits.HttpCloneURL(gitInfo, gitKind),
   370  			Commits:       []v1.CommitSummary{},
   371  			Issues:        []v1.IssueSummary{},
   372  			PullRequests:  []v1.IssueSummary{},
   373  		},
   374  	}
   375  
   376  	resolver := users.GitUserResolver{
   377  		GitProvider: gitProvider,
   378  		Namespace:   devNs,
   379  		JXClient:    jxClient,
   380  	}
   381  	if commits != nil && gitProvider != nil {
   382  		for _, commit := range *commits {
   383  			c := commit
   384  			if o.IncludeMergeCommits || len(commit.ParentHashes) <= 1 {
   385  				o.addCommit(&release.Spec, &c, &resolver)
   386  			}
   387  		}
   388  	}
   389  
   390  	release.Spec.DependencyUpdates = CollapseDependencyUpdates(release.Spec.DependencyUpdates)
   391  
   392  	// lets try to update the release
   393  	markdown, err := gits.GenerateMarkdown(&release.Spec, gitInfo)
   394  	if err != nil {
   395  		return err
   396  	}
   397  	header, err := o.getTemplateResult(&release.Spec, "header", o.Header, o.HeaderFile)
   398  	if err != nil {
   399  		return err
   400  	}
   401  	footer, err := o.getTemplateResult(&release.Spec, "footer", o.Footer, o.FooterFile)
   402  	if err != nil {
   403  		return err
   404  	}
   405  	markdown = header + markdown + footer
   406  
   407  	log.Logger().Debugf("Generated release notes:\n\n%s\n", markdown)
   408  
   409  	if version != "" && o.UpdateRelease && foundGitProvider {
   410  		tags, err := o.Git().FilterTags(o.Dir, version)
   411  		if err != nil {
   412  			return errors.Wrapf(err, "listing tags with pattern %s in %s", version, o.Dir)
   413  		}
   414  		vVersion := fmt.Sprintf("v%s", version)
   415  		vtags, err := o.Git().FilterTags(o.Dir, vVersion)
   416  		if err != nil {
   417  			return errors.Wrapf(err, "listing tags with pattern %s in %s", vVersion, o.Dir)
   418  		}
   419  		foundTag := false
   420  		foundVTag := false
   421  
   422  		for _, t := range tags {
   423  			if t == version {
   424  				foundTag = true
   425  				break
   426  			}
   427  		}
   428  		for _, t := range vtags {
   429  			if t == vVersion {
   430  				foundVTag = true
   431  				break
   432  			}
   433  		}
   434  		tagName := version
   435  		if foundVTag && !foundTag {
   436  			tagName = vVersion
   437  		}
   438  		releaseInfo := &gits.GitRelease{
   439  			Name:    version,
   440  			TagName: tagName,
   441  			Body:    markdown,
   442  		}
   443  		url := releaseInfo.HTMLURL
   444  		if url == "" {
   445  			url = releaseInfo.URL
   446  		}
   447  		if url == "" {
   448  			url = util.UrlJoin(gitInfo.HttpsURL(), "releases/tag", tagName)
   449  		}
   450  		err = gitProvider.UpdateRelease(gitInfo.Organisation, gitInfo.Name, tagName, releaseInfo)
   451  		if err != nil {
   452  			log.Logger().Warnf("Failed to update the release at %s: %s", url, err)
   453  			return nil
   454  		}
   455  		release.Spec.ReleaseNotesURL = url
   456  		log.Logger().Infof("Updated the release information at %s", util.ColorInfo(url))
   457  
   458  		// First, attach the current dependency matrix
   459  		dependencyMatrixFileName := filepath.Join(dir, dependencymatrix.DependencyMatrixDirName, dependencymatrix.DependencyMatrixYamlFileName)
   460  		if info, err := os.Stat(dependencyMatrixFileName); err != nil && os.IsNotExist(err) {
   461  			log.Logger().Debugf("Not adding dependency matrix %s as does not exist", dependencyMatrixFileName)
   462  		} else if err != nil {
   463  			return errors.Wrapf(err, "checking if %s exists", dependencyMatrixFileName)
   464  		} else if info.Size() == 0 {
   465  			log.Logger().Debugf("Not adding dependency matrix %s as has no content", dependencyMatrixFileName)
   466  		} else {
   467  			file, err := os.Open(dependencyMatrixFileName)
   468  			// The file will be closed by the release asset uploader
   469  			if err != nil {
   470  				return errors.Wrapf(err, "opening %s", dependencyMatrixFileName)
   471  			}
   472  			releaseAsset, err := gitProvider.UploadReleaseAsset(gitInfo.Organisation, gitInfo.Name, releaseInfo.ID, dependencymatrix.DependencyMatrixAssetName, file)
   473  			if err != nil {
   474  				return errors.Wrapf(err, "uploading %s to release %d of %s/%s", dependencyMatrixFileName, releaseInfo.ID, gitInfo.Organisation, gitInfo.Name)
   475  			}
   476  			log.Logger().Infof("Uploaded %s to release asset %s", dependencyMatrixFileName, releaseAsset.BrowserDownloadURL)
   477  		}
   478  		if len(release.Spec.DependencyUpdates) > 0 {
   479  			// Now, let's attach any dependency updates that were done as part of this release
   480  			file, err := ioutil.TempFile("", "")
   481  			if err != nil {
   482  				return errors.Wrapf(err, "creating temp file to write dependency updates to")
   483  			}
   484  			data := dependencymatrix.DependencyUpdates{
   485  				Updates: release.Spec.DependencyUpdates,
   486  			}
   487  			bytes, err := yaml.Marshal(data)
   488  			if err != nil {
   489  				return errors.Wrapf(err, "marshaling %+v to yaml", data)
   490  			}
   491  			err = ioutil.WriteFile(file.Name(), bytes, 0600)
   492  			if err != nil {
   493  				return errors.Wrapf(err, "writing dependency update yaml to %s", file.Name())
   494  			}
   495  			releaseAsset, err := gitProvider.UploadReleaseAsset(gitInfo.Organisation, gitInfo.Name, releaseInfo.ID, dependencymatrix.DependencyUpdatesAssetName, file)
   496  			if err != nil {
   497  				return errors.Wrapf(err, "uploading %s to release %d of %s/%s", dependencymatrix.DependencyUpdatesAssetName, releaseInfo.ID, gitInfo.Organisation, gitInfo.Name)
   498  			}
   499  			log.Logger().Infof("Uploaded %s to release asset %s", dependencymatrix.DependencyUpdatesAssetName, releaseAsset.BrowserDownloadURL)
   500  		}
   501  
   502  	} else if o.OutputMarkdownFile != "" {
   503  		err := ioutil.WriteFile(o.OutputMarkdownFile, []byte(markdown), util.DefaultWritePermissions)
   504  		if err != nil {
   505  			return err
   506  		}
   507  		log.Logger().Infof("\nGenerated Changelog: %s", util.ColorInfo(o.OutputMarkdownFile))
   508  	} else {
   509  		log.Logger().Infof("\nGenerated Changelog:")
   510  		log.Logger().Infof("%s\n", markdown)
   511  	}
   512  
   513  	o.State.Release = release
   514  	// now lets marshal the release YAML
   515  	data, err := yaml.Marshal(release)
   516  
   517  	if err != nil {
   518  		return err
   519  	}
   520  	if data == nil {
   521  		return fmt.Errorf("Could not marshal release to yaml")
   522  	}
   523  	releaseFile := filepath.Join(templatesDir, o.ReleaseYamlFile)
   524  	crdFile := filepath.Join(templatesDir, o.CrdYamlFile)
   525  	if o.GenerateReleaseYaml {
   526  		err = ioutil.WriteFile(releaseFile, data, util.DefaultWritePermissions)
   527  		if err != nil {
   528  			return fmt.Errorf("Failed to save Release YAML file %s: %s", releaseFile, err)
   529  		}
   530  		log.Logger().Infof("generated: %s", util.ColorInfo(releaseFile))
   531  	}
   532  	cleanVersion := strings.TrimPrefix(version, "v")
   533  	release.Spec.Version = cleanVersion
   534  	if o.GenerateCRD {
   535  		exists, err := util.FileExists(crdFile)
   536  		if err != nil {
   537  			return fmt.Errorf("Failed to check for CRD YAML file %s: %s", crdFile, err)
   538  		}
   539  		if o.OverwriteCRD || !exists {
   540  			err = ioutil.WriteFile(crdFile, []byte(ReleaseCrdYaml), util.DefaultWritePermissions)
   541  			if err != nil {
   542  				return fmt.Errorf("Failed to save Release CRD YAML file %s: %s", crdFile, err)
   543  			}
   544  			log.Logger().Infof("generated: %s", util.ColorInfo(crdFile))
   545  		}
   546  	}
   547  	appName := ""
   548  	if gitInfo != nil {
   549  		appName = gitInfo.Name
   550  	}
   551  	if appName == "" {
   552  		appName = release.Spec.Name
   553  	}
   554  	if appName == "" {
   555  		appName = release.Spec.GitRepository
   556  	}
   557  	if !o.NoReleaseInDev {
   558  		devRelease := *release
   559  		devRelease.ResourceVersion = ""
   560  		devRelease.Namespace = devNs
   561  		devRelease.Name = naming.ToValidName(appName + "-" + cleanVersion)
   562  		devRelease.Spec.Name = appName
   563  		_, err := kube.GetOrCreateRelease(jxClient, devNs, &devRelease)
   564  		if err != nil {
   565  			log.Logger().Warnf("%s", err)
   566  		} else {
   567  			log.Logger().Infof("Created Release %s resource in namespace %s", devRelease.Name, devNs)
   568  		}
   569  	}
   570  	releaseNotesURL := release.Spec.ReleaseNotesURL
   571  	pipeline := ""
   572  	build := o.Build
   573  	pipeline, build = o.GetPipelineName(gitInfo, pipeline, build, appName)
   574  	if pipeline != "" && build != "" {
   575  		name := naming.ToValidName(pipeline + "-" + build)
   576  		// lets see if we can update the pipeline
   577  		activities := jxClient.JenkinsV1().PipelineActivities(devNs)
   578  		lastCommitSha := ""
   579  		lastCommitMessage := ""
   580  		lastCommitURL := ""
   581  		commits := release.Spec.Commits
   582  		if len(commits) > 0 {
   583  			lastCommit := commits[len(commits)-1]
   584  			lastCommitSha = lastCommit.SHA
   585  			lastCommitMessage = lastCommit.Message
   586  			lastCommitURL = lastCommit.URL
   587  		}
   588  		log.Logger().Infof("Updating PipelineActivity %s with version %s", name, cleanVersion)
   589  
   590  		key := &kube.PromoteStepActivityKey{
   591  			PipelineActivityKey: kube.PipelineActivityKey{
   592  				Name:              name,
   593  				Pipeline:          pipeline,
   594  				Build:             build,
   595  				ReleaseNotesURL:   releaseNotesURL,
   596  				LastCommitSHA:     lastCommitSha,
   597  				LastCommitMessage: lastCommitMessage,
   598  				LastCommitURL:     lastCommitURL,
   599  				Version:           cleanVersion,
   600  				GitInfo:           gitInfo,
   601  			},
   602  		}
   603  		_, currentNamespace, err := o.KubeClientAndNamespace()
   604  		if err != nil {
   605  			return errors.Wrap(err, "getting current namespace")
   606  		}
   607  		a, created, err := key.GetOrCreate(jxClient, currentNamespace)
   608  		if err == nil && a != nil && !created {
   609  			_, err = activities.PatchUpdate(a)
   610  			if err != nil {
   611  				log.Logger().Warnf("Failed to update PipelineActivities %s: %s", name, err)
   612  			} else {
   613  				log.Logger().Infof("Updated PipelineActivities %s with release notes URL: %s", util.ColorInfo(name), util.ColorInfo(releaseNotesURL))
   614  			}
   615  		}
   616  	} else {
   617  		log.Logger().Infof("No pipeline and build number available on $JOB_NAME and $BUILD_NUMBER so cannot update PipelineActivities with the ReleaseNotesURL")
   618  	}
   619  	return nil
   620  }
   621  
   622  func (o *StepChangelogOptions) addCommit(spec *v1.ReleaseSpec, commit *object.Commit, resolver *users.GitUserResolver) {
   623  	// TODO
   624  	url := ""
   625  	branch := "master"
   626  
   627  	var author, committer *v1.User
   628  	var err error
   629  	sha := commit.Hash.String()
   630  	if commit.Author.Email != "" && commit.Author.Name != "" {
   631  		author, err = resolver.GitSignatureAsUser(&commit.Author)
   632  		if err != nil {
   633  			log.Logger().Warnf("failed to enrich commit with issues, error getting git signature for git author %s: %v", commit.Author, err)
   634  		}
   635  	}
   636  	if commit.Committer.Email != "" && commit.Committer.Name != "" {
   637  		committer, err = resolver.GitSignatureAsUser(&commit.Committer)
   638  		if err != nil {
   639  			log.Logger().Warnf("failed to enrich commit with issues, error getting git signature for git committer %s: %v", commit.Committer, err)
   640  		}
   641  	}
   642  	var authorDetails, committerDetails v1.UserDetails
   643  	if author != nil {
   644  		authorDetails = author.Spec
   645  	}
   646  	if committer != nil {
   647  		committerDetails = committer.Spec
   648  	}
   649  	dependencyUpdate, upstreamUpdates, err := o.ParseDependencyUpdateMessage(commit.Message, spec.GitCloneURL)
   650  	if err != nil {
   651  		log.Logger().Infof("Parsing %s for dependency updates", commit.Message)
   652  	}
   653  	if dependencyUpdate != nil {
   654  		if spec.DependencyUpdates == nil {
   655  			spec.DependencyUpdates = make([]v1.DependencyUpdate, 0)
   656  		}
   657  		spec.DependencyUpdates = append(spec.DependencyUpdates, *dependencyUpdate)
   658  	}
   659  	if upstreamUpdates != nil {
   660  		for _, u := range upstreamUpdates.Updates {
   661  			spec.DependencyUpdates = append(spec.DependencyUpdates, u)
   662  		}
   663  	}
   664  	commitSummary := v1.CommitSummary{
   665  		Message:   commit.Message,
   666  		URL:       url,
   667  		SHA:       sha,
   668  		Author:    &authorDetails,
   669  		Branch:    branch,
   670  		Committer: &committerDetails,
   671  	}
   672  
   673  	err = o.addIssuesAndPullRequests(spec, &commitSummary, commit)
   674  	if err != nil {
   675  		log.Logger().Warnf("Failed to enrich commit %s with issues: %s", sha, err)
   676  	}
   677  	spec.Commits = append(spec.Commits, commitSummary)
   678  
   679  }
   680  
   681  func (o *StepChangelogOptions) addIssuesAndPullRequests(spec *v1.ReleaseSpec, commit *v1.CommitSummary, rawCommit *object.Commit) error {
   682  	tracker := o.State.Tracker
   683  
   684  	gitProvider := o.State.GitProvider
   685  	if gitProvider == nil || !gitProvider.HasIssues() {
   686  		return nil
   687  	}
   688  	regex := GitHubIssueRegex
   689  	issueKind := issues.GetIssueProvider(tracker)
   690  	if !o.State.LoggedIssueKind {
   691  		o.State.LoggedIssueKind = true
   692  		log.Logger().Infof("Finding issues in commit messages using %s format", issueKind)
   693  	}
   694  	if issueKind == issues.Jira {
   695  		regex = JIRAIssueRegex
   696  	}
   697  	message := fullCommitMessageText(rawCommit)
   698  
   699  	matches := regex.FindAllStringSubmatch(message, -1)
   700  	jxClient, ns, err := o.JXClientAndDevNamespace()
   701  	if err != nil {
   702  		return err
   703  	}
   704  	resolver := users.GitUserResolver{
   705  		JXClient:    jxClient,
   706  		Namespace:   ns,
   707  		GitProvider: gitProvider,
   708  	}
   709  	for _, match := range matches {
   710  		for _, result := range match {
   711  			result = strings.TrimPrefix(result, "#")
   712  			if _, ok := o.State.FoundIssueNames[result]; !ok {
   713  				o.State.FoundIssueNames[result] = true
   714  				issue, err := tracker.GetIssue(result)
   715  				if err != nil {
   716  					log.Logger().Warnf("Failed to lookup issue %s in issue tracker %s due to %s", result, tracker.HomeURL(), err)
   717  					continue
   718  				}
   719  				if issue == nil {
   720  					log.Logger().Warnf("Failed to find issue %s for repository %s", result, tracker.HomeURL())
   721  					continue
   722  				}
   723  
   724  				var user v1.UserDetails
   725  				if issue.User == nil {
   726  					log.Logger().Warnf("Failed to find user for issue %s repository %s", result, tracker.HomeURL())
   727  				} else {
   728  					u, err := resolver.Resolve(issue.User)
   729  					if err != nil {
   730  						log.Logger().Warnf("Failed to resolve user %v for issue %s repository %s", issue.User, result, tracker.HomeURL())
   731  					} else if u != nil {
   732  						user = u.Spec
   733  					}
   734  				}
   735  
   736  				var closedBy v1.UserDetails
   737  				if issue.ClosedBy == nil {
   738  					log.Logger().Warnf("Failed to find closedBy user for issue %s repository %s", result, tracker.HomeURL())
   739  				} else {
   740  					u, err := resolver.Resolve(issue.User)
   741  					if err != nil {
   742  						log.Logger().Warnf("Failed to resolve closedBy user %v for issue %s repository %s", issue.User, result, tracker.HomeURL())
   743  					} else if u != nil {
   744  						closedBy = u.Spec
   745  					}
   746  				}
   747  
   748  				var assignees []v1.UserDetails
   749  				if issue.Assignees == nil {
   750  					log.Logger().Warnf("Failed to find assignees for issue %s repository %s", result, tracker.HomeURL())
   751  				} else {
   752  					u, err := resolver.GitUserSliceAsUserDetailsSlice(issue.Assignees)
   753  					if err != nil {
   754  						log.Logger().Warnf("Failed to resolve Assignees %v for issue %s repository %s", issue.Assignees, result, tracker.HomeURL())
   755  					}
   756  					assignees = u
   757  				}
   758  
   759  				labels := toV1Labels(issue.Labels)
   760  				commit.IssueIDs = append(commit.IssueIDs, result)
   761  				issueSummary := v1.IssueSummary{
   762  					ID:                result,
   763  					URL:               issue.URL,
   764  					Title:             issue.Title,
   765  					Body:              issue.Body,
   766  					User:              &user,
   767  					CreationTimestamp: kube.ToMetaTime(issue.CreatedAt),
   768  					ClosedBy:          &closedBy,
   769  					Assignees:         assignees,
   770  					Labels:            labels,
   771  				}
   772  				state := issue.State
   773  				if state != nil {
   774  					issueSummary.State = *state
   775  				}
   776  				if issue.IsPullRequest {
   777  					spec.PullRequests = append(spec.PullRequests, issueSummary)
   778  				} else {
   779  					spec.Issues = append(spec.Issues, issueSummary)
   780  				}
   781  			}
   782  		}
   783  	}
   784  	return nil
   785  }
   786  
   787  // toV1Labels converts git labels to IssueLabel
   788  func toV1Labels(labels []gits.GitLabel) []v1.IssueLabel {
   789  	answer := []v1.IssueLabel{}
   790  	for _, label := range labels {
   791  		answer = append(answer, v1.IssueLabel{
   792  			URL:   label.URL,
   793  			Name:  label.Name,
   794  			Color: label.Color,
   795  		})
   796  	}
   797  	return answer
   798  }
   799  
   800  // fullCommitMessageText returns the commit message
   801  func fullCommitMessageText(commit *object.Commit) string {
   802  	answer := commit.Message
   803  	fn := func(parent *object.Commit) error {
   804  		text := parent.Message
   805  		if text != "" {
   806  			sep := "\n"
   807  			if strings.HasSuffix(answer, "\n") {
   808  				sep = ""
   809  			}
   810  			answer += sep + text
   811  		}
   812  		return nil
   813  	}
   814  	fn(commit) //nolint:errcheck
   815  	return answer
   816  
   817  }
   818  
   819  func (o *StepChangelogOptions) getTemplateResult(releaseSpec *v1.ReleaseSpec, templateName string, templateText string, templateFile string) (string, error) {
   820  	if templateText == "" {
   821  		if templateFile == "" {
   822  			return "", nil
   823  		}
   824  		data, err := ioutil.ReadFile(templateFile)
   825  		if err != nil {
   826  			return "", err
   827  		}
   828  		templateText = string(data)
   829  	}
   830  	if templateText == "" {
   831  		return "", nil
   832  	}
   833  	tmpl, err := template.New(templateName).Parse(templateText)
   834  	if err != nil {
   835  		return "", err
   836  	}
   837  	var buffer bytes.Buffer
   838  	writer := bufio.NewWriter(&buffer)
   839  	err = tmpl.Execute(writer, releaseSpec)
   840  	writer.Flush()
   841  	return buffer.String(), err
   842  }
   843  
   844  //CollapseDependencyUpdates takes a raw set of dependencyUpdates, removes duplicates and collapses multiple updates to
   845  // the same org/repo:components into a sungle update
   846  func CollapseDependencyUpdates(dependencyUpdates []v1.DependencyUpdate) []v1.DependencyUpdate {
   847  	// Sort the dependency updates. This makes the outputs more readable, and it also allows us to more easily do duplicate removal and collapsing
   848  
   849  	sort.Slice(dependencyUpdates, func(i, j int) bool {
   850  		if dependencyUpdates[i].Owner == dependencyUpdates[j].Owner {
   851  			if dependencyUpdates[i].Repo == dependencyUpdates[j].Repo {
   852  				if dependencyUpdates[i].Component == dependencyUpdates[j].Component {
   853  					if dependencyUpdates[i].FromVersion == dependencyUpdates[j].FromVersion {
   854  						return dependencyUpdates[i].ToVersion < dependencyUpdates[j].ToVersion
   855  					}
   856  					return dependencyUpdates[i].FromVersion < dependencyUpdates[j].FromVersion
   857  				}
   858  				return dependencyUpdates[i].Component < dependencyUpdates[j].Component
   859  			}
   860  			return dependencyUpdates[i].Repo < dependencyUpdates[j].Repo
   861  		}
   862  		return dependencyUpdates[i].Owner < dependencyUpdates[j].Owner
   863  	})
   864  
   865  	// Collapse  entries
   866  	collapsed := make([]v1.DependencyUpdate, 0)
   867  
   868  	if len(dependencyUpdates) > 0 {
   869  		start := 0
   870  		for i := 1; i <= len(dependencyUpdates); i++ {
   871  			if i == len(dependencyUpdates) || dependencyUpdates[i-1].Owner != dependencyUpdates[i].Owner || dependencyUpdates[i-1].Repo != dependencyUpdates[i].Repo || dependencyUpdates[i-1].Component != dependencyUpdates[i].Component {
   872  				end := i - 1
   873  				collapsed = append(collapsed, v1.DependencyUpdate{
   874  					DependencyUpdateDetails: v1.DependencyUpdateDetails{
   875  						Owner:              dependencyUpdates[start].Owner,
   876  						Repo:               dependencyUpdates[start].Repo,
   877  						Component:          dependencyUpdates[start].Component,
   878  						URL:                dependencyUpdates[start].URL,
   879  						Host:               dependencyUpdates[start].Host,
   880  						FromVersion:        dependencyUpdates[start].FromVersion,
   881  						FromReleaseHTMLURL: dependencyUpdates[start].FromReleaseHTMLURL,
   882  						FromReleaseName:    dependencyUpdates[start].FromReleaseName,
   883  						ToVersion:          dependencyUpdates[end].ToVersion,
   884  						ToReleaseName:      dependencyUpdates[end].ToReleaseName,
   885  						ToReleaseHTMLURL:   dependencyUpdates[end].ToReleaseHTMLURL,
   886  					},
   887  				})
   888  				start = i
   889  			}
   890  		}
   891  	}
   892  	return collapsed
   893  }