Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

changelog-gen to generate changelog entries for local setup #13

Merged
merged 14 commits into from
Apr 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions cmd/changelog-entry/README.md
Original file line number Diff line number Diff line change
@@ -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.
3 changes: 3 additions & 0 deletions cmd/changelog-entry/changelog-entry.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:{{.Type}}
{{if ne .Subcategory ""}}{{.Subcategory}}: {{end}}{{.Description}}{{if ne .URL ""}} [GH-{{.PR}}]({{.URL}}){{end}}
```
241 changes: 241 additions & 0 deletions cmd/changelog-entry/main.go
Original file line number Diff line number Diff line change
@@ -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")
}
12 changes: 1 addition & 11 deletions cmd/changelog-pr-body-check/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
25 changes: 23 additions & 2 deletions entry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -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
)
Loading