github.com/olli-ai/jx/v2@v2.0.400-0.20210921045218-14731b4dd448/pkg/cmd/step/step_next_version.go (about)

     1  package step
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/xml"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"path/filepath"
     9  	"regexp"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/olli-ai/jx/v2/pkg/cmd/opts/step"
    14  
    15  	"github.com/olli-ai/jx/v2/pkg/semrel"
    16  
    17  	"github.com/pkg/errors"
    18  
    19  	"github.com/olli-ai/jx/v2/pkg/cmd/helper"
    20  
    21  	"github.com/olli-ai/jx/v2/pkg/util"
    22  
    23  	"encoding/json"
    24  
    25  	"github.com/blang/semver"
    26  	version "github.com/hashicorp/go-version"
    27  	"github.com/jenkins-x/jx-logging/pkg/log"
    28  	"github.com/olli-ai/jx/v2/pkg/cmd/opts"
    29  	"github.com/olli-ai/jx/v2/pkg/cmd/templates"
    30  	"github.com/spf13/cobra"
    31  )
    32  
    33  const (
    34  	packagejson = "package.json"
    35  	chartyaml   = "Chart.yaml"
    36  	pomxml      = "pom.xml"
    37  	makefile    = "Makefile"
    38  )
    39  
    40  // StepNextVersionOptions contains the command line flags
    41  type StepNextVersionOptions struct {
    42  	Filename        string
    43  	Dir             string
    44  	ChartsDir       string
    45  	Tag             bool
    46  	UseGitTagOnly   bool
    47  	NewVersion      string
    48  	SemanticRelease bool
    49  	step.StepOptions
    50  }
    51  
    52  type Project struct {
    53  	Version string `xml:"version"`
    54  }
    55  
    56  type PackageJSON struct {
    57  	Version string `json:"version"`
    58  }
    59  
    60  var (
    61  	StepNextVersionLong = templates.LongDesc(`
    62  		This pipeline step command works out a semantic version, writes a file ./VERSION and optionally updates a file
    63  `)
    64  
    65  	StepNextVersionExample = templates.Examples(`
    66  		jx step next-version
    67  		jx step next-version --filename package.json
    68  		jx step next-version --filename package.json --tag
    69  		jx step next-version --filename package.json --tag --version 1.2.3
    70  
    71  		# lets use git to create a new version from a tag and tag git
    72          jx step next-version --use-git-tag-only --tag
    73                
    74  `)
    75  )
    76  
    77  func NewCmdStepNextVersion(commonOpts *opts.CommonOptions) *cobra.Command {
    78  	options := StepNextVersionOptions{
    79  		StepOptions: step.StepOptions{
    80  			CommonOptions: commonOpts,
    81  		},
    82  	}
    83  	cmd := &cobra.Command{
    84  		Use:     "next-version",
    85  		Short:   "Writes next semantic version",
    86  		Long:    StepNextVersionLong,
    87  		Example: StepNextVersionExample,
    88  		Run: func(cmd *cobra.Command, args []string) {
    89  			options.Cmd = cmd
    90  			options.Args = args
    91  			err := options.Run()
    92  			helper.CheckErr(err)
    93  		},
    94  	}
    95  	cmd.Flags().StringVarP(&options.Filename, "filename", "f", "", "Filename that contains version property to update, e.g. package.json")
    96  	cmd.Flags().StringVarP(&options.NewVersion, "version", "", "", "optional version to use rather than generating a new one")
    97  	cmd.Flags().StringVarP(&options.Dir, "dir", "d", "", "the directory to look for files that contain a pom.xml or Makefile with the project version to bump")
    98  	cmd.Flags().StringVarP(&options.ChartsDir, "charts-dir", "", "", "the directory of the chart to update the version (in conjunction with --tag)")
    99  	cmd.Flags().BoolVarP(&options.Tag, "tag", "t", false, "tag and push new version")
   100  	cmd.Flags().BoolVarP(&options.UseGitTagOnly, "use-git-tag-only", "", false, "only use a git tag so work out new semantic version, else specify filename [pom.xml,package.json,Makefile,Chart.yaml]")
   101  	cmd.Flags().BoolVarP(&options.SemanticRelease, "semantic-release", "", false, "use conventional commits to determine next version. Ignores the --use-git-tag-only and --version options See https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#-git-commit-guidelines")
   102  	return cmd
   103  }
   104  
   105  func (o *StepNextVersionOptions) Run() error {
   106  
   107  	var err error
   108  	if o.SemanticRelease {
   109  		err := o.Git().FetchTags(o.Dir)
   110  		if err != nil {
   111  			return errors.WithStack(err)
   112  		}
   113  		rev, tag, err := o.Git().GetCommitPointedToByLatestTag(o.Dir)
   114  		if err != nil {
   115  			return errors.WithStack(err)
   116  		}
   117  		log.Logger().Infof("latest tag %s and rev %s", util.ColorInfo(tag), util.ColorInfo(rev))
   118  		cur, err := o.Git().RevParse(o.Dir, "HEAD")
   119  		if err != nil {
   120  			return errors.WithStack(err)
   121  		}
   122  		newVersion, err := semrel.GetNewVersion(o.Dir, cur, o.Git(), tag, rev)
   123  		if err != nil {
   124  			return errors.Wrapf(err, "getting new semantic release version for %s", tag)
   125  		}
   126  		o.NewVersion = newVersion.String()
   127  	} else if o.NewVersion == "" {
   128  		o.NewVersion, err = o.getNewVersionFromTagAndFile()
   129  		if err != nil {
   130  			return err
   131  		}
   132  	}
   133  
   134  	// in declarative pipelines we sometimes need to write the version to a file rather than pass state
   135  	err = ioutil.WriteFile("VERSION", []byte(o.NewVersion), 0600)
   136  	if err != nil {
   137  		return err
   138  	}
   139  
   140  	log.Logger().Infof("created new version: %s and written to file: ./VERSION", util.ColorInfo(o.NewVersion))
   141  
   142  	// if filename flag set and recognised then update version, commit
   143  	if o.Filename != "" {
   144  		err = o.SetVersion()
   145  		if err != nil {
   146  			return err
   147  		}
   148  	}
   149  
   150  	// if tag set then tag it
   151  	if o.Tag {
   152  		tagOptions := StepTagOptions{
   153  			Flags: StepTagFlags{
   154  				Version:   o.NewVersion,
   155  				ChartsDir: o.ChartsDir,
   156  			},
   157  			StepOptions: o.StepOptions,
   158  		}
   159  		err = tagOptions.Run()
   160  		if err != nil {
   161  			return err
   162  		}
   163  	}
   164  	return nil
   165  }
   166  
   167  // GetVersion gets the version from a source file
   168  func (o *StepNextVersionOptions) GetVersion() (string, error) {
   169  	if o.UseGitTagOnly {
   170  		return "", nil
   171  	}
   172  	if o.Filename == "" {
   173  		// try and work out
   174  		return "", fmt.Errorf("no filename flag set to work out next semantic version.  choose pom.xml, Chart.yaml, package.json, Makefile or set the flag use-git-tag-only")
   175  	}
   176  
   177  	switch o.Filename {
   178  	case chartyaml:
   179  		chartFile := filepath.Join(o.Dir, chartyaml)
   180  		chart, err := ioutil.ReadFile(chartFile)
   181  		if err != nil {
   182  			return "", err
   183  		}
   184  
   185  		log.Logger().Debugf("Found Chart.yaml")
   186  		scanner := bufio.NewScanner(strings.NewReader(string(chart)))
   187  		for scanner.Scan() {
   188  			if strings.Contains(scanner.Text(), "version") {
   189  				parts := strings.Split(scanner.Text(), ":")
   190  
   191  				v := strings.TrimSpace(parts[1])
   192  				if v != "" {
   193  					log.Logger().Debugf("existing Chart version %v", v)
   194  					return v, nil
   195  				}
   196  			}
   197  		}
   198  	case packagejson:
   199  		packageFile := filepath.Join(o.Dir, packagejson)
   200  		p, err := ioutil.ReadFile(packageFile)
   201  		if err != nil {
   202  			return "", err
   203  		}
   204  
   205  		log.Logger().Debugf("found %s", packagejson)
   206  
   207  		var jsPackage PackageJSON
   208  		err = json.Unmarshal(p, &jsPackage)
   209  		if err != nil {
   210  			return "", err
   211  		}
   212  
   213  		if jsPackage.Version != "" {
   214  			log.Logger().Debugf("existing version %s", jsPackage.Version)
   215  			return jsPackage.Version, nil
   216  		}
   217  
   218  	case pomxml:
   219  		pomFile := filepath.Join(o.Dir, pomxml)
   220  		p, err := ioutil.ReadFile(pomFile)
   221  		if err != nil {
   222  			return "", err
   223  		}
   224  
   225  		log.Logger().Debugf("found pom.xml")
   226  		var project Project
   227  		err = xml.Unmarshal(p, &project)
   228  		if err != nil {
   229  			return "", err
   230  		}
   231  		if project.Version != "" {
   232  			log.Logger().Debugf("existing version %s", project.Version)
   233  			return project.Version, nil
   234  		}
   235  
   236  	case makefile:
   237  		makefile := filepath.Join(o.Dir, makefile)
   238  		m, err := ioutil.ReadFile(makefile)
   239  		if err != nil {
   240  			return "", err
   241  		}
   242  
   243  		log.Logger().Debugf("found Makefile")
   244  		scanner := bufio.NewScanner(strings.NewReader(string(m)))
   245  		for scanner.Scan() {
   246  			if strings.HasPrefix(scanner.Text(), "VERSION") || strings.HasPrefix(scanner.Text(), "VERSION ") || strings.HasPrefix(scanner.Text(), "VERSION:") || strings.HasPrefix(scanner.Text(), "VERSION=") {
   247  				parts := strings.Split(scanner.Text(), "=")
   248  
   249  				v := strings.TrimSpace(parts[1])
   250  				if v != "" {
   251  					log.Logger().Debugf("existing Makefile version %s", v)
   252  					return v, nil
   253  				}
   254  			}
   255  		}
   256  	default:
   257  		return "", fmt.Errorf("no recognised file to obtain current version from")
   258  	}
   259  
   260  	return "", fmt.Errorf("cannot find version for file %s\n", o.Filename)
   261  }
   262  
   263  func (o *StepNextVersionOptions) getLatestTag() (string, error) {
   264  	// if repo isn't provided by flags fall back to using current repo if run from a git project
   265  	var versionsRaw []string
   266  
   267  	err := o.Git().FetchTags("")
   268  	if err != nil {
   269  		return "", fmt.Errorf("error fetching tags: %v", err)
   270  	}
   271  	tags, err := o.Git().Tags("")
   272  	if err != nil {
   273  		return "", err
   274  	}
   275  	if len(tags) == 0 {
   276  		// if no current flags exist then lets start at 0.0.0
   277  		return "0.0.0", fmt.Errorf("no existing tags found")
   278  	}
   279  
   280  	// build an array of all the tags
   281  	versionsRaw = make([]string, len(tags))
   282  	for i, tag := range tags {
   283  		log.Logger().Debugf("found tag %s", tag)
   284  		tag = strings.TrimPrefix(tag, "v")
   285  		if tag != "" {
   286  			versionsRaw[i] = tag
   287  		}
   288  	}
   289  
   290  	// turn the array into a new collection of versions that we can sort
   291  	var versions []*version.Version
   292  	for _, raw := range versionsRaw {
   293  		v, _ := version.NewVersion(raw)
   294  		if v != nil {
   295  			versions = append(versions, v)
   296  		}
   297  	}
   298  
   299  	if len(versions) == 0 {
   300  		// if no current flags exist then lets start at 0.0.0
   301  		return "0.0.0", fmt.Errorf("no existing tags found")
   302  	}
   303  
   304  	// return the latest tag
   305  	col := version.Collection(versions)
   306  	log.Logger().Debugf("version collection %v", col)
   307  
   308  	sort.Sort(col)
   309  	latest := len(versions)
   310  	if versions[latest-1] == nil {
   311  		return "0.0.0", fmt.Errorf("no existing tags found")
   312  	}
   313  	return versions[latest-1].String(), nil
   314  }
   315  
   316  func (o *StepNextVersionOptions) getNewVersionFromTagAndFile() (string, error) {
   317  
   318  	// get the latest github tag
   319  	tag, err := o.getLatestTag()
   320  	if err != nil && tag == "" {
   321  		return "", err
   322  	}
   323  
   324  	sv, err := semver.Parse(tag)
   325  	if err != nil {
   326  		return "", err
   327  	}
   328  
   329  	majorVersion := sv.Major
   330  	minorVersion := sv.Minor
   331  	patchVersion := sv.Patch + 1
   332  
   333  	// check if major or minor version has been changed
   334  	baseVersion, err := o.GetVersion()
   335  	if err != nil {
   336  		return "", err
   337  	}
   338  
   339  	// first use go-version to turn into a proper version, this handles 1.0-SNAPSHOT which semver doesn't
   340  	baseMajorVersion := uint64(0)
   341  	baseMinorVersion := uint64(0)
   342  	basePatchVersion := uint64(0)
   343  
   344  	if baseVersion != "" {
   345  		tmpVersion, err := version.NewVersion(baseVersion)
   346  		if err != nil {
   347  			return "", err
   348  		}
   349  		bsv, err := semver.New(tmpVersion.String())
   350  		if err != nil {
   351  			return "", err
   352  		}
   353  		baseMajorVersion = bsv.Major
   354  		baseMinorVersion = bsv.Minor
   355  		basePatchVersion = bsv.Patch
   356  	}
   357  
   358  	if baseMajorVersion > majorVersion ||
   359  		(baseMajorVersion == majorVersion &&
   360  			(baseMinorVersion > minorVersion) || (baseMinorVersion == minorVersion && basePatchVersion > patchVersion)) {
   361  		majorVersion = baseMajorVersion
   362  		minorVersion = baseMinorVersion
   363  		patchVersion = basePatchVersion
   364  	}
   365  
   366  	return fmt.Sprintf("%d.%d.%d", majorVersion, minorVersion, patchVersion), nil
   367  }
   368  
   369  // SetVersion Sets the version...
   370  func (o *StepNextVersionOptions) SetVersion() error {
   371  	var err error
   372  	var matchField string
   373  	var regex *regexp.Regexp
   374  	filename := filepath.Join(o.Dir, o.Filename)
   375  	b, err := ioutil.ReadFile(filename)
   376  	if err != nil {
   377  		return err
   378  	}
   379  	switch o.Filename {
   380  	case packagejson:
   381  		regex = regexp.MustCompile(`[0-9][0-9]{0,2}.[0-9][0-9]{0,2}(.[0-9][0-9]{0,2})?(.[0-9][0-9]{0,2})?(-development)?`)
   382  		matchField = "\"version\": \""
   383  
   384  	case chartyaml:
   385  		regex = regexp.MustCompile(`[0-9][0-9]{0,2}.[0-9][0-9]{0,2}(.[0-9][0-9]{0,2})?(.[0-9][0-9]{0,2})?(-.*)?`)
   386  		matchField = "version: "
   387  
   388  	default:
   389  		return fmt.Errorf("unrecognised filename %s, supported files are %s %s", o.Filename, packagejson, chartyaml)
   390  	}
   391  
   392  	lines := strings.Split(string(b), "\n")
   393  
   394  	replaced := false
   395  	for i, line := range lines {
   396  		if !replaced && strings.Contains(line, matchField) {
   397  			lines[i] = regex.ReplaceAllString(line, o.NewVersion)
   398  			replaced = true
   399  		} else {
   400  			lines[i] = line
   401  		}
   402  	}
   403  	output := strings.Join(lines, "\n")
   404  	err = ioutil.WriteFile(filename, []byte(output), 0600)
   405  	if err != nil {
   406  		return err
   407  	}
   408  
   409  	if o.Tag {
   410  		// lets not commit to git as we do that in the tag step
   411  		return nil
   412  	}
   413  	err = o.Git().Add(o.Dir, "*")
   414  	if err != nil {
   415  		return err
   416  	}
   417  
   418  	err = o.Git().CommitDir(o.Dir, fmt.Sprintf("release %s", o.NewVersion))
   419  	if err != nil {
   420  		return err
   421  	}
   422  	return nil
   423  }
   424  
   425  // returns a string array containing the git owner and repo name for a given URL