scripts/vbumper/main.go (146 lines of code) (raw):
// Copyright (c) 2015 Uber Technologies, Inc.
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
// vbumper helps bump version numbers in the repository and in the CHANGELOG.
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"html/template"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"time"
)
var (
_changelogFile = flag.String("changelog-file", "CHANGELOG.md", "Filename of the changelog file")
_versionFile = flag.String("version-file", "version.go", "Filename of where the version information is stored")
_version = flag.String("version", "", "Version to mention in changelog and version.go")
_versionDate = flag.String("version-date", "", "Date to use in the changelog, by default the current date")
_skipChangelog = flag.Bool("skip-changelog", false, "Skip updating the changelog")
)
func main() {
*_versionDate = time.Now().Format("2006-01-02")
flag.Parse()
if *_version == "" {
log.Fatal("Please specify the version to release using --version")
}
*_version = strings.TrimPrefix(*_version, "v")
prevVersion, err := updateChangelog()
if err != nil {
log.Fatal("failed to update changelog", err)
}
if err := updateVersion(prevVersion); err != nil {
log.Fatal("failed to update version", err)
}
}
func updateVersion(prevVersion string) error {
versionBytes, err := ioutil.ReadFile(*_versionFile)
if err != nil {
return err
}
newContents := insertNewVersion(string(versionBytes), prevVersion, *_version)
return ioutil.WriteFile(*_versionFile, []byte(newContents), 0666)
}
func insertNewVersion(contents, prevVersion, newVersion string) string {
// Find the version string in the file
versionStart := strings.Index(contents, prevVersion)
versionLine := contents[versionStart:]
versionEnd := strings.Index(versionLine, `"`) + versionStart
return contents[:versionStart] + newVersion + contents[versionEnd:]
}
func updateChangelog() (oldVersion string, _ error) {
changelogBytes, err := ioutil.ReadFile(*_changelogFile)
if err != nil {
return "", err
}
newLog, oldVersion, err := insertNewChangelog(string(changelogBytes))
if err != nil {
return "", err
}
newLog, err = insertChangesLink(newLog, oldVersion, *_version)
if err != nil {
return "", err
}
if *_skipChangelog {
return oldVersion, nil
}
return oldVersion, ioutil.WriteFile(*_changelogFile, []byte(newLog), 0666)
}
func insertNewChangelog(contents string) (string, string, error) {
prevVersionHeader := strings.Index(contents, "\n## [")
if prevVersionHeader < 0 {
return "", "", errors.New("failed to find version header in changelog")
}
// Skip the newline
prevVersionHeader++
versionLine := contents[prevVersionHeader:]
prevVersionEnd := strings.Index(versionLine, "]")
prevVersion := strings.TrimSpace(versionLine[4:prevVersionEnd])
// The version tag has a "v" prefix.
newChanges, err := getNewChangelog("v" + prevVersion)
if err != nil {
return "", "", err
}
newContents := contents[:prevVersionHeader] + newChanges + contents[prevVersionHeader:]
return newContents, prevVersion, nil
}
func getNewChangelog(prevVersion string) (string, error) {
changes, err := getChanges(prevVersion)
if err != nil {
return "", err
}
if len(changes) == 0 {
changes = []string{"No changes yet"}
}
buf := &bytes.Buffer{}
_changeTmpl.Execute(buf, struct {
Version string
Date string
Changes []string
}{
Version: *_version,
Date: *_versionDate,
Changes: changes,
})
return buf.String(), nil
}
var _changeTmpl = template.Must(template.New("changelog").Parse(
`## [{{ .Version }}] - {{ .Date }}
### Changed
{{ range .Changes }}
* {{ . -}}
{{ end }}
`))
func getChanges(prevVersion string) ([]string, error) {
cmd := exec.Command("git", "log", "--format=%s", "--no-merges", prevVersion+"..HEAD")
cmd.Stderr = os.Stderr
out, err := cmd.Output()
if err != nil {
return nil, err
}
lines := strings.Split(string(out), "\n")
newLines := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
newLines = append(newLines, line)
}
return newLines, nil
}
func insertChangesLink(contents, prevVersion, version string) (string, error) {
linksMarker := strings.Index(contents, "(Version Links)")
if linksMarker == -1 {
return "", errors.New("failed to find marker for version links section")
}
newLine := strings.IndexByte(contents[linksMarker:], '\n')
if newLine < 0 {
return "", errors.New("failed to find newline after version links section")
}
insertAt := linksMarker + newLine + 1
linkBlock := fmt.Sprintf("[%v]: %v\n", version, getChangesLink(prevVersion, version))
newContents := contents[:insertAt] + linkBlock + contents[insertAt:]
return newContents, nil
}
func getChangesLink(prevVersion, curVersion string) string {
// Example link:
// https://github.com/uber/tchannel-go/compare/v1.8.0...v1.8.1
return fmt.Sprintf("https://github.com/uber/tchannel-go/compare/v%v...v%v", prevVersion, curVersion)
}