diff --git a/.gitignore b/.gitignore index d63d50d..e623bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,9 @@ *.dll *.so *.dylib +go-semantic-release +!cmd/go-semantic-release # Test binary, build with `go test -c` *.test @@ -12,3 +14,5 @@ *.out .version .vscode/settings.json +CHANGELOG.md +cover.html diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..51f24bf --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,11 @@ +run: + tests: true + +linters-settings: + golint: + min-confidence: 0 +issues: + exclude-use-default: true +linters: + enable: + - golint \ No newline at end of file diff --git a/.release.yml b/.release.yml new file mode 100644 index 0000000..f505632 --- /dev/null +++ b/.release.yml @@ -0,0 +1,22 @@ +commitFormat: angular +title: "go-semantic-release release" +branch: + master: beta + rc: rc + beta: beta + alpha: alpha + add_git_releases: alpha +changelog: + printAll: false + template: '' + templatePath: '' +release: 'github' +assets: + - name: ./build/go-semantic-release + compress: false + - name: ./build/go-semantic-release.exe + compress: false +github: + repo: "go-semantic-release" + user: "nightapes" + customUrl: "" diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f2cd291 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,40 @@ +dist: xenial + +git: + depth: false + +language: go +go: + - 1.12.x +go_import_path: github.com/nightapes/go-semantic-release +cache: + directories: + - $HOME/.cache/go-build + - $HOME/gopath/pkg/mod + +services: +- docker + +notifications: + email: false + +env: + - GO111MODULE=on + +before_script: + - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.16.0 + +script: + - golangci-lint run ./... + - go test -v ./... + - go build -o build/go-semantic-release-temp ./cmd/go-semantic-release/ + - echo "Building version `./build/go-semantic-release-temp next --loglevel debug --no-cache`" + - go build -o build/go-semantic-release -ldflags "-X main.version=`./build/go-semantic-release-temp next`" ./cmd/go-semantic-release/ + - GOOS=windows GOARCH=386 go build -o build/go-semantic-release.exe -ldflags "-X main.version=`./build/go-semantic-release-temp next`" ./cmd/go-semantic-release/ + +after_success: + - ./build/go-semantic-release-temp release --loglevel debug + +branches: + except: + - /^v\d+\.\d+\.\d+$/ \ No newline at end of file diff --git a/README.md b/README.md index b124392..85aa0c4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,15 @@ # go-semantic-release +## Release Types + +| Type | Git tag | Changelog | Release | Write access git | Api token | +|--- |:---: |:---: |:---: |:---: |:---: | +| `git` | :white_check_mark: | | | :white_check_mark:| | +| `github` | :white_check_mark: | :white_check_mark: | :white_check_mark:| | :white_check_mark: | +| `gitlab` | :white_check_mark: | :white_check_mark: | :white_check_mark:| | :white_check_mark: | + + + ## Build `go build ./cmd/go-semantic-release/` diff --git a/README2.md b/README2.md new file mode 100644 index 0000000..e68c535 --- /dev/null +++ b/README2.md @@ -0,0 +1,2 @@ +go build ./cmd/main.go && ./main.exe version next --path /f/Repro/ambassador/ +go build ./cmd/main.go && ./main.exe --loglevel debug version set v1.1.1 --path /f/Repro/ambassador/ \ No newline at end of file diff --git a/cmd/go-semantic-release/commands/changelog.go b/cmd/go-semantic-release/commands/changelog.go new file mode 100644 index 0000000..625b020 --- /dev/null +++ b/cmd/go-semantic-release/commands/changelog.go @@ -0,0 +1,65 @@ +package commands + +import ( + "github.com/Nightapes/go-semantic-release/pkg/semanticrelease" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func init() { + changelogCmd.Flags().StringP("out", "o", "CHANGELOG.md", "Name of the file") + rootCmd.AddCommand(changelogCmd) +} + +var changelogCmd = &cobra.Command{ + Use: "changelog", + Short: "Generate changelog and save to file", + RunE: func(cmd *cobra.Command, args []string) error { + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + repository, err := cmd.Flags().GetString("repository") + if err != nil { + return err + } + + force, err := cmd.Flags().GetBool("no-cache") + if err != nil { + return err + } + + file, err := cmd.Flags().GetString("out") + if err != nil { + return err + } + + s, err := semanticrelease.New(readConfig(config), repository) + if err != nil { + return err + } + + provider, err := s.GetCIProvider() + if err != nil { + return err + } + + releaseVersion, commits, err := s.GetNextVersion(provider, force) + if err != nil { + return err + } + log.Debugf("Found %d commits till last release", len(commits)) + + generatedChangelog, err := s.GetChangelog(commits, releaseVersion) + if err != nil { + return err + } + + if err = s.WriteChangeLog(generatedChangelog.Content, file); err != nil { + log.Fatal(err) + } + + return nil + }, +} diff --git a/cmd/go-semantic-release/commands/next.go b/cmd/go-semantic-release/commands/next.go new file mode 100644 index 0000000..65ed0bc --- /dev/null +++ b/cmd/go-semantic-release/commands/next.go @@ -0,0 +1,53 @@ +package commands + +import ( + "fmt" + "github.com/Nightapes/go-semantic-release/pkg/semanticrelease" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(nextCmd) +} + +var nextCmd = &cobra.Command{ + Use: "next", + Short: "Get next release version", + RunE: func(cmd *cobra.Command, args []string) error { + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + repository, err := cmd.Flags().GetString("repository") + if err != nil { + return err + } + + force, err := cmd.Flags().GetBool("no-cache") + if err != nil { + return err + } + + s, err := semanticrelease.New(readConfig(config), repository) + if err != nil { + return err + } + + provider, err := s.GetCIProvider() + + if err != nil { + log.Infof("Will not calculate version, set fake version. Could not find CI Provider, if running locally, set env CI=true") + fmt.Println("0.0.0-fake.0") + return nil + } + + releaseVersion, _, err := s.GetNextVersion(provider, force) + if err != nil { + return err + } + fmt.Println(releaseVersion.Next.Version.String()) + return nil + }, +} diff --git a/cmd/go-semantic-release/commands/release.go b/cmd/go-semantic-release/commands/release.go new file mode 100644 index 0000000..efdd0f8 --- /dev/null +++ b/cmd/go-semantic-release/commands/release.go @@ -0,0 +1,43 @@ +package commands + +import ( + "github.com/Nightapes/go-semantic-release/pkg/semanticrelease" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(releaseCmd) +} + +var releaseCmd = &cobra.Command{ + Use: "release", + Short: "Make a release", + RunE: func(cmd *cobra.Command, args []string) error { + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + repository, err := cmd.Flags().GetString("repository") + if err != nil { + return err + } + + force, err := cmd.Flags().GetBool("no-cache") + if err != nil { + return err + } + + s, err := semanticrelease.New(readConfig(config), repository) + if err != nil { + return err + } + + provider, err := s.GetCIProvider() + if err != nil { + return err + } + + return s.Release(provider, force) + }, +} diff --git a/cmd/go-semantic-release/commands/root.go b/cmd/go-semantic-release/commands/root.go new file mode 100644 index 0000000..0dcfbe8 --- /dev/null +++ b/cmd/go-semantic-release/commands/root.go @@ -0,0 +1,60 @@ +package commands + +import ( + "os" + + "github.com/Nightapes/go-semantic-release/pkg/config" + log "github.com/sirupsen/logrus" + "github.com/spf13/cobra" +) + +var rootCmd = &cobra.Command{ + Use: "go-semantic-release", + Short: "Make simple releases", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + level, err := cmd.Flags().GetString("loglevel") + if err != nil { + return err + } + setLoglevel(level) + cmd.SilenceUsage = true + return nil + }, +} + +//Execute rootCmd +func Execute(version string) { + rootCmd.Version = version + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func init() { + currentDir, err := os.Getwd() + if err != nil { + panic(err) + } + rootCmd.PersistentFlags().StringP("repository", "r", currentDir, "Path to repository") + rootCmd.PersistentFlags().StringP("loglevel", "l", "error", "Set loglevel") + rootCmd.PersistentFlags().StringP("config", "c", ".release.yml", "Path to config file") + rootCmd.PersistentFlags().Bool("no-cache", false, "Ignore cache, don't use in ci build") +} + +func readConfig(file string) *config.ReleaseConfig { + releaseConfig, err := config.Read(file) + if err != nil { + log.Fatal(err) + } + return releaseConfig +} + +func setLoglevel(level string) { + parsed, err := log.ParseLevel(level) + if err != nil { + log.Errorf("Invalid loglevel %s", level) + } else { + log.SetLevel(parsed) + } + +} diff --git a/cmd/go-semantic-release/commands/set.go b/cmd/go-semantic-release/commands/set.go new file mode 100644 index 0000000..e98618d --- /dev/null +++ b/cmd/go-semantic-release/commands/set.go @@ -0,0 +1,41 @@ +package commands + +import ( + "github.com/Nightapes/go-semantic-release/pkg/semanticrelease" + + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(setCmd) +} + +var setCmd = &cobra.Command{ + Use: "set [version]", + Args: cobra.ExactArgs(1), + Short: "Set next release version", + RunE: func(cmd *cobra.Command, args []string) error { + + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + repository, err := cmd.Flags().GetString("repository") + if err != nil { + return err + } + + s, err := semanticrelease.New(readConfig(config), repository) + if err != nil { + return err + } + + provider, err := s.GetCIProvider() + if err != nil { + return err + } + + return s.SetVersion(provider, args[0]) + }, +} diff --git a/cmd/go-semantic-release/commands/zip.go b/cmd/go-semantic-release/commands/zip.go new file mode 100644 index 0000000..a459c32 --- /dev/null +++ b/cmd/go-semantic-release/commands/zip.go @@ -0,0 +1,37 @@ +package commands + +import ( + "github.com/Nightapes/go-semantic-release/pkg/semanticrelease" + "github.com/spf13/cobra" +) + +func init() { + rootCmd.AddCommand(zipCmd) +} + +var zipCmd = &cobra.Command{ + Use: "zip", + Short: "Zip configured artifact from release config", + RunE: func(cmd *cobra.Command, args []string) error { + config, err := cmd.Flags().GetString("config") + if err != nil { + return err + } + + repository, err := cmd.Flags().GetString("repository") + if err != nil { + return err + } + + s, err := semanticrelease.New(readConfig(config), repository) + if err != nil { + return err + } + + if err = s.ZipFiles(); err != nil { + return err + } + + return nil + }, +} diff --git a/cmd/go-semantic-release/main.go b/cmd/go-semantic-release/main.go index 73241e2..24dbfef 100644 --- a/cmd/go-semantic-release/main.go +++ b/cmd/go-semantic-release/main.go @@ -1,112 +1,11 @@ package main import ( - "os" - - "github.com/Nightapes/go-semantic-release/pkg/semanticrelease" - log "github.com/sirupsen/logrus" - "gopkg.in/urfave/cli.v1" // imports as package "cli" + "github.com/Nightapes/go-semantic-release/cmd/go-semantic-release/commands" ) +var version string + func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "loglevel", - Value: "error", - Usage: "Set loglevel 'LEVEL", - }, - } - - app.Commands = []cli.Command{ - { - Name: "version", - Aliases: []string{"v"}, - Usage: "version commands", - Subcommands: []cli.Command{ - { - Name: "set", - Usage: "set version `VERSION`", - Action: func(c *cli.Context) error { - setLoglevel(c.GlobalString("loglevel")) - path := c.String("path") - version := c.Args().First() - log.Infof("Version %s", version) - return semanticrelease.SetVersion(version, path) - }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Value: "release-config.json", - Usage: "Load release configuration from `FILE`", - }, - cli.StringFlag{ - Name: "path, p", - Usage: "`PATH` to repro ", - }, - }, - }, - { - Name: "next", - Usage: "get next `VERSION` or the set one ", - Action: func(c *cli.Context) error { - setLoglevel(c.GlobalString("loglevel")) - path := c.String("path") - return semanticrelease.GetNextVersion(path) - }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Value: "release-config.json", - Usage: "Load release configuration from `FILE`", - }, - cli.StringFlag{ - Name: "path, p", - Usage: "`PATH` to repro ", - }, - }, - }, - }, - }, - { - Name: "release", - Aliases: []string{}, - Usage: "make release", - Action: func(c *cli.Context) error { - return nil - }, - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Value: "release-config.json", - Usage: "Load release configuration from `FILE`", - }, - }, - }, - { - Name: "init", - Aliases: []string{}, - Usage: "create config", - Action: func(c *cli.Context) error { - return nil - }, - }, - } - - //gitutil.GetCommits(folder) - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} - -func setLoglevel(level string) { - parsed, err := log.ParseLevel(level) - if err != nil { - log.Errorf("Invalid loglevel %s", level) - } else { - log.SetLevel(parsed) - } - + commands.Execute(version) } diff --git a/go.mod b/go.mod index 738c147..d1e9cef 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,20 @@ go 1.12 require ( github.com/Masterminds/semver v1.4.2 - github.com/sirupsen/logrus v1.4.1 - github.com/urfave/cli v1.20.0 // indirect - golang.org/x/tools v0.0.0-20190508150211-cf84161cff3f // indirect - gopkg.in/src-d/go-git.v4 v4.11.0 - gopkg.in/urfave/cli.v1 v1.20.0 + github.com/coreos/etcd v3.3.10+incompatible + github.com/gliderlabs/ssh v0.2.2 // indirect + github.com/google/go-cmp v0.3.0 // indirect + github.com/google/go-github/v25 v25.1.3 + github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c // indirect + github.com/sirupsen/logrus v1.4.2 + github.com/spf13/cobra v0.0.5 + github.com/stretchr/testify v1.3.0 + golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 // indirect + golang.org/x/net v0.0.0-20190628185345-da137c7871d7 // indirect + golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 + golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect + google.golang.org/appengine v1.6.1 // indirect + gopkg.in/src-d/go-billy.v4 v4.3.1 + gopkg.in/src-d/go-git.v4 v4.12.0 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index e4a93b0..5a9a17d 100644 --- a/go.sum +++ b/go.sum @@ -1,77 +1,154 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc= github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emirpasic/gods v1.9.0 h1:rUF4PuzEjMChMiNsVjdI+SyLu7rEqpQ5reNFnhC7oFo= -github.com/emirpasic/gods v1.9.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg= +github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/gliderlabs/ssh v0.1.3/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= +github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-github/v25 v25.1.3 h1:Ht4YIQgUh4l4lc80fvGnw60khXysXvlgPxPP8uJG3EA= +github.com/google/go-github/v25 v25.1.3/go.mod h1:6z5pC69qHtrPJ0sXPsj4BLnd82b+r6sLB7qcBoRZqpw= +github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e h1:RgQk53JHp/Cjunrr1WlsXSZpqXn+uREuHvUVcK82CV8= github.com/kevinburke/ssh_config v0.0.0-20180830205328-81db2a75821e/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c h1:VAx3LRNjVNvjtgO7KFRuT/3aye/0zJvwn01rHSfoolo= +github.com/kevinburke/ssh_config v0.0.0-20190630040420-2e50c441276c/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0= -github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/pelletier/go-buffruneio v0.2.0 h1:U4t4R6YkofJ5xHm3dJzuRpPZ0mr5MMCoAWooScCR7aA= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= +github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= -github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= -github.com/xanzy/ssh-agent v0.2.0 h1:Adglfbi5p9Z0BmK2oKU9nTG+zKfniSfnaMYB+ULd+Ro= -github.com/xanzy/ssh-agent v0.2.0/go.mod h1:0NyE30eGUDliuLEHJgYte/zncp2zdTStcOnWhgSqHD8= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/xanzy/ssh-agent v0.2.1 h1:TCbipTQL2JiiCprBWx9frJ2eJlCYT00NmctrHxVAr70= +github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= +github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= +golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/crypto v0.0.0-20190422183909-d864b10871cd/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190502183928-7f726cade0ab/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7 h1:rTIdg5QFRR7XCaK4LCjBiPbx8j4DQRpdYMnGn/bJUEU= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6 h1:bjcUS9ztw9kFmmIxJInhon/0Is3p+EHBKNgquIzo1OI= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9 h1:lkiLiLBHGoH3XnqSLUIaBsilGMUjI+Uy2Xu2JLUtTas= golang.org/x/sys v0.0.0-20180903190138-2b024373dcd9/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190221075227-b4e8571b14e0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= +golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20190508150211-cf84161cff3f h1:sjxqKRXfnzgJFg/igBXeLZoBVAKXuAAljgr+PcNr7u8= -golang.org/x/tools v0.0.0-20190508150211-cf84161cff3f/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1 h1:QzqyMA1tlu6CgqCDUtU9V+ZKhLFT2dkJuANu5QaxI3I= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/src-d/go-billy.v4 v4.2.1 h1:omN5CrMrMcQ+4I8bJ0wEhOBPanIRWzFC953IiXKdYzo= -gopkg.in/src-d/go-billy.v4 v4.2.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= -gopkg.in/src-d/go-git-fixtures.v3 v3.1.1 h1:XWW/s5W18RaJpmo1l0IYGqXKuJITWRFuA45iOf1dKJs= -gopkg.in/src-d/go-git-fixtures.v3 v3.1.1/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= -gopkg.in/src-d/go-git.v4 v4.11.0 h1:cJwWgJ0DXifrNrXM6RGN1Y2yR60Rr1zQ9Q5DX5S9qgU= -gopkg.in/src-d/go-git.v4 v4.11.0/go.mod h1:Vtut8izDyrM8BUVQnzJ+YvmNcem2J89EmfZYCkLokZk= -gopkg.in/urfave/cli.v1 v1.20.0 h1:NdAVW6RYxDif9DhDHaAortIu956m2c0v+09AZBPTbE0= -gopkg.in/urfave/cli.v1 v1.20.0/go.mod h1:vuBzUtMdQeixQj8LVd+/98pzhxNGQoyuPBlsXHOQNO0= +gopkg.in/src-d/go-billy.v4 v4.3.0/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/src-d/go-billy.v4 v4.3.1 h1:OkK1DmefDy1Z6Veu82wdNj/cLpYORhdX4qdaYCPwc7s= +gopkg.in/src-d/go-billy.v4 v4.3.1/go.mod h1:tm33zBoOwxjYHZIE+OV8bxTWFMJLrconzFMd38aARFk= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0 h1:ivZFOIltbce2Mo8IjzUHAFoq/IylO9WHhNOAJK+LsJg= +gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= +gopkg.in/src-d/go-git.v4 v4.12.0 h1:CKgvBCJCcdfNnyXPYI4Cp8PaDDAmAPEN0CtfEdEAbd8= +gopkg.in/src-d/go-git.v4 v4.12.0/go.mod h1:zjlNnzc1Wjn43v3Mtii7RVxiReNP0fIu9npcXKzuNp4= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/internal/analyzer/analyzer.go b/internal/analyzer/analyzer.go index 3e32c64..8e6d1be 100644 --- a/internal/analyzer/analyzer.go +++ b/internal/analyzer/analyzer.go @@ -1,63 +1,102 @@ +// Package analyzer provides different commit analyzer package analyzer import ( + "fmt" + "github.com/Nightapes/go-semantic-release/internal/gitutil" + "github.com/Nightapes/go-semantic-release/pkg/config" log "github.com/sirupsen/logrus" ) +//Analyzer struct type Analyzer struct { - CommitFormat string + analyzeCommit analyzeCommit + Config config.ChangelogConfig } -type Rules struct { - Tag string - Release string +//Release types, like major +type Release string + +//Scope of the commit, like feat, fix,.. +type Scope string + +//Rule for commits +type Rule struct { + Tag string + TagString string + Release Release + Changelog bool } -type AnalyzeCommit interface { - Analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bool) - GetRules() []Rules +type analyzeCommit interface { + analyze(commit gitutil.Commit, tag Rule) (AnalyzedCommit, bool, error) + getRules() []Rule } +//AnalyzedCommit struct type AnalyzedCommit struct { Commit gitutil.Commit ParsedMessage string - Scope string + Scope Scope ParsedBreakingChangeMessage string + Tag string + TagString string + Print bool } -func New(format string) *Analyzer { - return &Analyzer{ - CommitFormat: format, +//New Analyzer struct for given commit format +func New(format string, config config.ChangelogConfig) (*Analyzer, error) { + analyzer := &Analyzer{ + Config: config, } -} - -func (a *Analyzer) Analyze(commits []gitutil.Commit) map[string][]AnalyzedCommit { - - var commitAnalayzer AnalyzeCommit - switch a.CommitFormat { + switch format { case "angular": - log.Infof("analyze angular format") - commitAnalayzer = NewAngular() + log.Debugf("Commit format set to angular") + analyzer.analyzeCommit = newAngular() + default: + return nil, fmt.Errorf("invalid commit format: %s", format) } + return analyzer, nil - analyzedCommits := make(map[string][]AnalyzedCommit) +} + +// GetRules from current mode +func (a *Analyzer) GetRules() []Rule { + return a.analyzeCommit.getRules() +} + +// Analyze commits and return commits splitted by major,minor,patch +func (a *Analyzer) Analyze(commits []gitutil.Commit) map[Release][]AnalyzedCommit { + + analyzedCommits := make(map[Release][]AnalyzedCommit) analyzedCommits["major"] = make([]AnalyzedCommit, 0) analyzedCommits["minor"] = make([]AnalyzedCommit, 0) analyzedCommits["patch"] = make([]AnalyzedCommit, 0) + analyzedCommits["none"] = make([]AnalyzedCommit, 0) for _, commit := range commits { - for _, rule := range commitAnalayzer.GetRules() { - analyzedCommit, hasBreakingChange := commitAnalayzer.Analyze(commit, rule.Tag) - if hasBreakingChange { - analyzedCommits["major"] = append(analyzedCommits["major"], analyzedCommit) - } else { - analyzedCommits[rule.Release] = append(analyzedCommits[rule.Release], analyzedCommit) + for _, rule := range a.analyzeCommit.getRules() { + analyzedCommit, hasBreakingChange, err := a.analyzeCommit.analyze(commit, rule) + if err == nil { + if a.Config.PrintAll { + analyzedCommit.Print = true + } else { + analyzedCommit.Print = rule.Changelog + } + if hasBreakingChange { + analyzedCommits["major"] = append(analyzedCommits["major"], analyzedCommit) + } else { + analyzedCommits[rule.Release] = append(analyzedCommits[rule.Release], analyzedCommit) + } + break } } } + log.Debugf("Analyzed commits: major=%d minor=%d patch=%d none=%d", len(analyzedCommits["major"]), len(analyzedCommits["minor"]), len(analyzedCommits["patch"]), len(analyzedCommits["none"])) + return analyzedCommits } diff --git a/internal/analyzer/analyzer_test.go b/internal/analyzer/analyzer_test.go new file mode 100644 index 0000000..0002a73 --- /dev/null +++ b/internal/analyzer/analyzer_test.go @@ -0,0 +1,16 @@ +package analyzer_test + +import ( + "testing" + + "github.com/Nightapes/go-semantic-release/internal/analyzer" + "github.com/Nightapes/go-semantic-release/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestAnalyzer(t *testing.T) { + + _, err := analyzer.New("unknown", config.ChangelogConfig{}) + assert.Error(t, err) + +} diff --git a/internal/analyzer/angular.go b/internal/analyzer/angular.go index 936e42e..47fe6d2 100644 --- a/internal/analyzer/angular.go +++ b/internal/analyzer/angular.go @@ -1,65 +1,113 @@ +// Package analyzer provides different commit analyzer package analyzer import ( + "fmt" "regexp" "strings" + log "github.com/sirupsen/logrus" + "github.com/Nightapes/go-semantic-release/internal/gitutil" ) -type Angular struct { - rules []Rules +type angular struct { + rules []Rule regex string } -func NewAngular() *Angular { - return &Angular{ +func newAngular() *angular { + return &angular{ regex: `(TAG)(?:\((.*)\))?: (.*)`, - rules: []Rules{ + rules: []Rule{ { - Tag: "feat", - Release: "minor", + Tag: "feat", + TagString: "Features", + Release: "minor", + Changelog: true, }, { - Tag: "fix", - Release: "patch", + Tag: "fix", + TagString: "Bug fixes", + Release: "patch", + Changelog: true, }, { - Tag: "perf", - Release: "patch", + Tag: "perf", + TagString: "Performance improvments", + Release: "patch", + Changelog: true, + }, { + Tag: "docs", + TagString: "Documentation changes", + Release: "none", + Changelog: false, + }, + { + Tag: "style", + TagString: "Style", + Release: "none", + Changelog: false, + }, { + Tag: "refactor", + TagString: "Code refactor", + Release: "none", + Changelog: false, + }, { + Tag: "test", + TagString: "Testing", + Release: "none", + Changelog: false, + }, { + Tag: "chore", + TagString: "Changes to the build process or auxiliary tools and libraries such as documentation generation", + Release: "none", + Changelog: false, + }, { + Tag: "build", + TagString: "Changes to CI/CD", + Release: "none", + Changelog: false, }, }, } } -func (a *Angular) GetRules() []Rules { +func (a *angular) getRules() []Rule { return a.rules } -func (a *Angular) Analyze(commit gitutil.Commit, tag string) (AnalyzedCommit, bool) { +func (a *angular) analyze(commit gitutil.Commit, rule Rule) (AnalyzedCommit, bool, error) { analyzed := AnalyzedCommit{ - Commit: commit, + Commit: commit, + Tag: rule.Tag, + TagString: rule.TagString, } - re := regexp.MustCompile(strings.Replace(a.regex, "TAG", tag, -1)) - matches := re.FindAllStringSubmatch(commit.Message+" "+commit.Message, -1) + re := regexp.MustCompile(strings.Replace(a.regex, "TAG", rule.Tag, -1)) + matches := re.FindAllStringSubmatch(commit.Message, -1) if len(matches) >= 1 { if len(matches[0]) >= 3 { - analyzed.Scope = matches[0][2] + + analyzed.Scope = Scope(matches[0][2]) message := strings.Join(matches[0][3:], "") - splitted := strings.SplitN(message, "BREAKING CHANGE:", 1) + if !strings.Contains(message, "BREAKING CHANGE:") { + analyzed.ParsedMessage = strings.Trim(message, " ") - if len(splitted) == 1 { - analyzed.ParsedMessage = splitted[0] - return analyzed, false + log.Tracef("%s: found %s", commit.Message, rule.Tag) + return analyzed, false, nil } - analyzed.ParsedMessage = splitted[0] - analyzed.ParsedBreakingChangeMessage = splitted[1] - return analyzed, true + breakingChange := strings.SplitN(message, "BREAKING CHANGE:", 2) + analyzed.ParsedMessage = strings.Trim(breakingChange[0], " ") + analyzed.ParsedBreakingChangeMessage = strings.Trim(breakingChange[1], " ") + + log.Tracef(" %s, BREAKING CHANGE found", commit.Message) + return analyzed, true, nil } } - return analyzed, false + log.Tracef("%s does not match %s, skip", commit.Message, rule.Tag) + return analyzed, false, fmt.Errorf("not found") } diff --git a/internal/analyzer/angular_test.go b/internal/analyzer/angular_test.go new file mode 100644 index 0000000..c49b046 --- /dev/null +++ b/internal/analyzer/angular_test.go @@ -0,0 +1,173 @@ +package analyzer_test + +import ( + "testing" + + "github.com/Nightapes/go-semantic-release/internal/analyzer" + "github.com/Nightapes/go-semantic-release/internal/gitutil" + "github.com/Nightapes/go-semantic-release/pkg/config" + "github.com/stretchr/testify/assert" +) + +func TestAngular(t *testing.T) { + + testConfigs := []struct { + testCase string + commits []gitutil.Commit + analyzedCommits map[analyzer.Release][]analyzer.AnalyzedCommit + }{ + { + testCase: "feat", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(internal/changelog): my first commit", + Author: "me", + Hash: "12345667", + }, + Scope: "internal/changelog", + ParsedMessage: "my first commit", + Tag: "feat", + TagString: "Features", + Print: true, + }, + }, + "major": []analyzer.AnalyzedCommit{}, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + commits: []gitutil.Commit{ + gitutil.Commit{ + Message: "feat(internal/changelog): my first commit", + Author: "me", + Hash: "12345667", + }, + }, + }, + { + testCase: "feat breaking change", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(internal/changelog): my first commit", + Author: "me", + Hash: "12345667", + }, + Scope: "internal/changelog", + ParsedMessage: "my first commit", + Tag: "feat", + TagString: "Features", + Print: true, + }, + }, + "major": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(internal/changelog): my first break BREAKING CHANGE: change api to v2", + Author: "me", + Hash: "12345668", + }, + Scope: "internal/changelog", + ParsedMessage: "my first break", + Tag: "feat", + TagString: "Features", + Print: true, + ParsedBreakingChangeMessage: "change api to v2", + }, + }, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + commits: []gitutil.Commit{ + gitutil.Commit{ + Message: "feat(internal/changelog): my first commit", + Author: "me", + Hash: "12345667", + }, + gitutil.Commit{ + Message: "feat(internal/changelog): my first break BREAKING CHANGE: change api to v2", + Author: "me", + Hash: "12345668", + }, + }, + }, + { + testCase: "invalid", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "minor": []analyzer.AnalyzedCommit{}, + "major": []analyzer.AnalyzedCommit{}, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + commits: []gitutil.Commit{ + gitutil.Commit{ + Message: "internal/changelog: my first commit", + Author: "me", + Hash: "12345667", + }, + }, + }, + { + testCase: "feat and build", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(internal/changelog): my first commit", + Author: "me", + Hash: "12345667", + }, + Scope: "internal/changelog", + ParsedMessage: "my first commit", + Tag: "feat", + TagString: "Features", + Print: true, + }, + }, + "none": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "build(internal/changelog): my first build", + Author: "me", + Hash: "12345668", + }, + Scope: "internal/changelog", + ParsedMessage: "my first build", + Tag: "build", + TagString: "Changes to CI/CD", + Print: false, + ParsedBreakingChangeMessage: "", + }, + }, + "patch": []analyzer.AnalyzedCommit{}, + "major": []analyzer.AnalyzedCommit{}, + }, + commits: []gitutil.Commit{ + gitutil.Commit{ + Message: "feat(internal/changelog): my first commit", + Author: "me", + Hash: "12345667", + }, + gitutil.Commit{ + Message: "build(internal/changelog): my first build", + Author: "me", + Hash: "12345668", + }, + }, + }, + } + + angular, err := analyzer.New("angular", config.ChangelogConfig{}) + assert.NoError(t, err) + + for _, test := range testConfigs { + analyzedCommits := angular.Analyze(test.commits) + assert.Equalf(t, test.analyzedCommits["major"], analyzedCommits["major"], "Testcase %s should have major commits", test.testCase) + assert.Equalf(t, test.analyzedCommits["minor"], analyzedCommits["minor"], "Testcase %s should have minor commits", test.testCase) + assert.Equalf(t, test.analyzedCommits["patch"], analyzedCommits["patch"], "Testcase %s should have patch commits", test.testCase) + assert.Equalf(t, test.analyzedCommits["none"], analyzedCommits["none"], "Testcase %s should have none commits", test.testCase) + } + +} diff --git a/internal/cache/cache.go b/internal/cache/cache.go new file mode 100644 index 0000000..0c33d48 --- /dev/null +++ b/internal/cache/cache.go @@ -0,0 +1,93 @@ +// Package cache helper for cache version +package cache + +import ( + log "github.com/sirupsen/logrus" + "io/ioutil" + "path" + + "github.com/Masterminds/semver" + "github.com/Nightapes/go-semantic-release/internal/shared" + "gopkg.in/yaml.v2" +) + +// ReleaseVersion struct +type ReleaseVersion struct { + Last ReleaseVersionEntry `yaml:"last"` + Next ReleaseVersionEntry `yaml:"next"` + Branch string `yaml:"branch"` + Draft bool `yaml:"draft"` +} + +//ReleaseVersionEntry struct +type ReleaseVersionEntry struct { + Commit string `yaml:"commit"` + Version string `yaml:"version"` +} + +// Write version into .version +func Write(repository string, releaseVersion shared.ReleaseVersion) error { + completePath := path.Join(path.Dir(repository), ".version") + + toCache := &ReleaseVersion{ + Next: ReleaseVersionEntry{ + Commit: releaseVersion.Next.Commit, + Version: releaseVersion.Next.Version.String(), + }, + Last: ReleaseVersionEntry{ + Commit: releaseVersion.Last.Commit, + Version: releaseVersion.Last.Version.String(), + }, + Branch: releaseVersion.Branch, + Draft: releaseVersion.Draft, + } + + data, err := yaml.Marshal(toCache) + if err != nil { + return err + } + + log.Infof("Save %s with hash %s to cache", releaseVersion.Next.Version.String(), releaseVersion.Next.Commit) + return ioutil.WriteFile(completePath, data, 0644) +} + +// Read version into .version +func Read(repository string) (*shared.ReleaseVersion, error) { + completePath := path.Join(path.Dir(repository), ".version") + + content, err := ioutil.ReadFile(completePath) + if err != nil { + return &shared.ReleaseVersion{}, err + } + + var parsedContent ReleaseVersion + err = yaml.Unmarshal(content, &parsedContent) + if err != nil { + return &shared.ReleaseVersion{}, err + } + + nextVersion, err := semver.NewVersion(parsedContent.Next.Version) + if err != nil { + return nil, err + } + + lastVersion, err := semver.NewVersion(parsedContent.Last.Version) + if err != nil { + return nil, err + } + + releaseVersion := &shared.ReleaseVersion{ + Next: shared.ReleaseVersionEntry{ + Commit: parsedContent.Next.Commit, + Version: nextVersion, + }, + Last: shared.ReleaseVersionEntry{ + Commit: parsedContent.Last.Commit, + Version: lastVersion, + }, + Branch: parsedContent.Branch, + Draft: parsedContent.Draft, + } + log.Infof("Found cache, will return cached version %s", parsedContent.Next.Version) + return releaseVersion, nil +} diff --git a/internal/cache/cache_test.go b/internal/cache/cache_test.go new file mode 100644 index 0000000..07d619e --- /dev/null +++ b/internal/cache/cache_test.go @@ -0,0 +1,89 @@ +package cache_test + +import ( + "testing" + + "github.com/Masterminds/semver" + "github.com/Nightapes/go-semantic-release/internal/cache" + "github.com/Nightapes/go-semantic-release/internal/shared" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "path" +) + +func TestReadCacheNotFound(t *testing.T) { + + _, err := cache.Read("notfound/dir") + assert.Errorf(t, err, "Read non exsiting file") + +} + +func TestReadCacheInvalidContent(t *testing.T) { + + dir, err := ioutil.TempDir("", "prefix") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + completePath := path.Join(path.Dir(dir), ".version") + brokenContent := []byte("hello broken\ngo: lang\n") + err = ioutil.WriteFile(completePath, brokenContent, 0644) + assert.NoError(t, err) + + _, readError := cache.Read(dir) + assert.Errorf(t, readError, "Should give error, when broken content") + +} + +func TestWriteAndReadCache(t *testing.T) { + + dir, err := ioutil.TempDir("", "prefix") + + assert.NoError(t, err) + + content := shared.ReleaseVersion{ + Last: shared.ReleaseVersionEntry{ + Commit: "12345", + Version: createVersion("1.0.0"), + }, + Next: shared.ReleaseVersionEntry{ + Commit: "12346", + Version: createVersion("1.1.0"), + }, + Branch: "master", + Draft: true, + } + + defer os.RemoveAll(dir) + + writeError := cache.Write(dir, content) + assert.NoErrorf(t, writeError, "Should write file") + result, readError := cache.Read(dir) + assert.NoErrorf(t, readError, "Should read file") + + assert.EqualValues(t, &content, result) + +} + +func TestWriteNotFound(t *testing.T) { + + err := cache.Write("notfound/dir", shared.ReleaseVersion{ + Last: shared.ReleaseVersionEntry{ + Commit: "12345", + Version: createVersion("1.0.0"), + }, + Next: shared.ReleaseVersionEntry{ + Commit: "12346", + Version: createVersion("1.1.0"), + }, + Branch: "master", + Draft: true, + }) + assert.Errorf(t, err, "Write non exsiting file") + +} + +func createVersion(version string) *semver.Version { + ver, _ := semver.NewVersion(version) + return ver +} diff --git a/internal/calculator/calculator.go b/internal/calculator/calculator.go new file mode 100644 index 0000000..d2541fb --- /dev/null +++ b/internal/calculator/calculator.go @@ -0,0 +1,68 @@ +package calculator + +import ( + "strconv" + "strings" + + "github.com/Masterminds/semver" + "github.com/Nightapes/go-semantic-release/internal/analyzer" + + log "github.com/sirupsen/logrus" +) + +// Calculator struct +type Calculator struct{} + +// New Calculator struct +func New() *Calculator { + return &Calculator{} +} + +//IncPrerelease increase prerelease by one +func (c *Calculator) IncPrerelease(preReleaseType string, version *semver.Version) (semver.Version, error) { + defaultPrerelease := preReleaseType + ".0" + if version.Prerelease() == "" || !strings.HasPrefix(version.Prerelease(), preReleaseType) { + return version.SetPrerelease(defaultPrerelease) + } + + parts := strings.Split(version.Prerelease(), ".") + if len(parts) == 2 { + i, err := strconv.Atoi(parts[1]) + if err != nil { + log.Warnf("Could not parse release tag %s, use version %s", version.Prerelease(), version.String()) + return version.SetPrerelease(defaultPrerelease) + } + return version.SetPrerelease(preReleaseType + "." + strconv.Itoa((i + 1))) + + } + log.Warnf("Could not parse release tag %s, use version %s", version.Prerelease(), version.String()) + return version.SetPrerelease(defaultPrerelease) +} + +//CalculateNewVersion from given commits and lastversion +func (c *Calculator) CalculateNewVersion(commits map[analyzer.Release][]analyzer.AnalyzedCommit, lastVersion *semver.Version, releaseType string, firstRelease bool) (semver.Version, bool) { + switch releaseType { + case "beta", "alpha": + if len(commits["major"]) > 0 || len(commits["minor"]) > 0 || len(commits["patch"]) > 0 { + version, _ := c.IncPrerelease(releaseType, lastVersion) + return version, true + } + case "rc": + if len(commits["major"]) > 0 || len(commits["minor"]) > 0 || len(commits["patch"]) > 0 { + version, _ := c.IncPrerelease(releaseType, lastVersion) + return version, false + } + case "release": + if !firstRelease { + if len(commits["major"]) > 0 { + return lastVersion.IncMajor(), false + } else if len(commits["minor"]) > 0 { + return lastVersion.IncMinor(), false + } else if len(commits["patch"]) > 0 { + return lastVersion.IncPatch(), false + } + } + } + + return *lastVersion, false +} diff --git a/internal/calculator/calculator_test.go b/internal/calculator/calculator_test.go new file mode 100644 index 0000000..5c8f2b4 --- /dev/null +++ b/internal/calculator/calculator_test.go @@ -0,0 +1,219 @@ +package calculator_test + +import ( + "testing" + + "github.com/Masterminds/semver" + "github.com/Nightapes/go-semantic-release/internal/analyzer" + "github.com/Nightapes/go-semantic-release/internal/calculator" + "github.com/stretchr/testify/assert" +) + +func createVersion(version string) *semver.Version { + ver, _ := semver.NewVersion(version) + return ver +} + +func TestCalculator_IncPrerelease(t *testing.T) { + + testConfigs := []struct { + testCase string + preReleaseType string + lastVersion *semver.Version + nextVersion string + hasError bool + }{ + { + testCase: "version without preRelease", + preReleaseType: "alpha", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.0-alpha.0", + }, + { + testCase: "version with preRelease", + preReleaseType: "alpha", + lastVersion: createVersion("1.0.0-alpha.0"), + nextVersion: "1.0.0-alpha.1", + }, + { + testCase: "version with preRelease, change type", + preReleaseType: "beta", + lastVersion: createVersion("1.0.0-alpha.0"), + nextVersion: "1.0.0-beta.0", + }, + { + testCase: "version with preRelease but broken", + preReleaseType: "alpha", + lastVersion: createVersion("1.0.0-alpha.br0ken"), + nextVersion: "1.0.0-alpha.0", + }, + { + testCase: "version with preRelease but broken 2", + preReleaseType: "alpha", + lastVersion: createVersion("1.0.0-alphabr0ken"), + nextVersion: "1.0.0-alpha.0", + }, + } + + c := calculator.New() + + for _, test := range testConfigs { + next, err := c.IncPrerelease(test.preReleaseType, test.lastVersion) + assert.Equalf(t, test.hasError, err != nil, "Testcase %s should have error: %t -> %s", test.testCase, test.hasError, err) + assert.Equal(t, test.nextVersion, next.String()) + } + +} + +func TestCalculator_CalculateNewVersion(t *testing.T) { + + testConfigs := []struct { + testCase string + releaseType string + lastVersion *semver.Version + nextVersion string + isDraft bool + isFirst bool + analyzedCommits map[analyzer.Release][]analyzer.AnalyzedCommit + }{ + { + testCase: "version with preRelease alpha", + releaseType: "alpha", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.0-alpha.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{}, + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{}, + }, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: true, + }, + { + testCase: "version with preRelease beta", + releaseType: "beta", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.0-beta.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{}, + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{}, + }, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: true, + }, + { + testCase: "version without commits", + releaseType: "alpha", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{}, + "minor": []analyzer.AnalyzedCommit{}, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: false, + }, + { + testCase: "version with commits and first release", + releaseType: "release", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "minor": []analyzer.AnalyzedCommit{}, + "patch": []analyzer.AnalyzedCommit{}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: true, + isDraft: false, + }, + { + testCase: "version with commits and rc release", + releaseType: "rc", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.0-rc.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "minor": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "patch": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: false, + }, + { + testCase: "version with commits and rc release", + releaseType: "rc", + lastVersion: createVersion("1.0.0-rc.0"), + nextVersion: "1.0.0-rc.1", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{}, + "minor": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "patch": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: false, + }, + { + testCase: "version with commits and major release", + releaseType: "release", + lastVersion: createVersion("1.0.0"), + nextVersion: "2.0.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "minor": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "patch": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: false, + }, + { + testCase: "version with commits and minor release", + releaseType: "release", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.1.0", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{}, + "minor": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "patch": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: false, + }, + { + testCase: "version with commits and minor patch", + releaseType: "release", + lastVersion: createVersion("1.0.0"), + nextVersion: "1.0.1", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "major": []analyzer.AnalyzedCommit{}, + "minor": []analyzer.AnalyzedCommit{}, + "patch": []analyzer.AnalyzedCommit{analyzer.AnalyzedCommit{}}, + "none": []analyzer.AnalyzedCommit{}, + }, + isFirst: false, + isDraft: false, + }, + } + + c := calculator.New() + + for _, test := range testConfigs { + next, draft := c.CalculateNewVersion(test.analyzedCommits, test.lastVersion, test.releaseType, test.isFirst) + assert.Equalf(t, test.isDraft, draft, "Should have draft %t for testcase %s", test.isDraft, test.testCase) + assert.Equalf(t, test.nextVersion, next.String(), "Should have version %s for testcase %s", test.nextVersion, test.testCase) + } + +} diff --git a/internal/changelog/changelog.go b/internal/changelog/changelog.go new file mode 100644 index 0000000..e00a29f --- /dev/null +++ b/internal/changelog/changelog.go @@ -0,0 +1,132 @@ +package changelog + +import ( + "bytes" + "strings" + "text/template" + "time" + + "github.com/Nightapes/go-semantic-release/internal/analyzer" + "github.com/Nightapes/go-semantic-release/internal/shared" + "github.com/Nightapes/go-semantic-release/pkg/config" + + log "github.com/sirupsen/logrus" +) + +const defaultChangelogTitle string = `v{{.Version}} ({{.Now.Format "2006-01-02"}})` +const defaultChangelog string = `# v{{$.Version}} ({{.Now.Format "2006-01-02"}}) +{{ range $index,$commit := .BreakingChanges -}} +{{ if eq $index 0 }} +## BREAKING CHANGES +{{ end}} +* **{{$.Backtick}}{{$commit.Scope}}{{$.Backtick}}** {{$commit.ParsedBreakingChangeMessage}} +introduced by commit: +{{$commit.ParsedMessage}} {{if $.HasURL}} ([{{ printf "%.7s" $commit.Commit.Hash}}]({{ replace $.URL "{{hash}}" $commit.Commit.Hash}})) {{end}} +{{ end -}} +{{ range $key := .Order }} +{{ $commits := index $.Commits $key}} {{if $commits -}} +### {{ $key }} +{{ range $index,$commit := $commits -}} +* **{{$.Backtick}}{{$commit.Scope}}{{$.Backtick}}** {{$commit.ParsedMessage}} {{if $.HasURL}} ([{{ printf "%.7s" $commit.Commit.Hash}}]({{ replace $.URL "{{hash}}" $commit.Commit.Hash}})) {{end}} +{{ end -}} +{{ end -}} +{{ end -}} +` + +type changelogContent struct { + Commits map[string][]analyzer.AnalyzedCommit + BreakingChanges []analyzer.AnalyzedCommit + Order []string + Version string + Now time.Time + Backtick string + HasURL bool + URL string +} + +//Changelog struct +type Changelog struct { + config *config.ReleaseConfig + rules []analyzer.Rule + releaseTime time.Time +} + +//New Changelog struct for generating changelog from commits +func New(config *config.ReleaseConfig, rules []analyzer.Rule, releaseTime time.Time) *Changelog { + return &Changelog{ + config: config, + rules: rules, + releaseTime: releaseTime, + } +} + +// GenerateChanglog from given commits +func (c *Changelog) GenerateChanglog(templateConfig shared.ChangelogTemplateConfig, analyzedCommits map[analyzer.Release][]analyzer.AnalyzedCommit) (*shared.GeneratedChangelog, error) { + + commitsPerScope := map[string][]analyzer.AnalyzedCommit{} + commitsBreakingChange := []analyzer.AnalyzedCommit{} + order := make([]string, 0) + + for _, rule := range c.rules { + log.Debugf("Add %s to list", rule.TagString) + if rule.Changelog || c.config.Changelog.PrintAll { + order = append(order, rule.TagString) + } + } + + for _, commits := range analyzedCommits { + for _, commit := range commits { + if commit.Print { + if commit.ParsedBreakingChangeMessage != "" { + commitsBreakingChange = append(commitsBreakingChange, commit) + continue + } + if _, ok := commitsPerScope[commit.TagString]; !ok { + commitsPerScope[commit.TagString] = make([]analyzer.AnalyzedCommit, 0) + } + commitsPerScope[commit.TagString] = append(commitsPerScope[commit.TagString], commit) + } + } + } + + changelogContent := changelogContent{ + Version: templateConfig.Version, + Commits: commitsPerScope, + Now: c.releaseTime, + BreakingChanges: commitsBreakingChange, + Backtick: "`", + Order: order, + HasURL: templateConfig.CommitURL != "", + URL: templateConfig.CommitURL, + } + + title, err := generateTemplate(defaultChangelogTitle, changelogContent) + if err != nil { + return nil, err + } + content, err := generateTemplate(defaultChangelog, changelogContent) + + return &shared.GeneratedChangelog{Title: title, Content: content}, err +} + +func generateTemplate(text string, values changelogContent) (string, error) { + + funcMap := template.FuncMap{ + "replace": replace, + } + + var tpl bytes.Buffer + tmpl, err := template.New("template").Funcs(funcMap).Parse(text) + if err != nil { + return "", err + } + err = tmpl.Execute(&tpl, values) + if err != nil { + return "", err + } + return tpl.String(), nil +} + +func replace(input, from, to string) string { + return strings.Replace(input, from, to, -1) +} diff --git a/internal/changelog/changelog_test.go b/internal/changelog/changelog_test.go new file mode 100644 index 0000000..b4a473a --- /dev/null +++ b/internal/changelog/changelog_test.go @@ -0,0 +1,120 @@ +package changelog_test + +import ( + "testing" + + "github.com/Nightapes/go-semantic-release/internal/analyzer" + "github.com/Nightapes/go-semantic-release/internal/changelog" + "github.com/Nightapes/go-semantic-release/internal/gitutil" + "github.com/Nightapes/go-semantic-release/internal/shared" + "github.com/Nightapes/go-semantic-release/pkg/config" + "github.com/stretchr/testify/assert" + "time" +) + +func TestChangelog(t *testing.T) { + + templateConfig := shared.ChangelogTemplateConfig{ + CommitURL: "https://commit.url", + CompareURL: "https://compare.url", + Hash: "hash", + Version: "1.0.0", + } + + testConfigs := []struct { + testCase string + analyzedCommits map[analyzer.Release][]analyzer.AnalyzedCommit + result *shared.GeneratedChangelog + hasError bool + }{ + { + testCase: "feat", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(test): my first commit", + Author: "me", + Hash: "12345667", + }, + Scope: "internal/changelog", + ParsedMessage: "my first commit", + Tag: "feat", + TagString: "Features", + Print: true, + }, + }, + }, + result: &shared.GeneratedChangelog{ + Title: "v1.0.0 (2019-07-19)", + Content: "# v1.0.0 (2019-07-19)\n\n ### Features\n* **`internal/changelog`** my first commit ([1234566](https://commit.url)) \n\n ", + }, + hasError: false, + }, + { + testCase: "feat breaking change", + analyzedCommits: map[analyzer.Release][]analyzer.AnalyzedCommit{ + "minor": []analyzer.AnalyzedCommit{ + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(test): my first commit", + Author: "me", + Hash: "12345667", + }, + Scope: "internal/changelog", + ParsedMessage: "my first commit", + Tag: "feat", + TagString: "Features", + Print: true, + }, + analyzer.AnalyzedCommit{ + Commit: gitutil.Commit{ + Message: "feat(test): my first break: BREAKING CHANGE: change api to v2", + Author: "me", + Hash: "12345668", + }, + Scope: "internal/changelog", + ParsedMessage: "my first break", + Tag: "feat", + TagString: "Features", + Print: true, + ParsedBreakingChangeMessage: "change api to v2", + }, + }, + }, + result: &shared.GeneratedChangelog{ + Title: "v1.0.0 (2019-07-19)", + Content: "# v1.0.0 (2019-07-19)\n\n## BREAKING CHANGES\n\n* **`internal/changelog`** change api to v2 \nintroduced by commit: \nmy first break ([1234566](https://commit.url)) \n\n ### Features\n* **`internal/changelog`** my first commit ([1234566](https://commit.url)) \n\n ", + }, + hasError: false, + }, + } + + cl := changelog.New(&config.ReleaseConfig{}, []analyzer.Rule{ + { + Tag: "feat", + TagString: "Features", + Release: "minor", + Changelog: true, + }, + { + Tag: "fix", + TagString: "Bug fixes", + Release: "patch", + Changelog: true, + }, + { + Tag: "build", + TagString: "Build", + Release: "none", + Changelog: false, + }, + }, time.Date(2019, 7, 19, 0, 0, 0, 0, time.UTC)) + + for _, config := range testConfigs { + generatedChangelog, err := cl.GenerateChanglog(templateConfig, config.analyzedCommits) + assert.Equalf(t, config.hasError, err != nil, "Testcase %s should have error: %t -> %s", config.testCase, config.hasError, err) + assert.Equalf(t, config.result, generatedChangelog, "Testcase %s should have generated changelog", config.testCase) + } + +} diff --git a/internal/ci/ci.go b/internal/ci/ci.go new file mode 100644 index 0000000..4f1acd6 --- /dev/null +++ b/internal/ci/ci.go @@ -0,0 +1,57 @@ +package ci + +import ( + "fmt" + + "github.com/Nightapes/go-semantic-release/internal/gitutil" + log "github.com/sirupsen/logrus" + "os" + "strings" +) + +//ProviderConfig struct +type ProviderConfig struct { + IsPR bool + PR string + PRBranch string + Branch string + Tag string + Commit string + BuildURL string + Service string + Name string +} + +//Service interface +type Service interface { + detect(envs map[string]string) (*ProviderConfig, error) +} + +//ReadAllEnvs as a map +func ReadAllEnvs() map[string]string { + envs := map[string]string{} + for _, pair := range os.Environ() { + splitted := strings.SplitN(pair, "=", 2) + envs[splitted[0]] = splitted[1] + } + return envs +} + +//GetCIProvider get provider +func GetCIProvider(gitUtil *gitutil.GitUtil, envs map[string]string) (*ProviderConfig, error) { + + services := []Service{ + Travis{}, + Git{gitUtil: gitUtil}, // GIt must be the last option to check + } + + for _, service := range services { + config, err := service.detect(envs) + if err == nil { + log.Infof("Found CI: %s", config.Name) + return config, nil + } + log.Debugf("%s", err.Error()) + } + return nil, fmt.Errorf("could not find any CI, if running locally set env CI=true") +} diff --git a/internal/ci/ci_test.go b/internal/ci/ci_test.go new file mode 100644 index 0000000..38a24a1 --- /dev/null +++ b/internal/ci/ci_test.go @@ -0,0 +1,100 @@ +package ci_test + +import ( + "testing" + "time" + + "github.com/Nightapes/go-semantic-release/internal/ci" + "github.com/Nightapes/go-semantic-release/internal/gitutil" + "github.com/stretchr/testify/assert" + "gopkg.in/src-d/go-billy.v4/memfs" + "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing/object" + "gopkg.in/src-d/go-git.v4/storage/memory" +) + +func TestCi(t *testing.T) { + + fs := memfs.New() + + repository, err := git.Init(memory.NewStorage(), fs) + assert.NoError(t, err, "should open git repository") + + file, err := fs.Create("README.md") + assert.NoError(t, err, "should create file") + + w, err := repository.Worktree() + assert.NoError(t, err, "should get worktree") + + _, err = w.Add(file.Name()) + assert.NoError(t, err, "should add file") + + gitUtilInMemory := &gitutil.GitUtil{ + Repository: repository, + } + + newCommit, err := w.Commit("fix(test): add a commit", &git.CommitOptions{ + Author: &object.Signature{ + Name: "John Doe", + Email: "john@doe.org", + When: time.Now(), + }, + }) + assert.NoError(t, err, "should commit") + + testConfigs := []struct { + service string + envs map[string]string + result *ci.ProviderConfig + hasError bool + }{ + { + service: "none", + envs: map[string]string{}, + result: nil, + hasError: true, + }, + { + service: "Git", + envs: map[string]string{ + "CI": "true", + }, + result: &ci.ProviderConfig{IsPR: false, PR: "", PRBranch: "", Branch: "master", Tag: "", Commit: newCommit.String(), BuildURL: "", Service: "git", Name: "Git only"}, + hasError: false, + }, + { + service: "Travis PR", + envs: map[string]string{ + "TRAVIS": "true", + "TRAVIS_PULL_REQUEST": "10", + "TRAVIS_COMMIT": "190bfd6aa60022afd0ef830342cfb07e33c45f37", + "TRAVIS_TAG": "TAG", + "TRAVIS_BUILD_WEB_URL": "https://travis-ci.com/owner/repo/builds/1234", + "TRAVIS_BRANCH": "master", + "TRAVIS_PULL_REQUEST_BRANCH": "pr", + }, + result: &ci.ProviderConfig{IsPR: true, PR: "10", PRBranch: "pr", Branch: "master", Tag: "TAG", Commit: "190bfd6aa60022afd0ef830342cfb07e33c45f37", BuildURL: "https://travis-ci.com/owner/repo/builds/1234", Service: "travis", Name: "Travis CI"}, + hasError: false, + }, + { + service: "Travis Push", + envs: map[string]string{ + "TRAVIS": "true", + "TRAVIS_PULL_REQUEST": "false", + "TRAVIS_COMMIT": "190bfd6aa60022afd0ef830342cfb07e33c45f37", + "TRAVIS_TAG": "TAG", + "TRAVIS_BUILD_WEB_URL": "https://travis-ci.com/owner/repo/builds/1234", + "TRAVIS_BRANCH": "master", + }, + result: &ci.ProviderConfig{IsPR: false, PR: "", PRBranch: "", Branch: "master", Tag: "TAG", Commit: "190bfd6aa60022afd0ef830342cfb07e33c45f37", BuildURL: "https://travis-ci.com/owner/repo/builds/1234", Service: "travis", Name: "Travis CI"}, + hasError: false, + }, + } + + for _, config := range testConfigs { + provider, err := ci.GetCIProvider(gitUtilInMemory, config.envs) + assert.Equalf(t, config.hasError, err != nil, "Service %s should have error: %t -> %s", config.service, config.hasError, err) + assert.Equalf(t, config.result, provider, "Service %s should have provider", config.service) + } + +} diff --git a/internal/ci/git.go b/internal/ci/git.go new file mode 100644 index 0000000..7f814a1 --- /dev/null +++ b/internal/ci/git.go @@ -0,0 +1,37 @@ +package ci + +import ( + "fmt" + "github.com/Nightapes/go-semantic-release/internal/gitutil" +) + +//Git struct +type Git struct { + gitUtil *gitutil.GitUtil +} + +//Detect if on Git +func (t Git) detect(envs map[string]string) (*ProviderConfig, error) { + + if _, exists := envs["CI"]; !exists { + return nil, fmt.Errorf("running not git only") + } + + hash, err := t.gitUtil.GetHash() + if err != nil { + return nil, err + } + + currentBranch, err := t.gitUtil.GetBranch() + if err != nil { + return nil, err + } + + return &ProviderConfig{ + Service: "git", + Name: "Git only", + Commit: hash, + Branch: currentBranch, + IsPR: false, + }, nil +} diff --git a/internal/ci/travis.go b/internal/ci/travis.go new file mode 100644 index 0000000..7feed2e --- /dev/null +++ b/internal/ci/travis.go @@ -0,0 +1,41 @@ +package ci + +import ( + "fmt" + log "github.com/sirupsen/logrus" +) + +//Travis struct +type Travis struct{} + +//Detect if on travis +func (t Travis) detect(envs map[string]string) (*ProviderConfig, error) { + + if _, exists := envs["TRAVIS"]; !exists { + return nil, fmt.Errorf("not running on travis") + } + + isPR := false + + value := envs["TRAVIS_PULL_REQUEST"] + pr := "" + + if value == "false" { + log.Debugf("TRAVIS_PULL_REQUEST=%s, not running on pr", value) + } else { + isPR = true + pr = value + } + + return &ProviderConfig{ + Service: "travis", + Name: "Travis CI", + Commit: envs["TRAVIS_COMMIT"], + Tag: envs["TRAVIS_TAG"], + BuildURL: envs["TRAVIS_BUILD_WEB_URL"], + Branch: envs["TRAVIS_BRANCH"], + IsPR: isPR, + PR: pr, + PRBranch: envs["TRAVIS_PULL_REQUEST_BRANCH"], + }, nil +} diff --git a/internal/gitutil/gitutil.go b/internal/gitutil/gitutil.go index f68f7eb..2588e5c 100644 --- a/internal/gitutil/gitutil.go +++ b/internal/gitutil/gitutil.go @@ -1,3 +1,4 @@ +// Package gitutil provides helper methods for git package gitutil import ( @@ -7,32 +8,37 @@ import ( "github.com/Masterminds/semver" log "github.com/sirupsen/logrus" "gopkg.in/src-d/go-git.v4" + "gopkg.in/src-d/go-git.v4/plumbing" "gopkg.in/src-d/go-git.v4/plumbing/object" ) +// Commit struct type Commit struct { Message string Author string Hash string } -type GitUtils struct { +// GitUtil struct +type GitUtil struct { Repository *git.Repository } -func New(folder string) (*GitUtils, error) { +// New GitUtil struct and open git repository +func New(folder string) (*GitUtil, error) { r, err := git.PlainOpen(folder) if err != nil { return nil, err } - utils := &GitUtils{ + utils := &GitUtil{ Repository: r, } return utils, nil } -func (g *GitUtils) GetHash() (string, error) { +// GetHash from git HEAD +func (g *GitUtil) GetHash() (string, error) { ref, err := g.Repository.Head() if err != nil { return "", err @@ -40,38 +46,59 @@ func (g *GitUtils) GetHash() (string, error) { return ref.Hash().String(), nil } -func (g *GitUtils) GetBranch() (string, error) { +// GetBranch from git HEAD +func (g *GitUtil) GetBranch() (string, error) { ref, err := g.Repository.Head() if err != nil { return "", err } if !ref.Name().IsBranch() { - return "", fmt.Errorf("No branch found, found %s, please checkout a branch (git checkout )", ref.Name().String()) - } + branches, err := g.Repository.Branches() + if err != nil { + return "", err + } + var currentBranch string + found := branches.ForEach(func(p *plumbing.Reference) error { + + if p.Name().IsBranch() && p.Name().Short() != "origin" { + currentBranch = p.Name().Short() + return fmt.Errorf("break") + } + return nil + }) + + if found != nil { + log.Debugf("Found branch from HEAD %s", currentBranch) + return currentBranch, nil + } + + return "", fmt.Errorf("no branch found, found %s, please checkout a branch (git checkout -b )", ref.Name().String()) + } + log.Debugf("Found branch %s", ref.Name().Short()) return ref.Name().Short(), nil } -func (g *GitUtils) GetLastVersion() (*semver.Version, string, error) { +// GetLastVersion from git tags +func (g *GitUtil) GetLastVersion() (*semver.Version, string, error) { - log.Debugf("GetLastVersion") + var tags []*semver.Version + + gitTags, err := g.Repository.Tags() - tagObjects, err := g.Repository.TagObjects() if err != nil { return nil, "", err } - var tags []*semver.Version + err = gitTags.ForEach(func(p *plumbing.Reference) error { + v, err := semver.NewVersion(p.Name().Short()) + log.Tracef("%+v with hash: %s", p.Target(), p.Hash()) - err = tagObjects.ForEach(func(t *object.Tag) error { - v, err := semver.NewVersion(t.Name) - - if err != nil { - log.Debugf("Tag %s is not a valid version, skip", t.Name) - } else { - log.Debugf("Add tag %s", t.Name) + if err == nil { tags = append(tags, v) + } else { + log.Debugf("Tag %s is not a valid version, skip", p.Name().Short()) } return nil }) @@ -94,18 +121,13 @@ func (g *GitUtils) GetLastVersion() (*semver.Version, string, error) { return nil, "", err } - tagObject, err := g.Repository.TagObject(tag.Hash()) - if err != nil { - return nil, "", err - } - - log.Debugf("Found old hash %s", tagObject.Target.String()) - return tags[0], tagObject.Target.String(), nil + log.Debugf("Found old hash %s", tag.Hash().String()) + return tags[0], tag.Hash().String(), nil } -func (g *GitUtils) GetCommits(lastTagHash string) ([]Commit, error) { +// GetCommits from git hash to HEAD +func (g *GitUtil) GetCommits(lastTagHash string) ([]Commit, error) { - log.Printf("Read head") ref, err := g.Repository.Head() if err != nil { return nil, err @@ -121,11 +143,12 @@ func (g *GitUtils) GetCommits(lastTagHash string) ([]Commit, error) { err = cIter.ForEach(func(c *object.Commit) error { if c.Hash.String() == lastTagHash { - log.Infof("%s == %s", c.Hash.String(), lastTagHash) + log.Debugf("Found commit with hash %s, will stop here", c.Hash.String()) foundEnd = true - } + } if !foundEnd { + log.Tracef("Found commit with hash %s", c.Hash.String()) commit := Commit{ Message: c.Message, Author: c.Committer.Name, @@ -136,5 +159,5 @@ func (g *GitUtils) GetCommits(lastTagHash string) ([]Commit, error) { return nil }) - return commits, nil + return commits, err } diff --git a/internal/releaser/github/github.go b/internal/releaser/github/github.go new file mode 100644 index 0000000..3f73b95 --- /dev/null +++ b/internal/releaser/github/github.go @@ -0,0 +1,141 @@ +package github + +import ( + "context" + "fmt" + "net/http" + "os" + "strings" + + "github.com/Nightapes/go-semantic-release/internal/releaser/util" + "github.com/Nightapes/go-semantic-release/internal/shared" + "github.com/Nightapes/go-semantic-release/pkg/config" + + "github.com/google/go-github/v25/github" + log "github.com/sirupsen/logrus" +) + +// GITHUB identifer for github interface +const GITHUB = "github" + +// Client type struct +type Client struct { + config *config.GitHubProvider + client *github.Client + context context.Context + release *github.RepositoryRelease + baseURL string +} + +// New initialize a new GitHubRelease +func New(c *config.GitHubProvider) (*Client, error) { + var err error + + if c.AccessToken, err = util.GetAccessToken(GITHUB); err != nil { + return &Client{}, err + } + ctx := context.Background() + httpClient := util.CreateBearerHTTPClient(ctx, c.AccessToken) + + var client *github.Client + baseURL := "https://github.com" + if c.CustomURL == "" { + client = github.NewClient(httpClient) + } else { + if client, err = github.NewEnterpriseClient(c.CustomURL, c.CustomURL+"/api/v3/", httpClient); err != nil { + return &Client{}, err + } + baseURL = c.CustomURL + } + return &Client{ + config: c, + client: client, + context: ctx, + baseURL: baseURL, + }, err +} + +//GetCommitURL for github +func (g *Client) GetCommitURL() string { + return fmt.Sprintf("%s/%s/%s/commit/{{hash}}", g.baseURL, g.config.User, g.config.Repo) +} + +//GetCompareURL for github +func (g *Client) GetCompareURL(oldVersion, newVersion string) string { + return fmt.Sprintf("%s/%s/%s/compare/%s...%s", g.baseURL, g.config.User, g.config.Repo, oldVersion, newVersion) +} + +//ValidateConfig for github +func (g *Client) ValidateConfig() error { + log.Debugf("validate GitHub provider config") + + if g.config.Repo == "" { + return fmt.Errorf("github Repro is not set") + } + + if g.config.User == "" { + return fmt.Errorf("github User is not set") + } + + return nil + +} + +// CreateRelease creates release on remote +func (g *Client) CreateRelease(releaseVersion *shared.ReleaseVersion, generatedChangelog *shared.GeneratedChangelog) error { + + tag := releaseVersion.Next.Version.String() + log.Debugf("create release with version %s", tag) + + prerelease := releaseVersion.Next.Version.Prerelease() != "" + + release, resp, err := g.client.Repositories.CreateRelease(g.context, g.config.User, g.config.Repo, &github.RepositoryRelease{ + TagName: &tag, + TargetCommitish: &releaseVersion.Branch, + Name: &generatedChangelog.Title, + Body: &generatedChangelog.Content, + Draft: &releaseVersion.Draft, + Prerelease: &prerelease, + }) + + if err != nil { + if !strings.Contains(err.Error(), "already_exists") && resp.StatusCode >= http.StatusUnprocessableEntity { + return fmt.Errorf("could not create release: %v", err) + } + log.Infof("A release with tag %s already exits, will not perform a release or update", tag) + } else { + g.release = release + log.Debugf("Release repsone: %+v", *release) + log.Infof("Crated release") + } + + return nil +} + +// UploadAssets uploads specified assets +func (g *Client) UploadAssets(repoDir string, assets []config.Asset) error { + if g.release != nil { + filesToUpload, err := util.PrepareAssets(repoDir, assets) + if err != nil { + return err + } + for _, f := range filesToUpload { + + file, err := os.Open(*f) + if err != nil { + return err + } + fileInfo, _ := file.Stat() + + _, resp, err := g.client.Repositories.UploadReleaseAsset(g.context, g.config.User, g.config.Repo, g.release.GetID(), &github.UploadOptions{Name: fileInfo.Name()}, file) + if err != nil { + return err + } + + if resp.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("releaser: github: Could not upload asset %s: %s", file.Name(), resp.Status) + } + } + } + return nil +} diff --git a/internal/releaser/releaser.go b/internal/releaser/releaser.go new file mode 100644 index 0000000..5d82da3 --- /dev/null +++ b/internal/releaser/releaser.go @@ -0,0 +1,42 @@ +package releaser + +import ( + "fmt" + + "github.com/Nightapes/go-semantic-release/internal/releaser/github" + "github.com/Nightapes/go-semantic-release/internal/shared" + + "github.com/Nightapes/go-semantic-release/pkg/config" + log "github.com/sirupsen/logrus" +) + +// Releasers struct type +type Releasers struct { + config *config.ReleaseConfig +} + +// Releaser interface for providers +type Releaser interface { + ValidateConfig() error + CreateRelease(*shared.ReleaseVersion, *shared.GeneratedChangelog) error + UploadAssets(repoDir string, assets []config.Asset) error + GetCommitURL() string + GetCompareURL(oldVersion, newVersion string) string +} + +// New initialize a Relerser +func New(c *config.ReleaseConfig) *Releasers { + return &Releasers{ + config: c, + } +} + +//GetReleaser returns an initialized releaser +func (r *Releasers) GetReleaser() (Releaser, error) { + switch r.config.Release { + case github.GITHUB: + log.Debugf("initialize new %s-provider", github.GITHUB) + return github.New(&r.config.GitHubProvider) + } + return nil, fmt.Errorf("could not initialize a releaser from this type: %s", r.config.Release) +} diff --git a/internal/releaser/util/util.go b/internal/releaser/util/util.go new file mode 100644 index 0000000..86745e1 --- /dev/null +++ b/internal/releaser/util/util.go @@ -0,0 +1,111 @@ +package util + +import ( + "archive/zip" + "context" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/Nightapes/go-semantic-release/pkg/config" + log "github.com/sirupsen/logrus" + "golang.org/x/oauth2" +) + +//CreateBearerHTTPClient with given token +func CreateBearerHTTPClient(ctx context.Context, token string) *http.Client { + tokenSource := oauth2.StaticTokenSource(&oauth2.Token{ + AccessToken: token}, + ) + + client := oauth2.NewClient(ctx, tokenSource) + + return client +} + +// GetAccessToken lookup for the providers accesstoken +func GetAccessToken(providerName string) (string, error) { + var token string + var exists bool + envName := fmt.Sprintf("%s_ACCESS_TOKEN", strings.ToUpper(providerName)) + + log.Debugf("check if %s environment variable is set", envName) + + if token, exists = os.LookupEnv(envName); !exists { + return "", fmt.Errorf("could not find %s in the enviroment variables. Please check if it is set", envName) + } else if token == "" { + return "", fmt.Errorf("token %s is set in environment variables but is empty", envName) + } + return token, nil +} + +// PrepareAssets prepare all files before uploading +func PrepareAssets(repository string, assets []config.Asset) ([]*string, error) { + filesToUpload := []*string{} + for _, asset := range assets { + if asset.Name == "" { + return nil, fmt.Errorf("asset name declaration is empty, please check your configuration file") + } else if asset.Compress { + log.Debugf("Asset %s will now be compressed", asset.Name) + log.Debugf("Repo url %s", repository) + zipNameWithPath, err := zipFile(repository, asset.Name) + if err != nil { + return filesToUpload, err + } + filesToUpload = append(filesToUpload, &zipNameWithPath) + } else { + tmpFileName := fmt.Sprintf("%s/%s", repository, asset.Name) + filesToUpload = append(filesToUpload, &tmpFileName) + } + log.Debugf("Add asset %s to files to upload", asset.Name) + } + return filesToUpload, nil +} + +// ZipFile compress given file in zip format +func zipFile(repository string, file string) (string, error) { + + fileToZip, err := os.Open(repository + "/" + file) + if err != nil { + return "", err + } + defer fileToZip.Close() + + zipFileName := fmt.Sprintf("%s/%s.zip", strings.TrimSuffix(repository, "/"), file) + zipFile, err := os.Create(zipFileName) + + if err != nil { + return "", err + } + log.Debugf("Created zipfile %s", zipFile.Name()) + + defer zipFile.Close() + + fileToZipInfo, err := fileToZip.Stat() + if err != nil { + return "", err + } + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + fileToZipHeader, err := zip.FileInfoHeader(fileToZipInfo) + if err != nil { + return "", err + } + + fileToZipHeader.Name = fileToZipInfo.Name() + + fileToZipWriter, err := zipWriter.CreateHeader(fileToZipHeader) + if err != nil { + return "", err + } + + if _, err = io.Copy(fileToZipWriter, fileToZip); err != nil { + return "", err + } + + return zipFileName, nil +} diff --git a/internal/releaser/util/util_test.go b/internal/releaser/util/util_test.go new file mode 100644 index 0000000..9cf12b4 --- /dev/null +++ b/internal/releaser/util/util_test.go @@ -0,0 +1,119 @@ +package util_test + +import ( + "context" + "fmt" + "os" + "strings" + "testing" + + "github.com/Nightapes/go-semantic-release/pkg/config" + + "github.com/stretchr/testify/assert" + + "github.com/Nightapes/go-semantic-release/internal/releaser/util" +) + +func TestCreateBearerHTTPClient(t *testing.T) { + client := util.CreateBearerHTTPClient(context.Background(), "") + + assert.True(t, client != nil, "Client is empty") +} + +type testDoubleToken struct { + providerName, token string + valid bool +} + +var testDoubles = []testDoubleToken{ + testDoubleToken{providerName: "test0", token: "foo", valid: true}, + testDoubleToken{providerName: "test1", token: "", valid: false}, +} + +func TestGetAccessToken(t *testing.T) { + for _, testObject := range testDoubles { + envName := fmt.Sprintf("%s_ACCESS_TOKEN", strings.ToUpper(testObject.providerName)) + if err := os.Setenv(envName, testObject.token); err != nil { + fmt.Println(err.Error()) + } + + _, err := util.GetAccessToken(testObject.providerName) + + assert.Equal(t, testObject.valid, err == nil) + os.Unsetenv(envName) + } +} + +type testDoubleFiles struct { + testFiles []config.Asset + valid bool +} + +var files = []testDoubleFiles{ + testDoubleFiles{ + testFiles: []config.Asset{ + config.Asset{ + Name: "file0", + Compress: true, + }, + config.Asset{ + Name: "file1", + Compress: true, + }, + }, + valid: true, + }, + testDoubleFiles{ + testFiles: []config.Asset{ + config.Asset{ + Name: "", + Compress: true, + }, + config.Asset{ + Name: "", + Compress: false, + }, + }, + valid: false, + }, +} + +func TestPrepareAssets(t *testing.T) { + for _, testObject := range files { + workDir, _ := os.Getwd() + filesToDelete := []string{} + + for _, testFile := range testObject.testFiles { + + if testFile.Name != "" { + filesToDelete = append(filesToDelete, testFile.Name) + + file, err := os.Create(testFile.Name) + if err != nil { + fmt.Print(err.Error()) + } + defer file.Close() + if testFile.Compress { + filesToDelete = append(filesToDelete, testFile.Name+".zip") + } + } + + } + preparedFiles, err := util.PrepareAssets(workDir, testObject.testFiles) + + if err == nil { + assert.Equal(t, 2, len(preparedFiles)) + } + + assert.Equal(t, testObject.valid, err == nil) + + for _, file := range filesToDelete { + if err := os.Remove(file); err != nil { + fmt.Println(err.Error()) + } + + } + + } + +} diff --git a/internal/sdk/sdk.go b/internal/sdk/sdk.go deleted file mode 100644 index e69de29..0000000 diff --git a/internal/shared/shared.go b/internal/shared/shared.go new file mode 100644 index 0000000..2630aab --- /dev/null +++ b/internal/shared/shared.go @@ -0,0 +1,33 @@ +package shared + +import ( + "github.com/Masterminds/semver" +) + +//ReleaseVersion struct +type ReleaseVersion struct { + Last ReleaseVersionEntry + Next ReleaseVersionEntry + Branch string + Draft bool +} + +//ReleaseVersionEntry struct +type ReleaseVersionEntry struct { + Commit string + Version *semver.Version +} + +//GeneratedChangelog struct +type GeneratedChangelog struct { + Title string + Content string +} + +//ChangelogTemplateConfig struct +type ChangelogTemplateConfig struct { + CommitURL string + CompareURL string + Hash string + Version string +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go deleted file mode 100644 index 76787cc..0000000 --- a/internal/storage/storage.go +++ /dev/null @@ -1,42 +0,0 @@ -package storage - -import ( - "io/ioutil" - - "gopkg.in/yaml.v2" -) - -// VersionFileContent struct -type VersionFileContent struct { - Version string `yaml:"version"` - NextVersion string `yaml:"nextVersion"` - Commit string `yaml:"commit"` - Branch string `yaml:"branch"` -} - -// Write version into .version -func Write(versionFileContent VersionFileContent) error { - data, err := yaml.Marshal(&versionFileContent) - if err != nil { - return err - } - - return ioutil.WriteFile(".version", data, 0644) -} - -// Read version into .version -func Read() (*VersionFileContent, error) { - - content, err := ioutil.ReadFile(".version") - if err != nil { - return &VersionFileContent{}, err - } - - var versionFileContent VersionFileContent - err = yaml.Unmarshal(content, &versionFileContent) - if err != nil { - return &VersionFileContent{}, err - } - - return &versionFileContent, nil -} diff --git a/pkg/config/config.go b/pkg/config/config.go new file mode 100644 index 0000000..7c08d77 --- /dev/null +++ b/pkg/config/config.go @@ -0,0 +1,61 @@ +// Package config provides defimition of .release.yml and read method +package config + +import ( + "io/ioutil" + + log "github.com/sirupsen/logrus" + "gopkg.in/yaml.v2" +) + +// ChangelogConfig struct +type ChangelogConfig struct { + PrintAll bool `yaml:"printAll,omitempty"` + Template string `yaml:"template,omitempty"` + TemplatePath string `yaml:"templatePath,omitempty"` +} + +//Asset type struct +type Asset struct { + Name string `yaml:"name"` + Compress bool `yaml:"compress"` +} + +// GitHubProvider struct +type GitHubProvider struct { + Repo string `yaml:"repo"` + User string `yaml:"user"` + CustomURL string `yaml:"customUrl,omitempty"` + AccessToken string +} + +// ReleaseConfig struct +type ReleaseConfig struct { + CommitFormat string `yaml:"commitFormat"` + Branch map[string]string `yaml:"branch"` + Changelog ChangelogConfig `yaml:"changelog,omitempty"` + Release string `yaml:"release,omitempty"` + GitHubProvider GitHubProvider `yaml:"github,omitempty"` + Assets []Asset `yaml:"assets"` + ReleaseTitle string `yaml:"title"` + IsPreRelease, IsDraft bool +} + +// Read ReleaseConfig +func Read(configPath string) (*ReleaseConfig, error) { + + content, err := ioutil.ReadFile(configPath) + if err != nil { + return &ReleaseConfig{}, err + } + + var releaseConfig ReleaseConfig + err = yaml.Unmarshal(content, &releaseConfig) + if err != nil { + return &ReleaseConfig{}, err + } + + log.Tracef("Found config %+v", releaseConfig) + + return &releaseConfig, nil +} diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go new file mode 100644 index 0000000..ca3cc13 --- /dev/null +++ b/pkg/config/config_test.go @@ -0,0 +1,112 @@ +package config_test + +import ( + "testing" + + "github.com/Nightapes/go-semantic-release/pkg/config" + "github.com/stretchr/testify/assert" + "io/ioutil" + "os" + "path" +) + +func TestReadCacheNotFound(t *testing.T) { + + _, err := config.Read("notfound/dir") + assert.Errorf(t, err, "Read non exsiting file") + +} + +func TestReadCacheInvalidContent(t *testing.T) { + + dir, err := ioutil.TempDir("", "prefix") + assert.NoError(t, err) + defer os.RemoveAll(dir) + + completePath := path.Join(path.Dir(dir), ".release.yml") + brokenContent := []byte("hello broken\ngo: lang\n") + err = ioutil.WriteFile(completePath, brokenContent, 0644) + assert.NoError(t, err) + + _, readError := config.Read(completePath) + assert.Errorf(t, readError, "Should give error, when broken content") + +} + +func TestWriteAndReadCache(t *testing.T) { + + dir, err := ioutil.TempDir("", "prefix") + + assert.NoError(t, err) + defer os.RemoveAll(dir) + + completePath := path.Join(path.Dir(dir), ".release.yml") + content := []byte(` +commitFormat: angular +title: "go-semantic-release release" +branch: + master: release + rc: rc + beta: beta + alpha: alpha + add_git_releases: alpha +changelog: + printAll: false + template: '' + templatePath: '' +release: 'github' +assets: + - name: ./build/go-semantic-release + compress: false +github: + repo: "go-semantic-release" + user: "nightapes" + customUrl: "" +`) + err = ioutil.WriteFile(completePath, content, 0644) + assert.NoError(t, err) + + result, readError := config.Read(completePath) + assert.NoErrorf(t, readError, "Should read file") + + assert.EqualValues(t, &config.ReleaseConfig{ + CommitFormat: "angular", + Branch: map[string]string{"add_git_releases": "alpha", "alpha": "alpha", "beta": "beta", "master": "release", "rc": "rc"}, + Changelog: config.ChangelogConfig{ + PrintAll: false, + Template: "", + TemplatePath: ""}, + Release: "github", + GitHubProvider: config.GitHubProvider{ + Repo: "go-semantic-release", + User: "nightapes", + CustomURL: "", + AccessToken: ""}, + Assets: []config.Asset{ + config.Asset{ + Name: "./build/go-semantic-release", + Compress: false}}, + ReleaseTitle: "go-semantic-release release", + IsPreRelease: false, + IsDraft: false, + }, result) + +} + +// func TestWriteNotFound(t *testing.T) { + +// err := cache.Write("notfound/dir", shared.ReleaseVersion{ +// Last: shared.ReleaseVersionEntry{ +// Commit: "12345", +// Version: createVersion("1.0.0"), +// }, +// Next: shared.ReleaseVersionEntry{ +// Commit: "12346", +// Version: createVersion("1.1.0"), +// }, +// Branch: "master", +// Draft: true, +// }) +// assert.Errorf(t, err, "Write non exsiting file") + +// } diff --git a/pkg/semanticrelease/semantic-release.go b/pkg/semanticrelease/semantic-release.go index 5d97007..4d90658 100644 --- a/pkg/semanticrelease/semantic-release.go +++ b/pkg/semanticrelease/semantic-release.go @@ -1,114 +1,238 @@ package semanticrelease import ( - "fmt" + "io/ioutil" + "strings" + "time" "github.com/Masterminds/semver" "github.com/Nightapes/go-semantic-release/internal/analyzer" + "github.com/Nightapes/go-semantic-release/internal/cache" + "github.com/Nightapes/go-semantic-release/internal/calculator" + "github.com/Nightapes/go-semantic-release/internal/changelog" + "github.com/Nightapes/go-semantic-release/internal/ci" "github.com/Nightapes/go-semantic-release/internal/gitutil" - "github.com/Nightapes/go-semantic-release/internal/storage" + "github.com/Nightapes/go-semantic-release/internal/releaser" + "github.com/Nightapes/go-semantic-release/internal/releaser/util" + "github.com/Nightapes/go-semantic-release/internal/shared" + "github.com/Nightapes/go-semantic-release/pkg/config" log "github.com/sirupsen/logrus" ) -// GetNextVersion from .version or calculate new -func GetNextVersion(repro string) error { - util, err := gitutil.New(repro) +// SemanticRelease struct +type SemanticRelease struct { + config *config.ReleaseConfig + gitutil *gitutil.GitUtil + analyzer *analyzer.Analyzer + calculator *calculator.Calculator + releaser releaser.Releaser + repository string +} + +// New SemanticRelease struct +func New(c *config.ReleaseConfig, repository string) (*SemanticRelease, error) { + util, err := gitutil.New(repository) if err != nil { - return err + return nil, err } - hash, err := util.GetHash() + analyzer, err := analyzer.New(c.CommitFormat, c.Changelog) if err != nil { - return err + return nil, err } - content, err := storage.Read() - - if err == nil && content.Commit == hash { - fmt.Printf(content.NextVersion) - return nil - } - - log.Debugf("Mismatch git and version file %s - %s", content.Commit, hash) - - lastVersion, lastVersionHash, err := util.GetLastVersion() + releaser, err := releaser.New(c).GetReleaser() if err != nil { - return err + return nil, err } + return &SemanticRelease{ + config: c, + gitutil: util, + releaser: releaser, + analyzer: analyzer, + repository: repository, + calculator: calculator.New(), + }, nil +} + +//GetCIProvider result with ci config +func (s *SemanticRelease) GetCIProvider() (*ci.ProviderConfig, error) { + return ci.GetCIProvider(s.gitutil, ci.ReadAllEnvs()) +} + +// GetNextVersion from .version or calculate new from commits +func (s *SemanticRelease) GetNextVersion(provider *ci.ProviderConfig, force bool) (*shared.ReleaseVersion, map[analyzer.Release][]analyzer.AnalyzedCommit, error) { + log.Debugf("Ignore .version file if exits, %t", force) + if !force { + releaseVersion, err := cache.Read(s.repository) + if err != nil { + return nil, nil, err + } + + if releaseVersion.Next.Commit == provider.Commit && releaseVersion != nil { + return releaseVersion, nil, nil + } + } + + lastVersion, lastVersionHash, err := s.gitutil.GetLastVersion() + if err != nil { + return nil, nil, err + } + + firstRelease := false + if lastVersion == nil { defaultVersion, _ := semver.NewVersion("1.0.0") - SetVersion(defaultVersion.String(), repro) - fmt.Printf(defaultVersion.String()) - return nil + lastVersion = defaultVersion + firstRelease = true } - commits, err := util.GetCommits(lastVersionHash) + commits, err := s.gitutil.GetCommits(lastVersionHash) if err != nil { - return err + return nil, nil, err } log.Debugf("Found %d commits till last release", len(commits)) - a := analyzer.New("angular") - result := a.Analyze(commits) + analyzedCommits := s.analyzer.Analyze(commits) + isDraft := false var newVersion semver.Version - - if len(result["major"]) > 0 { - newVersion = lastVersion.IncMajor() - return nil - } else if len(result["minor"]) > 0 { - newVersion = lastVersion.IncMinor() - } else if len(result["patch"]) > 0 { - newVersion = lastVersion.IncPatch() + for branch, releaseType := range s.config.Branch { + if provider.Branch == branch || strings.HasPrefix(provider.Branch, branch) { + log.Debugf("Found branch config for branch %s with release type %s", provider.Branch, releaseType) + newVersion, isDraft = s.calculator.CalculateNewVersion(analyzedCommits, lastVersion, releaseType, firstRelease) + break + } } - SetVersion(newVersion.String(), repro) - fmt.Printf(newVersion.String()) + releaseVersion := shared.ReleaseVersion{ + Next: shared.ReleaseVersionEntry{ + Commit: provider.Commit, + Version: &newVersion, + }, + Last: shared.ReleaseVersionEntry{ + Commit: lastVersionHash, + Version: lastVersion, + }, + Branch: provider.Branch, + Draft: isDraft, + } - return err + log.Infof("New version %s -> %s", lastVersion.String(), newVersion.String()) + err = cache.Write(s.repository, releaseVersion) + if err != nil { + return nil, nil, err + } + return &releaseVersion, analyzedCommits, err } -func SetVersion(version string, repro string) error { - - util, err := gitutil.New(repro) - if err != nil { - return err - } +//SetVersion for git repository +func (s *SemanticRelease) SetVersion(provider *ci.ProviderConfig, version string) error { newVersion, err := semver.NewVersion(version) if err != nil { return err } - hash, err := util.GetHash() + lastVersion, lastVersionHash, err := s.gitutil.GetLastVersion() if err != nil { return err } - - branch, err := util.GetBranch() - if err != nil { - return err + if lastVersion == nil { + lastVersion, _ = semver.NewVersion("1.0.0") } - newVersionContent := storage.VersionFileContent{ - Commit: hash, - NextVersion: newVersion.String(), - Branch: branch, - } - - lastVersion, _, err := util.GetLastVersion() - if err != nil { - return err - } - - if lastVersion != nil { - newVersionContent.Version = lastVersion.String() - } - - return storage.Write(newVersionContent) + return cache.Write(s.repository, shared.ReleaseVersion{ + Next: shared.ReleaseVersionEntry{ + Commit: provider.Commit, + Version: newVersion, + }, + Last: shared.ReleaseVersionEntry{ + Commit: lastVersionHash, + Version: lastVersion, + }, + Branch: provider.Branch, + }) } -func Release() { +// GetChangelog from last version till now +func (s *SemanticRelease) GetChangelog(analyzedCommits map[analyzer.Release][]analyzer.AnalyzedCommit, releaseVersion *shared.ReleaseVersion) (*shared.GeneratedChangelog, error) { + c := changelog.New(s.config, s.analyzer.GetRules(), time.Now()) + return c.GenerateChanglog(shared.ChangelogTemplateConfig{ + Version: releaseVersion.Next.Version.String(), + Hash: releaseVersion.Last.Commit, + CommitURL: s.releaser.GetCommitURL(), + CompareURL: s.releaser.GetCompareURL(releaseVersion.Last.Version.String(), releaseVersion.Next.Version.String()), + }, analyzedCommits) + +} + +// WriteChangeLog wirtes changelog content to the given file +func (s *SemanticRelease) WriteChangeLog(changelogContent, file string) error { + return ioutil.WriteFile(file, []byte(changelogContent), 0644) +} + +// Release pusblish release to provider +func (s *SemanticRelease) Release(provider *ci.ProviderConfig, force bool) error { + + if provider.IsPR { + log.Debugf("Will not perform a new release. This is a pull request") + return nil + } + + if _, ok := s.config.Branch[provider.Branch]; !ok { + log.Debugf("Will not perform a new release. Current %s branch is not configured in release config", provider.Branch) + return nil + } + + releaseVersion, analyzedCommits, err := s.GetNextVersion(provider, force) + if err != nil { + log.Debugf("Could not get next version") + return err + } + + if releaseVersion.Next.Version.Equal(releaseVersion.Next.Version) { + log.Infof("No new version, no release needed") + return nil + } + + generatedChanglog, err := s.GetChangelog(analyzedCommits, releaseVersion) + if err != nil { + log.Debugf("Could not get changelog") + return err + } + + releaser, err := releaser.New(s.config).GetReleaser() + if err != nil { + return err + } + + err = releaser.ValidateConfig() + if err != nil { + return err + } + + if err = releaser.CreateRelease(releaseVersion, generatedChanglog); err != nil { + return err + } + + if err = releaser.UploadAssets(s.repository, s.config.Assets); err != nil { + return err + } + + return nil +} + +// ZipFiles zip files configured in release config +func (s *SemanticRelease) ZipFiles() error { + for _, file := range s.config.Assets { + if file.Compress { + if _, err := util.PrepareAssets(s.repository, s.config.Assets); err != nil { + return err + } + } + } + return nil }