1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-26 04:22:05 +02:00

feat: changelog from github (#2548)

* feat: changelog from github

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* refactor: unifying client mocks

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* refactor: unifying client mocks

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* fix: mocks

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* test: added tests

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* fix: remove unused code

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* test: added more

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* fix: testdata

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* fix: fmt

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* test: fix

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* test: fix

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>

* docs: improve docs

Signed-off-by: Carlos A Becker <caarlos0@gmail.com>
This commit is contained in:
Carlos Alexandro Becker 2021-10-04 09:32:30 -03:00 committed by GitHub
parent c739724f12
commit 89e5a4ebf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 313 additions and 54 deletions

View File

@ -35,6 +35,7 @@ checksum:
changelog:
sort: asc
use: github
filters:
exclude:
- '^docs:'

View File

@ -12,6 +12,9 @@ import (
"github.com/goreleaser/goreleaser/pkg/context"
)
// ErrNotImplemented is returned when a client does not implement certain feature.
var ErrNotImplemented = fmt.Errorf("not implemented")
// Info of the repository.
type Info struct {
Description string
@ -40,6 +43,7 @@ type Client interface {
CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo Repo, content []byte, path, message string) (err error)
Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) (err error)
GetDefaultBranch(ctx *context.Context, repo Repo) (string, error)
Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error)
}
// New creates a new client depending on the token type.
@ -56,8 +60,6 @@ func newWithToken(ctx *context.Context, token string) (Client, error) {
return NewGitLab(ctx, token)
case context.TokenTypeGitea:
return NewGitea(ctx, token)
case context.TokenTypeMock:
return NewMock(), nil
default:
return nil, fmt.Errorf("invalid client token type: %q", ctx.TokenType)
}

View File

@ -66,6 +66,10 @@ func NewGitea(ctx *context.Context, token string) (Client, error) {
return &giteaClient{client: client}, nil
}
func (c *giteaClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
return "", ErrNotImplemented
}
// CloseMilestone closes a given milestone.
func (c *giteaClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
closedState := gitea.StateClosed

View File

@ -601,3 +601,30 @@ func TestGiteaGetDefaultBranchErr(t *testing.T) {
_, err = client.GetDefaultBranch(ctx, repo)
require.Error(t, err)
}
func TestGiteaChangelog(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if strings.HasSuffix(r.URL.Path, "api/v1/version") {
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "{\"version\":\"1.12.0\"}")
}
}))
defer srv.Close()
ctx := context.New(config.Project{
GiteaURLs: config.GiteaURLs{
API: srv.URL,
},
})
client, err := NewGitea(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
Branch: "somebranch",
}
_, err = client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
require.EqualError(t, err, ErrNotImplemented.Error())
}

View File

