You've already forked goreleaser
mirror of
https://github.com/goreleaser/goreleaser.git
synced 2025-07-15 01:34:21 +02:00
feat: support closing milestones (#1657)
* feat: support closing milestones Reference: https://github.com/goreleaser/goreleaser/issues/1415 * refactor: Adjust milestone handling for code simplification, add ErrNoMilestoneFound, and fix milestone documentation close default Reference: https://github.com/goreleaser/goreleaser/pull/1657#pullrequestreview-445025743 * refactor: Use single repo config in milestones instead of each VCS * fix: Ensure milestone Pipe is included in Defaulters * feat: Add fail_on_error configuration to milestone configuration Co-authored-by: Radek Simko <radek.simko@gmail.com>
This commit is contained in:
@ -32,6 +32,7 @@ func (r Repo) String() string {
|
||||
|
||||
// Client interface.
|
||||
type Client interface {
|
||||
CloseMilestone(ctx *context.Context, repo Repo, title string) (err error)
|
||||
CreateRelease(ctx *context.Context, body string) (releaseID string, err error)
|
||||
ReleaseURLTemplate(ctx *context.Context) (string, error)
|
||||
CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo Repo, content []byte, path, message string) (err error)
|
||||
@ -66,6 +67,15 @@ func NewWithToken(ctx *context.Context, token string) (Client, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// ErrNoMilestoneFound is an error when no milestone is found.
|
||||
type ErrNoMilestoneFound struct {
|
||||
Title string
|
||||
}
|
||||
|
||||
func (e ErrNoMilestoneFound) Error() string {
|
||||
return fmt.Sprintf("no milestone found: %s", e.Title)
|
||||
}
|
||||
|
||||
// RetriableError is an error that will cause the action to be retried.
|
||||
type RetriableError struct {
|
||||
Err error
|
||||
|
@ -51,6 +51,32 @@ func NewGitea(ctx *context.Context, token string) (Client, error) {
|
||||
return &giteaClient{client: client}, nil
|
||||
}
|
||||
|
||||
// CloseMilestone closes a given milestone.
|
||||
func (c *giteaClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
|
||||
milestone, err := c.getMilestoneByTitle(repo, title)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if milestone == nil {
|
||||
return ErrNoMilestoneFound{Title: title}
|
||||
}
|
||||
|
||||
closedState := string(gitea.StateClosed)
|
||||
|
||||
opts := gitea.EditMilestoneOption{
|
||||
Deadline: milestone.Deadline,
|
||||
Description: &milestone.Description,
|
||||
State: &closedState,
|
||||
Title: milestone.Title,
|
||||
}
|
||||
|
||||
_, err = c.client.EditMilestone(repo.Owner, repo.Name, milestone.ID, opts)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateFile creates a file in the repository at a given path
|
||||
// or updates the file if it exists.
|
||||
func (c *giteaClient) CreateFile(
|
||||
@ -193,3 +219,21 @@ func (c *giteaClient) Upload(
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getMilestoneByTitle returns a milestone by title.
|
||||
func (c *giteaClient) getMilestoneByTitle(repo Repo, title string) (*gitea.Milestone, error) {
|
||||
// The Gitea API/SDK does not provide lookup by title functionality currently.
|
||||
milestones, err := c.client.ListRepoMilestones(repo.Owner, repo.Name, gitea.ListMilestoneOption{})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, milestone := range milestones {
|
||||
if milestone.Title == title {
|
||||
return milestone, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -56,6 +56,32 @@ func NewGitHub(ctx *context.Context, token string) (Client, error) {
|
||||
return &githubClient{client: client}, nil
|
||||
}
|
||||
|
||||
// CloseMilestone closes a given milestone.
|
||||
func (c *githubClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
|
||||
milestone, err := c.getMilestoneByTitle(ctx, repo, title)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if milestone == nil {
|
||||
return ErrNoMilestoneFound{Title: title}
|
||||
}
|
||||
|
||||
closedState := "closed"
|
||||
milestone.State = &closedState
|
||||
|
||||
_, _, err = c.client.Issues.EditMilestone(
|
||||
ctx,
|
||||
repo.Owner,
|
||||
repo.Name,
|
||||
*milestone.Number,
|
||||
milestone,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *githubClient) CreateFile(
|
||||
ctx *context.Context,
|
||||
commitAuthor config.CommitAuthor,
|
||||
@ -187,3 +213,38 @@ func (c *githubClient) Upload(
|
||||
}
|
||||
return RetriableError{err}
|
||||
}
|
||||
|
||||
// getMilestoneByTitle returns a milestone by title.
|
||||
func (c *githubClient) getMilestoneByTitle(ctx *context.Context, repo Repo, title string) (*github.Milestone, error) {
|
||||
// The GitHub API/SDK does not provide lookup by title functionality currently.
|
||||
opts := &github.MilestoneListOptions{
|
||||
ListOptions: github.ListOptions{PerPage: 100},
|
||||
}
|
||||
|
||||
for {
|
||||
milestones, resp, err := c.client.Issues.ListMilestones(
|
||||
ctx,
|
||||
repo.Owner,
|
||||
repo.Name,
|
||||
opts,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range milestones {
|
||||
if m != nil && m.Title != nil && *m.Title == title {
|
||||
return m, nil
|
||||
}
|
||||
}
|
||||
|
||||
if resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
opts.Page = resp.NextPage
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -48,6 +48,37 @@ func NewGitLab(ctx *context.Context, token string) (Client, error) {
|
||||
return &gitlabClient{client: client}, nil
|
||||
}
|
||||
|
||||
// CloseMilestone closes a given milestone.
|
||||
func (c *gitlabClient) CloseMilestone(ctx *context.Context, repo Repo, title string) error {
|
||||
milestone, err := c.getMilestoneByTitle(repo, title)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if milestone == nil {
|
||||
return ErrNoMilestoneFound{Title: title}
|
||||
}
|
||||
|
||||
closeStateEvent := "close"
|
||||
|
||||
opts := &gitlab.UpdateMilestoneOptions{
|
||||
Description: &milestone.Description,
|
||||
DueDate: milestone.DueDate,
|
||||
StartDate: milestone.StartDate,
|
||||
StateEvent: &closeStateEvent,
|
||||
Title: &milestone.Title,
|
||||
}
|
||||
|
||||
_, _, err = c.client.Milestones.UpdateMilestone(
|
||||
repo.String(),
|
||||
milestone.ID,
|
||||
opts,
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// CreateFile gets a file in the repository at a given path
|
||||
// and updates if it exists or creates it for later pipes in the pipeline.
|
||||
func (c *gitlabClient) CreateFile(
|
||||
@ -319,3 +350,32 @@ func extractProjectFileHashFrom(projectFileURL string) (string, error) {
|
||||
}).Debug("extracted file hash")
|
||||
return fileHash, nil
|
||||
}
|
||||
|
||||
// getMilestoneByTitle returns a milestone by title.
|
||||
func (c *gitlabClient) getMilestoneByTitle(repo Repo, title string) (*gitlab.Milestone, error) {
|
||||
opts := &gitlab.ListMilestonesOptions{
|
||||
Title: &title,
|
||||
}
|
||||
|
||||
for {
|
||||
milestones, resp, err := c.client.Milestones.ListMilestones(repo.String(), opts)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, milestone := range milestones {
|
||||
if milestone != nil && milestone.Title == title {
|
||||
return milestone, nil
|
||||
}
|
||||
}
|
||||
|
||||
if resp.NextPage == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
opts.Page = resp.NextPage
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
@ -1,27 +1,26 @@
|
||||
package release
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/git"
|
||||
"github.com/goreleaser/goreleaser/pkg/config"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// remoteRepo gets the repo name from the Git config.
|
||||
func remoteRepo() (result config.Repo, err error) {
|
||||
if !git.IsRepo() {
|
||||
// ExtractRepoFromConfig gets the repo name from the Git config.
|
||||
func ExtractRepoFromConfig() (result config.Repo, err error) {
|
||||
if !IsRepo() {
|
||||
return result, errors.New("current folder is not a git repository")
|
||||
}
|
||||
out, err := git.Run("config", "--get", "remote.origin.url")
|
||||
out, err := Run("config", "--get", "remote.origin.url")
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("repository doesn't have an `origin` remote")
|
||||
}
|
||||
return extractRepoFromURL(out), nil
|
||||
return ExtractRepoFromURL(out), nil
|
||||
}
|
||||
|
||||
func extractRepoFromURL(s string) config.Repo {
|
||||
func ExtractRepoFromURL(s string) config.Repo {
|
||||
// removes the .git suffix and any new lines
|
||||
s = strings.NewReplacer(
|
||||
".git", "",
|
@ -1,8 +1,9 @@
|
||||
package release
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/git"
|
||||
"github.com/goreleaser/goreleaser/internal/testlib"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
@ -12,7 +13,7 @@ func TestRepoName(t *testing.T) {
|
||||
defer back()
|
||||
testlib.GitInit(t)
|
||||
testlib.GitRemoteAdd(t, "git@github.com:goreleaser/goreleaser.git")
|
||||
repo, err := remoteRepo()
|
||||
repo, err := git.ExtractRepoFromConfig()
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "goreleaser/goreleaser", repo.String())
|
||||
}
|
||||
@ -27,7 +28,7 @@ func TestExtractRepoFromURL(t *testing.T) {
|
||||
"https://github.enterprise.com/crazy/url/goreleaser/goreleaser.git",
|
||||
} {
|
||||
t.Run(url, func(t *testing.T) {
|
||||
repo := extractRepoFromURL(url)
|
||||
repo := git.ExtractRepoFromURL(url)
|
||||
assert.Equal(t, "goreleaser/goreleaser", repo.String())
|
||||
})
|
||||
}
|
@ -1,18 +1,19 @@
|
||||
package git
|
||||
package git_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/git"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestGit(t *testing.T) {
|
||||
out, err := Run("status")
|
||||
out, err := git.Run("status")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, out)
|
||||
|
||||
out, err = Run("command-that-dont-exist")
|
||||
out, err = git.Run("command-that-dont-exist")
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, out)
|
||||
assert.Equal(
|
||||
@ -23,18 +24,18 @@ func TestGit(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestRepo(t *testing.T) {
|
||||
assert.True(t, IsRepo(), "goreleaser folder should be a git repo")
|
||||
assert.True(t, git.IsRepo(), "goreleaser folder should be a git repo")
|
||||
|
||||
assert.NoError(t, os.Chdir(os.TempDir()))
|
||||
assert.False(t, IsRepo(), os.TempDir()+" folder should be a git repo")
|
||||
assert.False(t, git.IsRepo(), os.TempDir()+" folder should be a git repo")
|
||||
}
|
||||
|
||||
func TestClean(t *testing.T) {
|
||||
out, err := Clean("asdasd 'ssadas'\nadasd", nil)
|
||||
out, err := git.Clean("asdasd 'ssadas'\nadasd", nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "asdasd ssadas", out)
|
||||
|
||||
out, err = Clean(Run("command-that-dont-exist"))
|
||||
out, err = git.Clean(git.Run("command-that-dont-exist"))
|
||||
assert.Error(t, err)
|
||||
assert.Empty(t, out)
|
||||
assert.Equal(
|
||||
|
@ -731,6 +731,10 @@ type DummyClient struct {
|
||||
NotImplemented bool
|
||||
}
|
||||
|
||||
func (dc *DummyClient) CloseMilestone(ctx *context.Context, repo client.Repo, title string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *DummyClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) {
|
||||
return
|
||||
}
|
||||
|
2
internal/pipe/milestone/doc.go
Normal file
2
internal/pipe/milestone/doc.go
Normal file
@ -0,0 +1,2 @@
|
||||
// Package milestone implements Pipe and manages VCS milestones.
|
||||
package milestone
|
98
internal/pipe/milestone/milestone.go
Normal file
98
internal/pipe/milestone/milestone.go
Normal file
@ -0,0 +1,98 @@
|
||||
package milestone
|
||||
|
||||
import (
|
||||
"github.com/apex/log"
|
||||
"github.com/goreleaser/goreleaser/internal/client"
|
||||
"github.com/goreleaser/goreleaser/internal/git"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe"
|
||||
"github.com/goreleaser/goreleaser/internal/tmpl"
|
||||
"github.com/goreleaser/goreleaser/pkg/config"
|
||||
"github.com/goreleaser/goreleaser/pkg/context"
|
||||
)
|
||||
|
||||
const defaultNameTemplate = "{{ .Tag }}"
|
||||
|
||||
// Pipe for milestone.
|
||||
type Pipe struct{}
|
||||
|
||||
func (Pipe) String() string {
|
||||
return "milestones"
|
||||
}
|
||||
|
||||
// Default sets the pipe defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
if len(ctx.Config.Milestones) == 0 {
|
||||
ctx.Config.Milestones = append(ctx.Config.Milestones, config.Milestone{})
|
||||
}
|
||||
|
||||
for i := range ctx.Config.Milestones {
|
||||
milestone := &ctx.Config.Milestones[i]
|
||||
|
||||
if milestone.NameTemplate == "" {
|
||||
milestone.NameTemplate = defaultNameTemplate
|
||||
}
|
||||
|
||||
if milestone.Repo.Name == "" {
|
||||
repo, err := git.ExtractRepoFromConfig()
|
||||
|
||||
if err != nil && !ctx.Snapshot {
|
||||
return err
|
||||
}
|
||||
|
||||
milestone.Repo = repo
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Publish the release.
|
||||
func (Pipe) Publish(ctx *context.Context) error {
|
||||
if ctx.SkipPublish {
|
||||
return pipe.ErrSkipPublishEnabled
|
||||
}
|
||||
c, err := client.New(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return doPublish(ctx, c)
|
||||
}
|
||||
|
||||
func doPublish(ctx *context.Context, vcsClient client.Client) error {
|
||||
for i := range ctx.Config.Milestones {
|
||||
milestone := &ctx.Config.Milestones[i]
|
||||
|
||||
if !milestone.Close {
|
||||
return pipe.Skip("milestone pipe is disabled")
|
||||
}
|
||||
|
||||
name, err := tmpl.New(ctx).Apply(milestone.NameTemplate)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repo := client.Repo{
|
||||
Name: milestone.Repo.Name,
|
||||
Owner: milestone.Repo.Owner,
|
||||
}
|
||||
|
||||
log.WithField("milestone", name).
|
||||
WithField("repo", repo.String()).
|
||||
Info("closing milestone")
|
||||
|
||||
err = vcsClient.CloseMilestone(ctx, repo, name)
|
||||
|
||||
if err != nil {
|
||||
if milestone.FailOnError {
|
||||
return err
|
||||
}
|
||||
|
||||
log.WithField("milestone", name).
|
||||
WithField("repo", repo.String()).
|
||||
Warnf("error closing milestone: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
222
internal/pipe/milestone/milestone_test.go
Normal file
222
internal/pipe/milestone/milestone_test.go
Normal file
@ -0,0 +1,222 @@
|
||||
package milestone
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/artifact"
|
||||
"github.com/goreleaser/goreleaser/internal/client"
|
||||
"github.com/goreleaser/goreleaser/internal/testlib"
|
||||
"github.com/goreleaser/goreleaser/pkg/config"
|
||||
"github.com/goreleaser/goreleaser/pkg/context"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestDefaultWithRepoConfig(t *testing.T) {
|
||||
_, back := testlib.Mktmp(t)
|
||||
defer back()
|
||||
testlib.GitInit(t)
|
||||
testlib.GitRemoteAdd(t, "git@github.com:githubowner/githubrepo.git")
|
||||
|
||||
var ctx = &context.Context{
|
||||
Config: config.Project{
|
||||
Milestones: []config.Milestone{
|
||||
{
|
||||
Repo: config.Repo{
|
||||
Name: "configrepo",
|
||||
Owner: "configowner",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx.TokenType = context.TokenTypeGitHub
|
||||
assert.NoError(t, Pipe{}.Default(ctx))
|
||||
assert.Equal(t, "configrepo", ctx.Config.Milestones[0].Repo.Name)
|
||||
assert.Equal(t, "configowner", ctx.Config.Milestones[0].Repo.Owner)
|
||||
}
|
||||
|
||||
func TestDefaultWithRepoRemote(t *testing.T) {
|
||||
_, back := testlib.Mktmp(t)
|
||||
defer back()
|
||||
testlib.GitInit(t)
|
||||
testlib.GitRemoteAdd(t, "git@github.com:githubowner/githubrepo.git")
|
||||
|
||||
var ctx = context.New(config.Project{})
|
||||
ctx.TokenType = context.TokenTypeGitHub
|
||||
assert.NoError(t, Pipe{}.Default(ctx))
|
||||
assert.Equal(t, "githubrepo", ctx.Config.Milestones[0].Repo.Name)
|
||||
assert.Equal(t, "githubowner", ctx.Config.Milestones[0].Repo.Owner)
|
||||
}
|
||||
|
||||
func TestDefaultWithNameTemplate(t *testing.T) {
|
||||
var ctx = &context.Context{
|
||||
Config: config.Project{
|
||||
Milestones: []config.Milestone{
|
||||
{
|
||||
NameTemplate: "confignametemplate",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, Pipe{}.Default(ctx))
|
||||
assert.Equal(t, "confignametemplate", ctx.Config.Milestones[0].NameTemplate)
|
||||
}
|
||||
|
||||
func TestDefaultWithoutGitRepo(t *testing.T) {
|
||||
_, back := testlib.Mktmp(t)
|
||||
defer back()
|
||||
var ctx = &context.Context{
|
||||
Config: config.Project{},
|
||||
}
|
||||
ctx.TokenType = context.TokenTypeGitHub
|
||||
assert.EqualError(t, Pipe{}.Default(ctx), "current folder is not a git repository")
|
||||
assert.Empty(t, ctx.Config.Milestones[0].Repo.String())
|
||||
}
|
||||
|
||||
func TestDefaultWithoutGitRepoOrigin(t *testing.T) {
|
||||
_, back := testlib.Mktmp(t)
|
||||
defer back()
|
||||
var ctx = &context.Context{
|
||||
Config: config.Project{},
|
||||
}
|
||||
ctx.TokenType = context.TokenTypeGitHub
|
||||
testlib.GitInit(t)
|
||||
assert.EqualError(t, Pipe{}.Default(ctx), "repository doesn't have an `origin` remote")
|
||||
assert.Empty(t, ctx.Config.Milestones[0].Repo.String())
|
||||
}
|
||||
|
||||
func TestDefaultWithoutGitRepoSnapshot(t *testing.T) {
|
||||
_, back := testlib.Mktmp(t)
|
||||
defer back()
|
||||
var ctx = &context.Context{
|
||||
Config: config.Project{},
|
||||
}
|
||||
ctx.TokenType = context.TokenTypeGitHub
|
||||
ctx.Snapshot = true
|
||||
assert.NoError(t, Pipe{}.Default(ctx))
|
||||
assert.Empty(t, ctx.Config.Milestones[0].Repo.String())
|
||||
}
|
||||
|
||||
func TestDefaultWithoutNameTemplate(t *testing.T) {
|
||||
var ctx = &context.Context{
|
||||
Config: config.Project{
|
||||
Milestones: []config.Milestone{},
|
||||
},
|
||||
}
|
||||
assert.NoError(t, Pipe{}.Default(ctx))
|
||||
assert.Equal(t, "{{ .Tag }}", ctx.Config.Milestones[0].NameTemplate)
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
assert.NotEmpty(t, Pipe{}.String())
|
||||
}
|
||||
|
||||
func TestPublishCloseDisabled(t *testing.T) {
|
||||
var ctx = context.New(config.Project{
|
||||
Milestones: []config.Milestone{
|
||||
{
|
||||
Close: false,
|
||||
},
|
||||
},
|
||||
})
|
||||
client := &DummyClient{}
|
||||
testlib.AssertSkipped(t, doPublish(ctx, client))
|
||||
assert.Equal(t, "", client.ClosedMilestone)
|
||||
}
|
||||
|
||||
func TestPublishCloseEnabled(t *testing.T) {
|
||||
var ctx = context.New(config.Project{
|
||||
Milestones: []config.Milestone{
|
||||
{
|
||||
Close: true,
|
||||
NameTemplate: defaultNameTemplate,
|
||||
Repo: config.Repo{
|
||||
Name: "configrepo",
|
||||
Owner: "configowner",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
ctx.Git.CurrentTag = "v1.0.0"
|
||||
client := &DummyClient{}
|
||||
assert.NoError(t, doPublish(ctx, client))
|
||||
assert.Equal(t, "v1.0.0", client.ClosedMilestone)
|
||||
}
|
||||
|
||||
func TestPublishCloseError(t *testing.T) {
|
||||
var config = config.Project{
|
||||
Milestones: []config.Milestone{
|
||||
{
|
||||
Close: true,
|
||||
NameTemplate: defaultNameTemplate,
|
||||
Repo: config.Repo{
|
||||
Name: "configrepo",
|
||||
Owner: "configowner",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var ctx = context.New(config)
|
||||
ctx.Git.CurrentTag = "v1.0.0"
|
||||
client := &DummyClient{
|
||||
FailToCloseMilestone: true,
|
||||
}
|
||||
assert.NoError(t, doPublish(ctx, client))
|
||||
assert.Equal(t, "", client.ClosedMilestone)
|
||||
}
|
||||
|
||||
func TestPublishCloseFailOnError(t *testing.T) {
|
||||
var config = config.Project{
|
||||
Milestones: []config.Milestone{
|
||||
{
|
||||
Close: true,
|
||||
FailOnError: true,
|
||||
NameTemplate: defaultNameTemplate,
|
||||
Repo: config.Repo{
|
||||
Name: "configrepo",
|
||||
Owner: "configowner",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
var ctx = context.New(config)
|
||||
ctx.Git.CurrentTag = "v1.0.0"
|
||||
client := &DummyClient{
|
||||
FailToCloseMilestone: true,
|
||||
}
|
||||
assert.Error(t, doPublish(ctx, client))
|
||||
assert.Equal(t, "", client.ClosedMilestone)
|
||||
}
|
||||
|
||||
type DummyClient struct {
|
||||
ClosedMilestone string
|
||||
FailToCloseMilestone bool
|
||||
}
|
||||
|
||||
func (c *DummyClient) CloseMilestone(ctx *context.Context, repo client.Repo, title string) error {
|
||||
if c.FailToCloseMilestone {
|
||||
return errors.New("milestone failed")
|
||||
}
|
||||
|
||||
c.ClosedMilestone = title
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DummyClient) CreateRelease(ctx *context.Context, body string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *DummyClient) ReleaseURLTemplate(ctx *context.Context) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (c *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo client.Repo, content []byte, path, msg string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DummyClient) Upload(ctx *context.Context, releaseID string, artifact *artifact.Artifact, file *os.File) error {
|
||||
return nil
|
||||
}
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/brew"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/custompublishers"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/docker"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/milestone"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/release"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/scoop"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/snapcraft"
|
||||
@ -46,6 +47,7 @@ var publishers = []Publisher{
|
||||
// brew and scoop use the release URL, so, they should be last
|
||||
brew.Pipe{},
|
||||
scoop.Pipe{},
|
||||
milestone.Pipe{},
|
||||
}
|
||||
|
||||
// Run the pipe.
|
||||
|
@ -8,6 +8,7 @@ import (
|
||||
"github.com/goreleaser/goreleaser/internal/artifact"
|
||||
"github.com/goreleaser/goreleaser/internal/client"
|
||||
"github.com/goreleaser/goreleaser/internal/extrafiles"
|
||||
"github.com/goreleaser/goreleaser/internal/git"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe"
|
||||
"github.com/goreleaser/goreleaser/internal/semerrgroup"
|
||||
"github.com/goreleaser/goreleaser/pkg/context"
|
||||
@ -50,7 +51,7 @@ func (Pipe) Default(ctx *context.Context) error {
|
||||
case context.TokenTypeGitLab:
|
||||
{
|
||||
if ctx.Config.Release.GitLab.Name == "" {
|
||||
repo, err := remoteRepo()
|
||||
repo, err := git.ExtractRepoFromConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -62,7 +63,7 @@ func (Pipe) Default(ctx *context.Context) error {
|
||||
case context.TokenTypeGitea:
|
||||
{
|
||||
if ctx.Config.Release.Gitea.Name == "" {
|
||||
repo, err := remoteRepo()
|
||||
repo, err := git.ExtractRepoFromConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -75,7 +76,7 @@ func (Pipe) Default(ctx *context.Context) error {
|
||||
|
||||
// We keep github as default for now
|
||||
if ctx.Config.Release.GitHub.Name == "" {
|
||||
repo, err := remoteRepo()
|
||||
repo, err := git.ExtractRepoFromConfig()
|
||||
if err != nil && !ctx.Snapshot {
|
||||
return err
|
||||
}
|
||||
|
@ -538,6 +538,10 @@ type DummyClient struct {
|
||||
Lock sync.Mutex
|
||||
}
|
||||
|
||||
func (c *DummyClient) CloseMilestone(ctx *context.Context, repo client.Repo, title string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DummyClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) {
|
||||
if c.FailToCreateRelease {
|
||||
return "", errors.New("release failed")
|
||||
|
@ -1023,6 +1023,10 @@ type DummyClient struct {
|
||||
NotImplemented bool
|
||||
}
|
||||
|
||||
func (dc *DummyClient) CloseMilestone(ctx *context.Context, repo client.Repo, title string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (dc *DummyClient) CreateRelease(ctx *context.Context, body string) (releaseID string, err error) {
|
||||
return
|
||||
}
|
||||
|
@ -282,6 +282,14 @@ type Release struct {
|
||||
ExtraFiles []ExtraFile `yaml:"extra_files,omitempty"`
|
||||
}
|
||||
|
||||
// Milestone config used for VCS milestone.
|
||||
type Milestone struct {
|
||||
Repo Repo `yaml:",omitempty"`
|
||||
Close bool `yaml:",omitempty"`
|
||||
FailOnError bool `yaml:"fail_on_error,omitempty"`
|
||||
NameTemplate string `yaml:"name_template,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraFile on a release.
|
||||
type ExtraFile struct {
|
||||
Glob string `yaml:"glob,omitempty"`
|
||||
@ -477,6 +485,7 @@ type Project struct {
|
||||
ProjectName string `yaml:"project_name,omitempty"`
|
||||
Env []string `yaml:",omitempty"`
|
||||
Release Release `yaml:",omitempty"`
|
||||
Milestones []Milestone `yaml:",omitempty"`
|
||||
Brews []Homebrew `yaml:",omitempty"`
|
||||
Scoop Scoop `yaml:",omitempty"`
|
||||
Builds []Build `yaml:",omitempty"`
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/build"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/checksums"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/docker"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/milestone"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/nfpm"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/project"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/release"
|
||||
@ -50,4 +51,5 @@ var Defaulters = []Defaulter{
|
||||
blob.Pipe{},
|
||||
brew.Pipe{},
|
||||
scoop.Pipe{},
|
||||
milestone.Pipe{},
|
||||
}
|
||||
|
35
www/docs/customization/milestone.md
Normal file
35
www/docs/customization/milestone.md
Normal file
@ -0,0 +1,35 @@
|
||||
---
|
||||
title: Milestone
|
||||
---
|
||||
|
||||
GoReleaser can close repository milestones after successfully
|
||||
publishing all artifacts.
|
||||
|
||||
Let's see what can be customized in the `milestones` section:
|
||||
|
||||
```yaml
|
||||
# .goreleaser.yml
|
||||
milestones:
|
||||
# You can have multiple milestone configs
|
||||
-
|
||||
# Repository for the milestone
|
||||
# Default is extracted from the origin remote URL
|
||||
repo:
|
||||
owner: user
|
||||
name: repo
|
||||
|
||||
# Whether to close the milestone
|
||||
# Default is false
|
||||
close: true
|
||||
|
||||
# Fail release on errors, such as missing milestone on close
|
||||
# Default is false
|
||||
fail_on_error: true
|
||||
|
||||
# Name of the milestone
|
||||
# Default is `{{ .Tag }}`
|
||||
name_template: "Current Release"
|
||||
```
|
||||
|
||||
!!! tip
|
||||
Learn more about the [name template engine](/customization/templates).
|
@ -70,6 +70,7 @@ nav:
|
||||
- customization/homebrew.md
|
||||
- customization/upload.md
|
||||
- customization/templates.md
|
||||
- customization/milestone.md
|
||||
- customization/nfpm.md
|
||||
- customization/project.md
|
||||
- customization/release.md
|
||||
|
Reference in New Issue
Block a user