1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-01-10 03:47:03 +02:00
goreleaser/internal/pipe/changelog/changelog.go

415 lines
9.9 KiB
Go
Raw Normal View History

// Package changelog provides the release changelog to goreleaser.
package changelog
import (
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/caarlos0/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"
)
// ErrInvalidSortDirection happens when the sort order is invalid.
var ErrInvalidSortDirection = errors.New("invalid sort direction")
const li = "* "
type useChangelog string
func (u useChangelog) formatable() bool {
return u != "github-native"
}
const (
useGit = "git"
useGitHub = "github"
useGitLab = "gitlab"
useGitHubNative = "github-native"
)
// Pipe for checksums.
type Pipe struct{}
feat: improve output and pipe skipping (#2480) * refactor: improve middleware Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: upload tests Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: twitter tests Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: source tests Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: snapshot tests Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * test: improved some tests Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: snapcraft skip Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip slack Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip sign Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip scoop Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip reddit Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip discord Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip publish Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip nfpm Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip milestone Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip custompublishers Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip checksums Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip changelog Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip brew Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip blob Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip before Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip artifactory Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip announce Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip defaults Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: cmds Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip docker Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * chore: todo Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: go.mod Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip release Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: remove old skip pipe errors Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip teams Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip brew Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix/test: skip smtp Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: lint issues Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip docker Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip brew and scoop Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip docker Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * fix: skip http/artifactory Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * test: increase coverage Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com> * test: fix Signed-off-by: Carlos Alexandro Becker <caarlos0@gmail.com>
2021-09-18 15:21:29 +02:00
func (Pipe) String() string { return "generating changelog" }
func (Pipe) Skip(ctx *context.Context) bool { return ctx.Config.Changelog.Skip || ctx.Snapshot }
// Run the pipe.
func (Pipe) Run(ctx *context.Context) error {
notes, err := loadContent(ctx, ctx.ReleaseNotesFile, ctx.ReleaseNotesTmpl)
if err != nil {
return err
}
ctx.ReleaseNotes = notes
if ctx.ReleaseNotesFile != "" || ctx.ReleaseNotesTmpl != "" {
return nil
}
footer, err := loadContent(ctx, ctx.ReleaseFooterFile, ctx.ReleaseFooterTmpl)
if err != nil {
return err
}
header, err := loadContent(ctx, ctx.ReleaseHeaderFile, ctx.ReleaseHeaderTmpl)
if err != nil {
return err
}
if err := checkSortDirection(ctx.Config.Changelog.Sort); err != nil {
return err
}
entries, err := buildChangelog(ctx)
if err != nil {
return err
}
feat: add gitlab for releases (#1038) * outlines gitlab client integration * makes client parameter more explicit * adds gitlab url to config * changes releaseID to string to adapt to gitlab * updates to latest gitlab client lib 0.18 * fixes copy paster in gitlab upload func * fixes gitlab typo in config * adds gitlab token to env and context * release now uses the client factory method * skips brew pipe if it is not a github release * add github tokentype to publish tests * skips scoop pipe if it is not a github release * corrects brew skip msg * adds gitlab token to main test * adds gitlab to release docs * validates config and errors accordingly * adapt release pipe name to include gitlab * fixes gitlab client after testing * moves not-configured brew and scoop pipe checks as first check * adds more debug to gitlab client * adapts changelog generation for gitlab markdown * adds debug log for gitlab changelog * env needs to run before changelog pipe * moves gitlab default download url to default pipe * moves multiple releases check to from config to release pipe * release differs now for github and gitlab * adds debug gitlab release update msgs * moves env pipe as second after before because it determines the token type other pipes depend on * adaptes error check on gitlab release creation * Revert "adaptes error check on gitlab release creation" This reverts commit 032024571c76140f8e2207ee01cc08088f37594b. * simplifies gitlab client logic. removes comments * skips tls verification for gitlab client if specified in config * updates the docs * adds clarification that brew and scoop are not supported if it is a gitlab release * fixes copy paster in release.md * adds missing blob pipe in defaults and publish due to missing in merge * updates comment in gitlab client
2019-06-29 16:02:40 +02:00
changes, err := formatChangelog(ctx, entries)
if err != nil {
return err
}
changelogElements := []string{changes}
if header != "" {
changelogElements = append([]string{header}, changelogElements...)
}
if footer != "" {
changelogElements = append(changelogElements, footer)
}
ctx.ReleaseNotes = strings.Join(changelogElements, "\n\n")
if !strings.HasSuffix(ctx.ReleaseNotes, "\n") {
ctx.ReleaseNotes += "\n"
}
path := filepath.Join(ctx.Config.Dist, "CHANGELOG.md")
log.WithField("changelog", path).Info("writing")
return os.WriteFile(path, []byte(ctx.ReleaseNotes), 0o644) //nolint: gosec
}
type changelogGroup struct {
title string
entries []string
order int
}
func formatChangelog(ctx *context.Context, entries []string) (string, error) {
newLine := "\n"
if ctx.TokenType == context.TokenTypeGitLab || ctx.TokenType == context.TokenTypeGitea {
// We need two or more whitespace to let markdown interpret
// it as newline. See https://docs.gitlab.com/ee/user/markdown.html#newlines for details
log.Debug("is gitlab or gitea changelog")
newLine = " \n"
}
if !useChangelog(ctx.Config.Changelog.Use).formatable() {
return strings.Join(entries, newLine), nil
}
for i := range entries {
entry := entries[i]
abbr := ctx.Config.Changelog.Abbrev
switch abbr {
case 0:
continue
case -1:
_, rest, _ := strings.Cut(entry, " ")
entries[i] = rest
default:
commit, rest, _ := strings.Cut(entry, " ")
if abbr > len(commit) {
continue
}
entries[i] = fmt.Sprintf("%s %s", commit[:abbr], rest)
}
}
result := []string{"## Changelog"}
if len(ctx.Config.Changelog.Groups) == 0 {
log.Debug("not grouping entries")
return strings.Join(append(result, filterAndPrefixItems(entries)...), newLine), nil
}
log.Debug("grouping entries")
var groups []changelogGroup
for _, group := range ctx.Config.Changelog.Groups {
item := changelogGroup{
title: group.Title,
order: group.Order,
}
if group.Regexp == "" {
// If no regexp is provided, we purge all strikethrough entries and add remaining entries to the list
item.entries = filterAndPrefixItems(entries)
// clear array
entries = nil
} else {
regex, err := regexp.Compile(group.Regexp)
if err != nil {
return "", fmt.Errorf("failed to group into %q: %w", group.Title, err)
}
log.Debugf("group: %#v", group)
i := 0
for _, entry := range entries {
match := regex.MatchString(entry)
log.Debugf("entry: %s match: %b\n", entry, match)
if match {
item.entries = append(item.entries, li+entry)
} else {
// Keep unmatched entry.
entries[i] = entry
i++
}
}
entries = entries[:i]
}
groups = append(groups, item)
if len(entries) == 0 {
break // No more entries to process.
}
}
sort.Slice(groups, func(i, j int) bool { return groups[i].order < groups[j].order })
for _, group := range groups {
if len(group.entries) > 0 {
result = append(result, fmt.Sprintf("\n### %s", group.title))
result = append(result, group.entries...)
}
}
return strings.Join(result, newLine), nil
}
func filterAndPrefixItems(ss []string) []string {
var r []string
for _, s := range ss {
if s != "" {
r = append(r, li+s)
}
}
return r
}
func loadFromFile(file string) (string, error) {
bts, err := os.ReadFile(file)
if err != nil {
return "", err
}
log.WithField("file", file).Debugf("read %d bytes", len(bts))
return string(bts), nil
}
func checkSortDirection(mode string) error {
switch mode {
2022-12-31 03:57:24 +02:00
case "", "asc", "desc":
return nil
2022-12-31 03:57:24 +02:00
default:
return ErrInvalidSortDirection
}
}
func buildChangelog(ctx *context.Context) ([]string, error) {
l, err := getChangeloger(ctx)
if err != nil {
return nil, err
}
log, err := l.Log(ctx)
if err != nil {
return nil, err
}
entries := strings.Split(log, "\n")
if lastLine := entries[len(entries)-1]; strings.TrimSpace(lastLine) == "" {
entries = entries[0 : len(entries)-1]
}
if !useChangelog(ctx.Config.Changelog.Use).formatable() {
return entries, nil
}
entries, err = filterEntries(ctx, entries)
if err != nil {
return entries, err
}
return sortEntries(ctx, entries), nil
}
func filterEntries(ctx *context.Context, entries []string) ([]string, error) {
for _, filter := range ctx.Config.Changelog.Filters.Exclude {
r, err := regexp.Compile(filter)
if err != nil {
return entries, err
}
entries = remove(r, entries)
}
return entries, nil
}
func sortEntries(ctx *context.Context, entries []string) []string {
direction := ctx.Config.Changelog.Sort
if direction == "" {
return entries
}
result := make([]string, len(entries))
copy(result, entries)
sort.Slice(result, func(i, j int) bool {
imsg := extractCommitInfo(result[i])
jmsg := extractCommitInfo(result[j])
if direction == "asc" {
return strings.Compare(imsg, jmsg) < 0
}
return strings.Compare(imsg, jmsg) > 0
})
return result
}
func remove(filter *regexp.Regexp, entries []string) (result []string) {
for _, entry := range entries {
if !filter.MatchString(extractCommitInfo(entry)) {
result = append(result, entry)
}
}
return result
}
func extractCommitInfo(line string) string {
return strings.Join(strings.Split(line, " ")[1:], " ")
}
func getChangeloger(ctx *context.Context) (changeloger, error) {
switch ctx.Config.Changelog.Use {
case useGit:
fallthrough
case "":
return gitChangeloger{}, nil
case useGitHub:
2021-11-06 21:58:07 +02:00
fallthrough
case useGitLab:
2021-11-06 21:58:07 +02:00
return newSCMChangeloger(ctx)
case useGitHubNative:
return newGithubChangeloger(ctx)
default:
return nil, fmt.Errorf("invalid changelog.use: %q", ctx.Config.Changelog.Use)
}
}
func newGithubChangeloger(ctx *context.Context) (changeloger, error) {
cli, err := client.NewGitHub(ctx, ctx.Token)
if err != nil {
return nil, err
}
repo, err := git.ExtractRepoFromConfig(ctx)
if err != nil {
return nil, err
}
if err := repo.CheckSCM(); err != nil {
return nil, err
}
return &githubNativeChangeloger{
client: cli,
repo: client.Repo{
Owner: repo.Owner,
Name: repo.Name,
},
}, nil
}
2021-11-06 21:58:07 +02:00
func newSCMChangeloger(ctx *context.Context) (changeloger, error) {
cli, err := client.New(ctx)
if err != nil {
return nil, err
}
repo, err := git.ExtractRepoFromConfig(ctx)
if err != nil {
return nil, err
}
if err := repo.CheckSCM(); err != nil {
return nil, err
}
return &scmChangeloger{
client: cli,
repo: client.Repo{
Owner: repo.Owner,
Name: repo.Name,
},
}, nil
}
func loadContent(ctx *context.Context, fileName, tmplName string) (string, error) {
if tmplName != "" {
log.Debugf("loading template %q", tmplName)
content, err := loadFromFile(tmplName)
if err != nil {
return "", err
}
content, err = tmpl.New(ctx).Apply(content)
if strings.TrimSpace(content) == "" && err == nil {
log.Warnf("loaded %q, but it evaluates to an empty string", tmplName)
}
return content, err
}
if fileName != "" {
log.Debugf("loading file %q", fileName)
content, err := loadFromFile(fileName)
if strings.TrimSpace(content) == "" && err == nil {
log.Warnf("loaded %q, but it is empty", fileName)
}
return content, err
}
return "", nil
}
type changeloger interface {
Log(ctx *context.Context) (string, error)
}
type gitChangeloger struct{}
var validSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
func (g gitChangeloger) Log(ctx *context.Context) (string, error) {
args := []string{"log", "--pretty=oneline", "--abbrev-commit", "--no-decorate", "--no-color"}
prev, current := comparePair(ctx)
if validSHA1.MatchString(prev) {
args = append(args, prev, current)
} else {
args = append(args, fmt.Sprintf("tags/%s..tags/%s", ctx.Git.PreviousTag, ctx.Git.CurrentTag))
}
return git.Run(ctx, args...)
}
type scmChangeloger struct {
client client.Client
repo client.Repo
}
func (c *scmChangeloger) Log(ctx *context.Context) (string, error) {
prev, current := comparePair(ctx)
return c.client.Changelog(ctx, c.repo, prev, current)
}
type githubNativeChangeloger struct {
client client.GitHubClient
repo client.Repo
}
func (c *githubNativeChangeloger) Log(ctx *context.Context) (string, error) {
return c.client.GenerateReleaseNotes(ctx, c.repo, ctx.Git.PreviousTag, ctx.Git.CurrentTag)
}
func comparePair(ctx *context.Context) (prev string, current string) {
prev = ctx.Git.PreviousTag
current = ctx.Git.CurrentTag
if prev == "" {
prev = ctx.Git.FirstCommit
}
return
}