@ -8,6 +8,7 @@ import (
"os"
"reflect"
"strconv"
"strings"
"github.com/apex/log"
"github.com/google/go-github/v35/github"
@ -24,6 +25,12 @@ type githubClient struct {
client *github.Client
}
// NewUnauthenticatedGitHub returns a github client that is not authenticated.
// Used in tests only.
func NewUnauthenticatedGitHub() Client {
return &githubClient{client: github.NewClient(nil)}
}
// NewGitHub returns a github client implementation.
func NewGitHub(ctx *context.Context, token string) (Client, error) {
ts := oauth2.StaticTokenSource(
@ -51,6 +58,23 @@ func NewGitHub(ctx *context.Context, token string) (Client, error) {
return &githubClient{client: client}, nil
}
func (c *githubClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
result, _, err := c.client.Repositories.CompareCommits(ctx, repo.Owner, repo.Name, prev, current)
if err != nil {
return "", err
}
var log []string
for _, commit := range result.Commits {
log = append(log, fmt.Sprintf(
"- %s: %s (@%s)",
commit.GetSHA(),
strings.Split(commit.Commit.GetMessage(), "\n")[0],
commit.GetAuthor().GetLogin(),
))
}
return strings.Join(log, "\n"), nil
}
// GetDefaultBranch returns the default branch of a github repo
func (c *githubClient) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
p, res, err := c.client.Repositories.Get(ctx, repo.Owner, repo.Name)

View File

@ -2,8 +2,10 @@ package client
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
"text/template"
@ -14,6 +16,17 @@ import (
)
func TestNewGitHubClient(t *testing.T) {
t.Run("unauthenticated", func(t *testing.T) {
ctx := context.New(config.Project{})
repo := Repo{
Owner: "goreleaser",
Name: "goreleaser",
}
b, err := NewUnauthenticatedGitHub().GetDefaultBranch(ctx, repo)
require.NoError(t, err)
require.Equal(t, "master", b)
})
t.Run("good urls", func(t *testing.T) {
githubURL := "https://github.mycompany.com"
ctx := context.New(config.Project{
@ -207,7 +220,7 @@ func TestGithubGetDefaultBranch(t *testing.T) {
// Assume the request to create a branch was good
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "{}")
fmt.Fprint(w, `{"default_branch": "main"}`)
}))
defer srv.Close()
@ -216,6 +229,7 @@ func TestGithubGetDefaultBranch(t *testing.T) {
API: srv.URL + "/",
},
})
client, err := NewGitHub(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
@ -224,8 +238,9 @@ func TestGithubGetDefaultBranch(t *testing.T) {
Branch: "somebranch",
}
_, err = client.GetDefaultBranch(ctx, repo)
b, err := client.GetDefaultBranch(ctx, repo)
require.NoError(t, err)
require.Equal(t, "main", b)
require.Equal(t, 1, totalRequests)
}
@ -255,3 +270,35 @@ func TestGithubGetDefaultBranchErr(t *testing.T) {
_, err = client.GetDefaultBranch(ctx, repo)
require.Error(t, err)
}
func TestChangelog(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
if r.URL.Path == "/repos/someone/something/compare/v1.0.0...v1.1.0" {
r, err := os.Open("testdata/github/compare.json")
require.NoError(t, err)
_, err = io.Copy(w, r)
require.NoError(t, err)
return
}
}))
defer srv.Close()
ctx := context.New(config.Project{
GitHubURLs: config.GitHubURLs{
API: srv.URL + "/",
},
})
client, err := NewGitHub(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
Branch: "somebranch",
}
log, err := client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
require.NoError(t, err)
require.Equal(t, "- 6dcb09b5b57875f334f61aebed695e2e4193db5e: Fix all the bugs (@octocat)", log)
}

View File

@ -49,6 +49,10 @@ func NewGitLab(ctx *context.Context, token string) (Client, error) {
return &gitlabClient{client: client}, nil
}
func (c *gitlabClient) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
return "", ErrNotImplemented
}
// GetDefaultBranch get the default branch
func (c *gitlabClient) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
projectID := repo.String()

View File

@ -380,3 +380,26 @@ func TestGitlabGetDefaultBranchErr(t *testing.T) {
_, err = client.GetDefaultBranch(ctx, repo)
require.Error(t, err)
}
func TestGitlabChangelog(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
}))
defer srv.Close()
ctx := context.New(config.Project{
GitLabURLs: config.GitLabURLs{
API: srv.URL,
},
})
client, err := NewGitLab(ctx, "test-token")
require.NoError(t, err)
repo := Repo{
Owner: "someone",
Name: "something",
Branch: "somebranch",
}
_, err = client.Changelog(ctx, repo, "v1.0.0", "v1.1.0")
require.EqualError(t, err, ErrNotImplemented.Error())
}

View File

