You've already forked goreleaser
							
							
				mirror of
				https://github.com/goreleaser/goreleaser.git
				synced 2025-10-30 23:58:09 +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