diff --git a/cmd/changelog-entry/README.md b/cmd/changelog-entry/README.md new file mode 100644 index 0000000..902bd07 --- /dev/null +++ b/cmd/changelog-entry/README.md @@ -0,0 +1,33 @@ +# changelog-entry + +`changelog-entry` is a command that will generate a changelog entry based on the information passed and the information retrieved from the Github repository. + +The default changelog entry template is embedded from [`changelog-entry.tmpl`](changelog-entry.tmpl) but a path to a custom template can also can be passed as parameter. + +The type parameter can be one of the following: +* bug +* note +* enhancement +* new-resource +* new-datasource +* deprecation +* breaking-change +* feature + +## Usage + +```sh +$ changelog-entry -type improvement -subcategory monitoring -description "optimize the monitoring endpoint to avoid losing logs when under high load" +``` + +If parameters are missing the command will prompt to fill them, the pull request number is optional and if not provided the command will try to guess it based on the current branch name and remote if the current directory is in a git repository. + +## Output + +``````markdown +```release-note:improvement +monitoring: optimize the monitoring endpoint to avoid losing logs when under high load +``` +`````` + +Any failures will be logged to stderr. The entry will be written to a file named `{PR_NUMBER}.txt`, in the current directory unless an output directory is specified. diff --git a/cmd/changelog-entry/changelog-entry.tmpl b/cmd/changelog-entry/changelog-entry.tmpl new file mode 100644 index 0000000..cfca419 --- /dev/null +++ b/cmd/changelog-entry/changelog-entry.tmpl @@ -0,0 +1,3 @@ +```release-note:{{.Type}} +{{if ne .Subcategory ""}}{{.Subcategory}}: {{end}}{{.Description}}{{if ne .URL ""}} [GH-{{.PR}}]({{.URL}}){{end}} +``` diff --git a/cmd/changelog-entry/main.go b/cmd/changelog-entry/main.go new file mode 100644 index 0000000..a304fa8 --- /dev/null +++ b/cmd/changelog-entry/main.go @@ -0,0 +1,241 @@ +package main + +import ( + "bytes" + "context" + _ "embed" + "errors" + "flag" + "fmt" + "github.com/go-git/go-git/v5" + "github.com/google/go-github/github" + "github.com/hashicorp/go-changelog" + "github.com/manifoldco/promptui" + "os" + "path" + "regexp" + "strings" + "text/template" +) + +//go:embed changelog-entry.tmpl +var changelogTmplDefault string + +type Note struct { + // service or area of codebase the pull request changes + Subcategory string + // release note type (Bug...) + Type string + // release note text + Description string + // pull request number + PR int + // URL of the pull request + URL string +} + +func main() { + pwd, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + var subcategory, changeType, description, changelogTmpl, dir, url string + var pr int + var Url bool + flag.BoolVar(&Url, "add-url", false, "add GitHub issue URL (omitted by default due to formatting in changelog-build)") + flag.IntVar(&pr, "pr", -1, "pull request number") + flag.StringVar(&subcategory, "subcategory", "", "the service or area of the codebase the pull request changes (optional)") + flag.StringVar(&changeType, "type", "", "the type of change") + flag.StringVar(&description, "description", "", "the changelog entry content") + flag.StringVar(&changelogTmpl, "changelog-template", "", "the path of the file holding the template to use for the changelog entries") + flag.StringVar(&dir, "dir", "", "the relative path from the current directory of where the changelog entry file should be written") + flag.Parse() + + if pr == -1 { + pr, url, err = getPrNumberFromGithub(pwd) + if err != nil { + fmt.Fprintln(os.Stderr, "Must specify pull request number or run in a git repo with a GitHub remote origin:", err) + fmt.Fprintln(os.Stderr, "") + flag.Usage() + os.Exit(1) + } + } + fmt.Fprintln(os.Stderr, "Found matching pull request:", url) + + if changeType == "" { + prompt := promptui.Select{ + Label: "Select a change type", + Items: changelog.TypeValues, + } + + _, changeType, err = prompt.Run() + + if err != nil { + fmt.Fprintln(os.Stderr, "Must specify the change type") + fmt.Fprintln(os.Stderr, "") + flag.Usage() + os.Exit(1) + } + } else { + if !changelog.TypeValid(changeType) { + fmt.Fprintln(os.Stderr, "Must specify a valid type") + fmt.Fprintln(os.Stderr, "") + flag.Usage() + os.Exit(1) + } + } + + if subcategory == "" { + prompt := promptui.Prompt{Label: "Subcategory (optional)"} + subcategory, err = prompt.Run() + } + + if description == "" { + prompt := promptui.Prompt{Label: "Description"} + description, err = prompt.Run() + if err != nil { + fmt.Fprintln(os.Stderr, "Must specify the change description") + fmt.Fprintln(os.Stderr, "") + flag.Usage() + os.Exit(1) + } + } + + var tmpl *template.Template + if changelogTmpl != "" { + file, err := os.ReadFile(changelogTmpl) + if err != nil { + os.Exit(1) + } + tmpl, err = template.New("").Parse(string(file)) + if err != nil { + os.Exit(1) + } + } else { + tmpl, err = template.New("").Parse(changelogTmplDefault) + if err != nil { + os.Exit(1) + } + } + + if !Url { + url = "" + } + n := Note{Type: changeType, Description: description, Subcategory: subcategory, PR: pr, URL: url} + + var buf bytes.Buffer + err = tmpl.Execute(&buf, n) + fmt.Printf("\n%s\n", buf.String()) + if err != nil { + os.Exit(1) + } + filename := fmt.Sprintf("%d.txt", pr) + filepath := path.Join(pwd, dir, filename) + err = os.WriteFile(filepath, buf.Bytes(), 0644) + if err != nil { + os.Exit(1) + } + fmt.Fprintln(os.Stderr, "Created changelog entry at", filepath) +} + +func OpenGit(path string) (*git.Repository, error) { + r, err := git.PlainOpen(path) + if err != nil { + if path == "/" { + return r, err + } else { + return OpenGit(path[:strings.LastIndex(path, "/")]) + } + } + return r, err +} + +func getPrNumberFromGithub(path string) (int, string, error) { + r, err := OpenGit(path) + if err != nil { + return -1, "", err + } + + ref, err := r.Head() + if err != nil { + return -1, "", err + } + + localBranch, err := r.Branch(ref.Name().Short()) + if err != nil { + return -1, "", err + } + + remote, err := r.Remote("origin") + if err != nil { + return -1, "", err + } + + if len(remote.Config().URLs) <= 0 { + return -1, "", errors.New("not able to parse repo and org") + } + remoteUrl := remote.Config().URLs[0] + + re := regexp.MustCompile(`.*github\.com:(.*)/(.*)\.git`) + m := re.FindStringSubmatch(remoteUrl) + if len(m) < 3 { + return -1, "", errors.New("not able to parse repo and org") + } + + cli := github.NewClient(nil) + + ctx := context.Background() + + githubOrg := m[1] + githubRepo := m[2] + + opt := &github.PullRequestListOptions{ + ListOptions: github.ListOptions{PerPage: 200}, + Sort: "updated", + Direction: "desc", + } + + list, _, err := cli.PullRequests.List(ctx, githubOrg, githubRepo, opt) + if err != nil { + return -1, "", err + } + + for _, pr := range list { + head := pr.GetHead() + if head == nil { + continue + } + + branch := head.GetRef() + if branch == "" { + continue + } + + repo := head.GetRepo() + if repo == nil { + continue + } + + // Allow finding PRs from forks - localBranch.Remote will return the + // remote name for branches of origin, but the remote URL for forks + var gitRemote string + remote, err := r.Remote(localBranch.Remote) + if err != nil { + gitRemote = localBranch.Remote + } else { + gitRemote = remote.Config().URLs[0] + } + + if (gitRemote == *repo.SSHURL || gitRemote == *repo.CloneURL) && + localBranch.Name == branch { + n := pr.GetNumber() + + if n != 0 { + return n, pr.GetHTMLURL(), nil + } + } + } + + return -1, "", errors.New("no match found") +} diff --git a/cmd/changelog-pr-body-check/main.go b/cmd/changelog-pr-body-check/main.go index 48e8518..e2c701c 100644 --- a/cmd/changelog-pr-body-check/main.go +++ b/cmd/changelog-pr-body-check/main.go @@ -74,17 +74,7 @@ func main() { } var unknownTypes []string for _, note := range notes { - switch note.Type { - case "none", - "bug", - "note", - "enhancement", - "new-resource", - "new-datasource", - "deprecation", - "breaking-change", - "feature": - default: + if !changelog.TypeValid(note.Type) { unknownTypes = append(unknownTypes, note.Type) } } diff --git a/entry.go b/entry.go index 5058756..4a0a59f 100644 --- a/entry.go +++ b/entry.go @@ -16,7 +16,16 @@ import ( "github.com/go-git/go-git/v5/storage/memory" ) -// Entry is a raw changelog entry. +var TypeValues = []string{"enhancement", + "feature", + "bug", + "note", + "new-resource", + "new-datasource", + "deprecation", + "breaking-change", +} + type Entry struct { Issue string Body string @@ -130,7 +139,7 @@ func Diff(repo, ref1, ref2, dir string) (*EntryList, error) { if err := wt.Checkout(&git.CheckoutOptions{ Hash: *rev2, Force: true, - }); err != nil { + }); err != nil { return nil, fmt.Errorf("could not checkout repository at %s: %w", ref2, err) } entriesAfterFI, err := wt.Filesystem.ReadDir(dir) @@ -148,6 +157,9 @@ func Diff(repo, ref1, ref2, dir string) (*EntryList, error) { Hash: *rev1, Force: true, }) + if err != nil { + return nil, err + } entriesBeforeFI, err := wt.Filesystem.ReadDir(dir) if err != nil { return nil, fmt.Errorf("could not read repository directory %s: %w", dir, err) @@ -202,3 +214,12 @@ func Diff(repo, ref1, ref2, dir string) (*EntryList, error) { entries.SortByIssue() return entries, nil } + +func TypeValid(Type string) bool { + for _, a := range TypeValues { + if a == Type { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index 678c062..2442813 100644 --- a/go.mod +++ b/go.mod @@ -1,12 +1,13 @@ module github.com/hashicorp/go-changelog -go 1.13 +go 1.16 require ( github.com/go-git/go-billy/v5 v5.0.0 github.com/go-git/go-git/v5 v5.2.0 github.com/google/go-github v17.0.0+incompatible github.com/google/go-querystring v1.0.0 // indirect + github.com/manifoldco/promptui v0.8.0 golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d golang.org/x/sync v0.0.0-20190423024810-112230192c58 // indirect ) diff --git a/go.sum b/go.sum index 6c22976..c100de1 100644 --- a/go.sum +++ b/go.sum @@ -5,13 +5,18 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= 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/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 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.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.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -36,6 +41,8 @@ github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ 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/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd h1:Coekwdh0v2wtGp9Gmz1Ze3eVRAWJMLokvN3QjdzCHLY= github.com/kevinburke/ssh_config v0.0.0-20190725054713-01f96b0aa0cd/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -43,6 +50,14 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= +github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 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/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= @@ -72,6 +87,7 @@ golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4Iltr golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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= @@ -84,6 +100,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=