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