@ -30,12 +30,16 @@ type Mock struct {
Lock sync.Mutex
}
func (c *Mock) Changelog(ctx *context.Context, repo Repo, prev, current string) (string, error) {
return "", ErrNotImplemented
}
func (c *Mock) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
return nil
}
func (c *Mock) GetDefaultBranch(ctx *context.Context, repo Repo) (string, error) {
return "", errors.New("Mock does not yet implement GetDefaultBranch")
return "", ErrNotImplemented
}
func (c *Mock) CreateRelease(ctx *context.Context, body string) (string, error) {

View File

@ -0,0 +1,13 @@
{
"commits": [
{
"sha": "6dcb09b5b57875f334f61aebed695e2e4193db5e",
"commit": {
"message": "Fix all the bugs\nlalalal"
},
"author": {
"login": "octocat"
}
}
]
}

View File

@ -11,6 +11,7 @@ import (
"strings"
"github.com/apex/log"
"github.com/goreleaser/goreleaser/internal/client"
"github.com/goreleaser/goreleaser/internal/git"
"github.com/goreleaser/goreleaser/internal/tmpl"
"github.com/goreleaser/goreleaser/pkg/context"
@ -106,7 +107,7 @@ func checkSortDirection(mode string) error {
}
func buildChangelog(ctx *context.Context) ([]string, error) {
log, err := getChangelog(ctx.Git.CurrentTag)
log, err := getChangelog(ctx, ctx.Git.CurrentTag)
if err != nil {
return nil, err
}
@ -161,21 +162,37 @@ func extractCommitInfo(line string) string {
return strings.Join(strings.Split(line, " ")[1:], " ")
}
func getChangelog(tag string) (string, error) {
func getChangelog(ctx *context.Context, tag string) (string, error) {
prev, err := previous(tag)
if err != nil {
return "", err
}
if isSHA1(prev) {
return gitLog(prev, tag)
}
return gitLog(fmt.Sprintf("tags/%s..tags/%s", prev, tag))
return doGetChangelog(ctx, prev, tag)
}
func gitLog(refs ...string) (string, error) {
args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
args = append(args, refs...)
return git.Run(args...)
func doGetChangelog(ctx *context.Context, prev, tag string) (string, error) {
l, err := getChangeloger(ctx)
if err != nil {
return "", err
}
return l.Log(ctx, prev, tag)
}
func getChangeloger(ctx *context.Context) (changeloger, error) {
switch ctx.Config.Changelog.Use {
case "git":
fallthrough
case "":
return gitChangeloger{}, nil
case "github":
client, err := client.New(ctx)
if err != nil {
return nil, err
}
return &scmChangeloger{client: client}, nil
default:
return nil, fmt.Errorf("invalid changelog.use: %q", ctx.Config.Changelog.Use)
}
}
func previous(tag string) (result string, err error) {
@ -190,13 +207,6 @@ func previous(tag string) (result string, err error) {
return
}
var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
// isSHA1 te lets us know if the ref is a SHA1 or not.
func isSHA1(ref string) bool {
return validSHA1.MatchString(ref)
}
func loadContent(ctx *context.Context, fileName, tmplName string) (string, error) {
if tmplName != "" {
log.Debugf("loading template %s", tmplName)
@ -214,3 +224,36 @@ func loadContent(ctx *context.Context, fileName, tmplName string) (string, error
return "", nil
}
type changeloger interface {
Log(ctx *context.Context, prev, current string) (string, error)
}
type gitChangeloger struct{}
var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
func (g gitChangeloger) Log(ctx *context.Context, prev, current string) (string, error) {
args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
if validSHA1.MatchString(prev) {
args = append(args, prev, current)
} else {
args = append(args, fmt.Sprintf("tags/%s..tags/%s", prev, current))
}
return git.Run(args...)
}
type scmChangeloger struct {
client client.Client
}
func (c *scmChangeloger) Log(ctx *context.Context, prev, current string) (string, error) {
repo, err := git.ExtractRepoFromConfig()
if err != nil {
return "", err
}
return c.client.Changelog(ctx, client.Repo{
Owner: repo.Owner,
Name: repo.Name,
}, prev, current)
}

View File

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/goreleaser/goreleaser/internal/client"
"github.com/goreleaser/goreleaser/internal/testlib"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
@ -103,8 +104,11 @@ func TestChangelogPreviousTagEnv(t *testing.T) {
testlib.GitCommit(t, "third")
testlib.GitTag(t, "v0.0.3")
ctx := context.New(config.Project{
Dist: folder,
Changelog: config.Changelog{Filters: config.Filters{}},
Dist: folder,
Changelog: config.Changelog{
Use: "git",
Filters: config.Filters{},
},
})
ctx.Git.CurrentTag = "v0.0.3"
require.NoError(t, os.Setenv("GORELEASER_PREVIOUS_TAG", "v0.0.1"))
@ -426,6 +430,59 @@ func TestChangeLogWithoutReleaseFooter(t *testing.T) {
require.Equal(t, rune(ctx.ReleaseNotes[len(ctx.ReleaseNotes)-1]), '\n')
}
func TestGetChangelogGitHub(t *testing.T) {
ctx := context.New(config.Project{
Changelog: config.Changelog{
Use: "github",
},
})
l := scmChangeloger{client: client.NewUnauthenticatedGitHub()}
log, err := l.Log(ctx, "v0.180.1", "v0.180.2")
require.NoError(t, err)
require.Equal(t, "- c90f1085f255d0af0b055160bfff5ee40f47af79: fix: do not skip any defaults (#2521) (@caarlos0)", log)
}
func TestGetChangeloger(t *testing.T) {
t.Run("default", func(t *testing.T) {
c, err := getChangeloger(context.New(config.Project{}))
require.NoError(t, err)
require.IsType(t, c, gitChangeloger{})
})
t.Run("git", func(t *testing.T) {
c, err := getChangeloger(context.New(config.Project{
Changelog: config.Changelog{
Use: "git",
},
}))
require.NoError(t, err)
require.IsType(t, c, gitChangeloger{})
})
t.Run("gituhb", func(t *testing.T) {
ctx := context.New(config.Project{
Changelog: config.Changelog{
Use: "github",
},
})
ctx.TokenType = context.TokenTypeGitHub
c, err := getChangeloger(ctx)
require.NoError(t, err)
require.IsType(t, c, &scmChangeloger{})
})
t.Run("invalid", func(t *testing.T) {
c, err := getChangeloger(context.New(config.Project{
Changelog: config.Changelog{
Use: "nope",
},
}))
require.EqualError(t, err, `invalid changelog.use: "nope"`)
require.Nil(t, c)
})
}
func TestSkip(t *testing.T) {
t.Run("skip on snapshot", func(t *testing.T) {
ctx := context.New(config.Project{})

View File

@ -242,3 +242,7 @@ func (c *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.Commi
func (c *DummyClient) Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) error {
return nil
}
func (c *DummyClient) Changelog(ctx *context.Context, repo client.Repo, prev, current string) (string, error) {
return "", errors.New("not implemented")
}

View File

@ -595,6 +595,7 @@ type Changelog struct {
Filters Filters `yaml:",omitempty"`
Sort string `yaml:",omitempty"`
Skip bool `yaml:",omitempty"` // TODO(caarlos0): rename to Disable to match other pipes
Use string `yaml:",omitempty"`
}
// EnvFiles holds paths to files that contains environment variables

View File

@ -59,8 +59,6 @@ const (
TokenTypeGitLab TokenType = "gitlab"
// TokenTypeGitea defines gitea as type of the token.
TokenTypeGitea TokenType = "gitea"
// TokenTypeMock is a mock token type used in tests.
TokenTypeMock = "mock"
)
// Context carries along some data through the pipes.

View File

@ -0,0 +1,34 @@
# Changelog
You can customize how the changelog is generated using the `changelog` section in the config file:
```yaml
# .goreleaser.yml
changelog:
# Set it to true if you wish to skip the changelog generation.
# This may result in an empty release notes on GitHub/GitLab/Gitea.
skip: true
# Changelog generation implementation to use.
#
# Valid options are:
# - `git`: uses `git log`;
# - `github`: uses the compare GitHub API, appending the author login to the changelog.
#
# Defaults to `git`.
use: github
# Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty
# Default is empty
sort: asc
filters:
# Commit messages matching the regexp listed here will be removed from
# the changelog
# Default is empty
exclude:
- '^docs:'
- typo
- (?i)foo
```

View File

@ -182,34 +182,6 @@ ALLOWED_TYPES = application/gzip|application/x-gzip|application/x-gtar|applicati
!!! warning
`draft` and `prerelease` are only supported by GitHub and Gitea.
## Customize the changelog
You can customize how the changelog is generated using the
`changelog` section in the config file:
```yaml
# .goreleaser.yml
changelog:
# Set it to true if you wish to skip the changelog generation.
# This may result in an empty release notes on GitHub/GitLab/Gitea.
skip: true
# Sorts the changelog by the commit's messages.
# Could either be asc, desc or empty
# Default is empty
sort: asc
filters:
# Commit messages matching the regexp listed here will be removed from
# the changelog
# Default is empty
exclude:
- '^docs:'
- typo
- (?i)foo
```
### Define Previous Tag
GoReleaser uses `git describe` to get the previous tag used for generating the Changelog.

View File

@ -96,6 +96,7 @@ nav:
- customization/homebrew.md
- customization/gofish.md
- customization/scoop.md
- customization/changelog.md
- customization/release.md
- customization/artifactory.md
- customization/publishers.md