You've already forked goreleaser
							
							
				mirror of
				https://github.com/goreleaser/goreleaser.git
				synced 2025-10-30 23:58:09 +02:00 
			
		
		
		
	feat: support custom tokens in Homebrew & Scoop (#1650)
This commit is contained in:
		| @@ -18,11 +18,23 @@ type Info struct { | ||||
| 	URL         string | ||||
| } | ||||
|  | ||||
| type Repo struct { | ||||
| 	Owner string | ||||
| 	Name  string | ||||
| } | ||||
|  | ||||
| func (r Repo) String() string { | ||||
| 	if r.Owner == "" && r.Name == "" { | ||||
| 		return "" | ||||
| 	} | ||||
| 	return r.Owner + "/" + r.Name | ||||
| } | ||||
|  | ||||
| // Client interface. | ||||
| type Client interface { | ||||
| 	CreateRelease(ctx *context.Context, body string) (releaseID string, err error) | ||||
| 	ReleaseURLTemplate(ctx *context.Context) (string, error) | ||||
| 	CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo config.Repo, content []byte, path, message string) (err error) | ||||
| 	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) | ||||
| } | ||||
|  | ||||
| @@ -30,13 +42,26 @@ type Client interface { | ||||
| func New(ctx *context.Context) (Client, error) { | ||||
| 	log.WithField("type", ctx.TokenType).Info("token type") | ||||
| 	if ctx.TokenType == context.TokenTypeGitHub { | ||||
| 		return NewGitHub(ctx) | ||||
| 		return NewGitHub(ctx, ctx.Token) | ||||
| 	} | ||||
| 	if ctx.TokenType == context.TokenTypeGitLab { | ||||
| 		return NewGitLab(ctx) | ||||
| 		return NewGitLab(ctx, ctx.Token) | ||||
| 	} | ||||
| 	if ctx.TokenType == context.TokenTypeGitea { | ||||
| 		return NewGitea(ctx) | ||||
| 		return NewGitea(ctx, ctx.Token) | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func NewWithToken(ctx *context.Context, token string) (Client, error) { | ||||
| 	if ctx.TokenType == context.TokenTypeGitHub { | ||||
| 		return NewGitHub(ctx, token) | ||||
| 	} | ||||
| 	if ctx.TokenType == context.TokenTypeGitLab { | ||||
| 		return NewGitLab(ctx, token) | ||||
| 	} | ||||
| 	if ctx.TokenType == context.TokenTypeGitea { | ||||
| 		return NewGitea(ctx, token) | ||||
| 	} | ||||
| 	return nil, nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										12
									
								
								internal/client/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								internal/client/config.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | ||||
| package client | ||||
|  | ||||
| import ( | ||||
| 	"github.com/goreleaser/goreleaser/pkg/config" | ||||
| ) | ||||
|  | ||||
| func RepoFromRef(ref config.RepoRef) Repo { | ||||
| 	return Repo{ | ||||
| 		Owner: ref.Owner, | ||||
| 		Name:  ref.Name, | ||||
| 	} | ||||
| } | ||||
| @@ -34,12 +34,12 @@ func getInstanceURL(apiURL string) (string, error) { | ||||
| } | ||||
|  | ||||
| // NewGitea returns a gitea client implementation. | ||||
| func NewGitea(ctx *context.Context) (Client, error) { | ||||
| func NewGitea(ctx *context.Context, token string) (Client, error) { | ||||
| 	instanceURL, err := getInstanceURL(ctx.Config.GiteaURLs.API) | ||||
| 	if err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
| 	client := gitea.NewClient(instanceURL, ctx.Token) | ||||
| 	client := gitea.NewClient(instanceURL, token) | ||||
| 	transport := &http.Transport{ | ||||
| 		TLSClientConfig: &tls.Config{ | ||||
| 			// nolint: gosec | ||||
| @@ -56,7 +56,7 @@ func NewGitea(ctx *context.Context) (Client, error) { | ||||
| func (c *giteaClient) CreateFile( | ||||
| 	ctx *context.Context, | ||||
| 	commitAuthor config.CommitAuthor, | ||||
| 	repo config.Repo, | ||||
| 	repo Repo, | ||||
| 	content []byte, | ||||
| 	path, | ||||
| 	message string, | ||||
|   | ||||
| @@ -245,7 +245,7 @@ func TestGiteaCreateFile(t *testing.T) { | ||||
| 	client := giteaClient{} | ||||
| 	ctx := context.Context{} | ||||
| 	author := config.CommitAuthor{} | ||||
| 	repo := config.Repo{} | ||||
| 	repo := Repo{} | ||||
| 	content := []byte{} | ||||
| 	path := "" | ||||
| 	message := "" | ||||
|   | ||||
| @@ -25,9 +25,9 @@ type githubClient struct { | ||||
| } | ||||
|  | ||||
| // NewGitHub returns a github client implementation. | ||||
| func NewGitHub(ctx *context.Context) (Client, error) { | ||||
| func NewGitHub(ctx *context.Context, token string) (Client, error) { | ||||
| 	ts := oauth2.StaticTokenSource( | ||||
| 		&oauth2.Token{AccessToken: ctx.Token}, | ||||
| 		&oauth2.Token{AccessToken: token}, | ||||
| 	) | ||||
| 	httpClient := oauth2.NewClient(ctx, ts) | ||||
| 	base := httpClient.Transport.(*oauth2.Transport).Base | ||||
| @@ -59,7 +59,7 @@ func NewGitHub(ctx *context.Context) (Client, error) { | ||||
| func (c *githubClient) CreateFile( | ||||
| 	ctx *context.Context, | ||||
| 	commitAuthor config.CommitAuthor, | ||||
| 	repo config.Repo, | ||||
| 	repo Repo, | ||||
| 	content []byte, | ||||
| 	path, | ||||
| 	message string, | ||||
|   | ||||
| @@ -11,34 +11,37 @@ import ( | ||||
|  | ||||
| func TestNewGitHubClient(t *testing.T) { | ||||
| 	t.Run("good urls", func(t *testing.T) { | ||||
| 		_, err := NewGitHub(context.New(config.Project{ | ||||
| 		ctx := context.New(config.Project{ | ||||
| 			GitHubURLs: config.GitHubURLs{ | ||||
| 				API:    "https://github.mycompany.com/api", | ||||
| 				Upload: "https://github.mycompany.com/upload", | ||||
| 			}, | ||||
| 		})) | ||||
| 		}) | ||||
| 		_, err := NewGitHub(ctx, ctx.Token) | ||||
|  | ||||
| 		require.NoError(t, err) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("bad api url", func(t *testing.T) { | ||||
| 		_, err := NewGitHub(context.New(config.Project{ | ||||
| 		ctx := context.New(config.Project{ | ||||
| 			GitHubURLs: config.GitHubURLs{ | ||||
| 				API:    "://github.mycompany.com/api", | ||||
| 				Upload: "https://github.mycompany.com/upload", | ||||
| 			}, | ||||
| 		})) | ||||
| 		}) | ||||
| 		_, err := NewGitHub(ctx, ctx.Token) | ||||
|  | ||||
| 		require.EqualError(t, err, `parse "://github.mycompany.com/api": missing protocol scheme`) | ||||
| 	}) | ||||
|  | ||||
| 	t.Run("bad upload url", func(t *testing.T) { | ||||
| 		_, err := NewGitHub(context.New(config.Project{ | ||||
| 		ctx := context.New(config.Project{ | ||||
| 			GitHubURLs: config.GitHubURLs{ | ||||
| 				API:    "https://github.mycompany.com/api", | ||||
| 				Upload: "not a url:4994", | ||||
| 			}, | ||||
| 		})) | ||||
| 		}) | ||||
| 		_, err := NewGitHub(ctx, ctx.Token) | ||||
|  | ||||
| 		require.EqualError(t, err, `parse "not a url:4994": first path segment in URL cannot contain colon`) | ||||
| 	}) | ||||
| @@ -46,7 +49,7 @@ func TestNewGitHubClient(t *testing.T) { | ||||
|  | ||||
| func TestGitHubUploadReleaseIDNotInt(t *testing.T) { | ||||
| 	var ctx = context.New(config.Project{}) | ||||
| 	client, err := NewGitHub(ctx) | ||||
| 	client, err := NewGitHub(ctx, ctx.Token) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	require.EqualError( | ||||
| @@ -69,7 +72,7 @@ func TestGitHubReleaseURLTemplate(t *testing.T) { | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| 	client, err := NewGitHub(ctx) | ||||
| 	client, err := NewGitHub(ctx, ctx.Token) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	urlTpl, err := client.ReleaseURLTemplate(ctx) | ||||
| @@ -85,7 +88,7 @@ func TestGitHubCreateReleaseWrongNameTemplate(t *testing.T) { | ||||
| 			NameTemplate: "{{.dddddddddd", | ||||
| 		}, | ||||
| 	}) | ||||
| 	client, err := NewGitHub(ctx) | ||||
| 	client, err := NewGitHub(ctx, ctx.Token) | ||||
| 	require.NoError(t, err) | ||||
|  | ||||
| 	str, err := client.CreateRelease(ctx, "") | ||||
|   | ||||
| @@ -26,8 +26,7 @@ type gitlabClient struct { | ||||
| } | ||||
|  | ||||
| // NewGitLab returns a gitlab client implementation. | ||||
| func NewGitLab(ctx *context.Context) (Client, error) { | ||||
| 	token := ctx.Token | ||||
| func NewGitLab(ctx *context.Context, token string) (Client, error) { | ||||
| 	transport := &http.Transport{ | ||||
| 		TLSClientConfig: &tls.Config{ | ||||
| 			// nolint: gosec | ||||
| @@ -54,7 +53,7 @@ func NewGitLab(ctx *context.Context) (Client, error) { | ||||
| func (c *gitlabClient) CreateFile( | ||||
| 	ctx *context.Context, | ||||
| 	commitAuthor config.CommitAuthor, | ||||
| 	repo config.Repo, | ||||
| 	repo Repo, | ||||
| 	content []byte, // the content of the formula.rb | ||||
| 	path, // the path to the formula.rb | ||||
| 	message string, // the commit msg | ||||
|   | ||||
| @@ -47,7 +47,7 @@ func TestGitLabReleaseURLTemplate(t *testing.T) { | ||||
| 			}, | ||||
| 		}, | ||||
| 	}) | ||||
| 	client, err := NewGitLab(ctx) | ||||
| 	client, err := NewGitLab(ctx, ctx.Token) | ||||
| 	assert.NoError(t, err) | ||||
|  | ||||
| 	urlTpl, err := client.ReleaseURLTemplate(ctx) | ||||
|   | ||||
| @@ -88,11 +88,13 @@ func (Pipe) Default(ctx *context.Context) error { | ||||
| 		} | ||||
| 		if brew.GitHub.String() != "" { | ||||
| 			deprecate.Notice(ctx, "brews.github") | ||||
| 			brew.Tap = brew.GitHub | ||||
| 			brew.Tap.Owner = brew.GitHub.Owner | ||||
| 			brew.Tap.Name = brew.GitHub.Name | ||||
| 		} | ||||
| 		if brew.GitLab.String() != "" { | ||||
| 			deprecate.Notice(ctx, "brews.gitlab") | ||||
| 			brew.Tap = brew.GitLab | ||||
| 			brew.Tap.Owner = brew.GitLab.Owner | ||||
| 			brew.Tap.Name = brew.GitLab.Name | ||||
| 		} | ||||
| 		if brew.CommitAuthor.Name == "" { | ||||
| 			brew.CommitAuthor.Name = "goreleaserbot" | ||||
| @@ -129,11 +131,24 @@ func contains(ss []string, s string) bool { | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| func doRun(ctx *context.Context, brew config.Homebrew, client client.Client) error { | ||||
| func doRun(ctx *context.Context, brew config.Homebrew, cl client.Client) error { | ||||
| 	if brew.Tap.Name == "" { | ||||
| 		return pipe.Skip("brew section is not configured") | ||||
| 	} | ||||
|  | ||||
| 	if brew.Tap.Token != "" { | ||||
| 		token, err := tmpl.New(ctx).ApplySingleEnvOnly(brew.Tap.Token) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		log.Debug("using custom token to publish homebrew formula") | ||||
| 		c, err := client.NewWithToken(ctx, token) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		cl = c | ||||
| 	} | ||||
|  | ||||
| 	// TODO: properly cover this with tests | ||||
| 	var filters = []artifact.Filter{ | ||||
| 		artifact.Or( | ||||
| @@ -160,7 +175,7 @@ func doRun(ctx *context.Context, brew config.Homebrew, client client.Client) err | ||||
| 		return ErrNoArchivesFound | ||||
| 	} | ||||
|  | ||||
| 	content, err := buildFormula(ctx, brew, client, archives) | ||||
| 	content, err := buildFormula(ctx, brew, cl, archives) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -169,7 +184,7 @@ func doRun(ctx *context.Context, brew config.Homebrew, client client.Client) err | ||||
| 	var path = filepath.Join(ctx.Config.Dist, filename) | ||||
| 	log.WithField("formula", path).Info("writing") | ||||
| 	if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil { //nolint: gosec | ||||
| 		return errors.Wrap(err, "failed to write brew tap") | ||||
| 		return errors.Wrap(err, "failed to write brew formula") | ||||
| 	} | ||||
|  | ||||
| 	if strings.TrimSpace(brew.SkipUpload) == "true" { | ||||
| @@ -182,7 +197,7 @@ func doRun(ctx *context.Context, brew config.Homebrew, client client.Client) err | ||||
| 		return pipe.Skip("prerelease detected with 'auto' upload, skipping homebrew publish") | ||||
| 	} | ||||
|  | ||||
| 	repo := brew.Tap | ||||
| 	repo := client.RepoFromRef(brew.Tap) | ||||
|  | ||||
| 	var gpath = buildFormulaPath(brew.Folder, filename) | ||||
| 	log.WithField("formula", gpath). | ||||
| @@ -190,7 +205,7 @@ func doRun(ctx *context.Context, brew config.Homebrew, client client.Client) err | ||||
| 		Info("pushing") | ||||
|  | ||||
| 	var msg = fmt.Sprintf("Brew formula update for %s version %s", ctx.Config.ProjectName, ctx.Git.CurrentTag) | ||||
| 	return client.CreateFile(ctx, brew.CommitAuthor, repo, []byte(content), gpath, msg) | ||||
| 	return cl.CreateFile(ctx, brew.CommitAuthor, repo, []byte(content), gpath, msg) | ||||
| } | ||||
|  | ||||
| func buildFormulaPath(folder, filename string) string { | ||||
|   | ||||
| @@ -269,7 +269,7 @@ func TestRunPipeForMultipleArmVersions(t *testing.T) { | ||||
| 						Dependencies: []config.HomebrewDependency{{Name: "zsh"}, {Name: "bash", Type: "recommended"}}, | ||||
| 						Conflicts:    []string{"gtk+", "qt"}, | ||||
| 						Install:      `bin.install "{{ .ProjectName }}"`, | ||||
| 						Tap: config.Repo{ | ||||
| 						Tap: config.RepoRef{ | ||||
| 							Owner: "test", | ||||
| 							Name:  "test", | ||||
| 						}, | ||||
| @@ -365,7 +365,7 @@ func TestRunPipeNoDarwin64Build(t *testing.T) { | ||||
| 		Config: config.Project{ | ||||
| 			Brews: []config.Homebrew{ | ||||
| 				{ | ||||
| 					Tap: config.Repo{ | ||||
| 					Tap: config.RepoRef{ | ||||
| 						Owner: "test", | ||||
| 						Name:  "test", | ||||
| 					}, | ||||
| @@ -383,7 +383,7 @@ func TestRunPipeMultipleArchivesSameOsBuild(t *testing.T) { | ||||
| 		config.Project{ | ||||
| 			Brews: []config.Homebrew{ | ||||
| 				{ | ||||
| 					Tap: config.Repo{ | ||||
| 					Tap: config.RepoRef{ | ||||
| 						Owner: "test", | ||||
| 						Name:  "test", | ||||
| 					}, | ||||
| @@ -537,7 +537,7 @@ func TestRunPipeBinaryRelease(t *testing.T) { | ||||
| 		config.Project{ | ||||
| 			Brews: []config.Homebrew{ | ||||
| 				{ | ||||
| 					Tap: config.Repo{ | ||||
| 					Tap: config.RepoRef{ | ||||
| 						Owner: "test", | ||||
| 						Name:  "test", | ||||
| 					}, | ||||
| @@ -566,7 +566,7 @@ func TestRunPipeNoUpload(t *testing.T) { | ||||
| 		Release:     config.Release{}, | ||||
| 		Brews: []config.Homebrew{ | ||||
| 			{ | ||||
| 				Tap: config.Repo{ | ||||
| 				Tap: config.RepoRef{ | ||||
| 					Owner: "test", | ||||
| 					Name:  "test", | ||||
| 				}, | ||||
| @@ -618,7 +618,7 @@ func TestRunEmptyTokenType(t *testing.T) { | ||||
| 		Release:     config.Release{}, | ||||
| 		Brews: []config.Homebrew{ | ||||
| 			{ | ||||
| 				Tap: config.Repo{ | ||||
| 				Tap: config.RepoRef{ | ||||
| 					Owner: "test", | ||||
| 					Name:  "test", | ||||
| 				}, | ||||
| @@ -653,7 +653,7 @@ func TestRunTokenTypeNotImplementedForBrew(t *testing.T) { | ||||
| 		Release:     config.Release{}, | ||||
| 		Brews: []config.Homebrew{ | ||||
| 			{ | ||||
| 				Tap: config.Repo{ | ||||
| 				Tap: config.RepoRef{ | ||||
| 					Owner: "test", | ||||
| 					Name:  "test", | ||||
| 				}, | ||||
| @@ -743,7 +743,7 @@ func (dc *DummyClient) ReleaseURLTemplate(ctx *context.Context) (string, error) | ||||
| 	return "https://dummyhost/download/{{ .Tag }}/{{ .ArtifactName }}", nil | ||||
| } | ||||
|  | ||||
| func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo config.Repo, content []byte, path, msg string) (err error) { | ||||
| func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo client.Repo, content []byte, path, msg string) (err error) { | ||||
| 	dc.CreatedFile = true | ||||
| 	dc.Content = string(content) | ||||
| 	return | ||||
|   | ||||
| @@ -550,7 +550,7 @@ func (c *DummyClient) ReleaseURLTemplate(ctx *context.Context) (string, error) { | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (c *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo config.Repo, content []byte, path, msg string) (err error) { | ||||
| func (c *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo client.Repo, content []byte, path, msg string) (err error) { | ||||
| 	return | ||||
| } | ||||
|  | ||||
| @@ -570,7 +570,7 @@ func (c *DummyClient) Upload(ctx *context.Context, releaseID string, artifact *a | ||||
| 	} | ||||
| 	if c.FailFirstUpload { | ||||
| 		c.FailFirstUpload = false | ||||
| 		return client.RetriableError{errors.New("upload failed, should retry")} | ||||
| 		return client.RetriableError{Err: errors.New("upload failed, should retry")} | ||||
| 	} | ||||
| 	c.UploadedFile = true | ||||
| 	c.UploadedFileNames = append(c.UploadedFileNames, artifact.Name) | ||||
|   | ||||
| @@ -34,6 +34,7 @@ func (Pipe) Publish(ctx *context.Context) error { | ||||
| 	if ctx.SkipPublish { | ||||
| 		return pipe.ErrSkipPublishEnabled | ||||
| 	} | ||||
|  | ||||
| 	client, err := client.New(ctx) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| @@ -60,15 +61,24 @@ func (Pipe) Default(ctx *context.Context) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func doRun(ctx *context.Context, client client.Client) error { | ||||
| 	if ctx.Config.Scoop.Bucket.Name == "" { | ||||
| func doRun(ctx *context.Context, cl client.Client) error { | ||||
| 	scoop := ctx.Config.Scoop | ||||
| 	if scoop.Bucket.Name == "" { | ||||
| 		return pipe.Skip("scoop section is not configured") | ||||
| 	} | ||||
|  | ||||
| 	// TODO mavogel: in another PR | ||||
| 	// check if release pipe is not configured! | ||||
| 	// if ctx.Config.Release.Disable { | ||||
| 	// } | ||||
| 	if scoop.Bucket.Token != "" { | ||||
| 		token, err := tmpl.New(ctx).ApplySingleEnvOnly(scoop.Bucket.Token) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		log.Debug("using custom token to publish scoop manifest") | ||||
| 		c, err := client.NewWithToken(ctx, token) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		cl = c | ||||
| 	} | ||||
|  | ||||
| 	// TODO: multiple archives | ||||
| 	if ctx.Config.Archives[0].Format == "binary" { | ||||
| @@ -85,9 +95,9 @@ func doRun(ctx *context.Context, client client.Client) error { | ||||
| 		return ErrNoWindows | ||||
| 	} | ||||
|  | ||||
| 	var path = ctx.Config.Scoop.Name + ".json" | ||||
| 	var path = scoop.Name + ".json" | ||||
|  | ||||
| 	data, err := dataFor(ctx, client, archives) | ||||
| 	data, err := dataFor(ctx, cl, archives) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @@ -99,10 +109,10 @@ func doRun(ctx *context.Context, client client.Client) error { | ||||
| 	if ctx.SkipPublish { | ||||
| 		return pipe.ErrSkipPublishEnabled | ||||
| 	} | ||||
| 	if strings.TrimSpace(ctx.Config.Scoop.SkipUpload) == "true" { | ||||
| 	if strings.TrimSpace(scoop.SkipUpload) == "true" { | ||||
| 		return pipe.Skip("scoop.skip_upload is true") | ||||
| 	} | ||||
| 	if strings.TrimSpace(ctx.Config.Scoop.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" { | ||||
| 	if strings.TrimSpace(scoop.SkipUpload) == "auto" && ctx.Semver.Prerelease != "" { | ||||
| 		return pipe.Skip("release is prerelease") | ||||
| 	} | ||||
| 	if ctx.Config.Release.Draft { | ||||
| @@ -113,15 +123,16 @@ func doRun(ctx *context.Context, client client.Client) error { | ||||
| 	} | ||||
|  | ||||
| 	commitMessage, err := tmpl.New(ctx). | ||||
| 		Apply(ctx.Config.Scoop.CommitMessageTemplate) | ||||
| 		Apply(scoop.CommitMessageTemplate) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	return client.CreateFile( | ||||
| 	repo := client.RepoFromRef(scoop.Bucket) | ||||
| 	return cl.CreateFile( | ||||
| 		ctx, | ||||
| 		ctx.Config.Scoop.CommitAuthor, | ||||
| 		ctx.Config.Scoop.Bucket, | ||||
| 		scoop.CommitAuthor, | ||||
| 		repo, | ||||
| 		content.Bytes(), | ||||
| 		path, | ||||
| 		commitMessage, | ||||
|   | ||||
| @@ -113,7 +113,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -156,7 +156,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -216,7 +216,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -259,7 +259,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -319,7 +319,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -377,7 +377,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -420,7 +420,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -498,7 +498,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							}, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -539,7 +539,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							Draft: true, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -589,7 +589,7 @@ func Test_doRun(t *testing.T) { | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							SkipUpload: "auto", | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -633,7 +633,7 @@ func Test_doRun(t *testing.T) { | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							SkipUpload: "true", | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -673,7 +673,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							Disable: true, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -713,7 +713,7 @@ func Test_doRun(t *testing.T) { | ||||
| 							Draft: true, | ||||
| 						}, | ||||
| 						Scoop: config.Scoop{ | ||||
| 							Bucket: config.Repo{ | ||||
| 							Bucket: config.RepoRef{ | ||||
| 								Owner: "test", | ||||
| 								Name:  "test", | ||||
| 							}, | ||||
| @@ -780,7 +780,7 @@ func Test_buildManifest(t *testing.T) { | ||||
| 						}, | ||||
| 					}, | ||||
| 					Scoop: config.Scoop{ | ||||
| 						Bucket: config.Repo{ | ||||
| 						Bucket: config.RepoRef{ | ||||
| 							Owner: "test", | ||||
| 							Name:  "test", | ||||
| 						}, | ||||
| @@ -820,7 +820,7 @@ func Test_buildManifest(t *testing.T) { | ||||
| 						}, | ||||
| 					}, | ||||
| 					Scoop: config.Scoop{ | ||||
| 						Bucket: config.Repo{ | ||||
| 						Bucket: config.RepoRef{ | ||||
| 							Owner: "test", | ||||
| 							Name:  "test", | ||||
| 						}, | ||||
| @@ -862,7 +862,7 @@ func Test_buildManifest(t *testing.T) { | ||||
| 						}, | ||||
| 					}, | ||||
| 					Scoop: config.Scoop{ | ||||
| 						Bucket: config.Repo{ | ||||
| 						Bucket: config.RepoRef{ | ||||
| 							Owner: "test", | ||||
| 							Name:  "test", | ||||
| 						}, | ||||
| @@ -968,7 +968,7 @@ func TestWrapInDirectory(t *testing.T) { | ||||
| 				}, | ||||
| 			}, | ||||
| 			Scoop: config.Scoop{ | ||||
| 				Bucket: config.Repo{ | ||||
| 				Bucket: config.RepoRef{ | ||||
| 					Owner: "test", | ||||
| 					Name:  "test", | ||||
| 				}, | ||||
| @@ -1034,7 +1034,7 @@ func (dc *DummyClient) ReleaseURLTemplate(ctx *context.Context) (string, error) | ||||
| 	return "", nil | ||||
| } | ||||
|  | ||||
| func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo config.Repo, content []byte, path, msg string) (err error) { | ||||
| func (dc *DummyClient) CreateFile(ctx *context.Context, commitAuthor config.CommitAuthor, repo client.Repo, content []byte, path, msg string) (err error) { | ||||
| 	dc.CreatedFile = true | ||||
| 	dc.Content = string(content) | ||||
| 	return | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import ( | ||||
| 	"bytes" | ||||
| 	"fmt" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| 	"strings" | ||||
| 	"text/template" | ||||
| 	"time" | ||||
| @@ -176,6 +177,41 @@ func (t *Template) Apply(s string) (string, error) { | ||||
| 	return out.String(), err | ||||
| } | ||||
|  | ||||
| type ExpectedSingleEnvErr struct{} | ||||
|  | ||||
| func (e ExpectedSingleEnvErr) Error() string { | ||||
| 	return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)" | ||||
| } | ||||
|  | ||||
| // ApplySingleEnvOnly enforces template to only contain a single environment variable | ||||
| // and nothing else. | ||||
| func (t *Template) ApplySingleEnvOnly(s string) (string, error) { | ||||
| 	s = strings.TrimSpace(s) | ||||
| 	if len(s) == 0 { | ||||
| 		return "", nil | ||||
| 	} | ||||
|  | ||||
| 	// text/template/parse (lexer) could be used here too, | ||||
| 	// but regexp reduces the complexity and should be sufficient, | ||||
| 	// given the context is mostly discouraging users from bad practice | ||||
| 	// of hard-coded credentials, rather than catch all possible cases | ||||
| 	envOnlyRe := regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`) | ||||
| 	if !envOnlyRe.Match([]byte(s)) { | ||||
| 		return "", ExpectedSingleEnvErr{} | ||||
| 	} | ||||
|  | ||||
| 	var out bytes.Buffer | ||||
| 	tmpl, err := template.New("tmpl"). | ||||
| 		Option("missingkey=error"). | ||||
| 		Parse(s) | ||||
| 	if err != nil { | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	err = tmpl.Execute(&out, t.fields) | ||||
| 	return out.String(), err | ||||
| } | ||||
|  | ||||
| func replace(replacements map[string]string, original string) string { | ||||
| 	result := replacements[original] | ||||
| 	if result == "" { | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import ( | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"testing" | ||||
| 	"text/template" | ||||
|  | ||||
| 	"github.com/goreleaser/goreleaser/internal/artifact" | ||||
| 	"github.com/goreleaser/goreleaser/pkg/config" | ||||
| @@ -212,6 +213,72 @@ func TestFuncMap(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestApplySingleEnvOnly(t *testing.T) { | ||||
| 	ctx := context.New(config.Project{ | ||||
| 		Env: []string{ | ||||
| 			"FOO=value", | ||||
| 			"BAR=another", | ||||
| 		}, | ||||
| 	}) | ||||
|  | ||||
| 	testCases := []struct { | ||||
| 		name        string | ||||
| 		tpl         string | ||||
| 		expectedErr error | ||||
| 	}{ | ||||
| 		{ | ||||
| 			"empty tpl", | ||||
| 			"", | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"whitespaces", | ||||
| 			" 	", | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"plain-text only", | ||||
| 			"raw-token", | ||||
| 			ExpectedSingleEnvErr{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"variable with spaces", | ||||
| 			"{{ .Env.FOO }}", | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"variable without spaces", | ||||
| 			"{{.Env.FOO}}", | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"variable with outer spaces", | ||||
| 			"  {{ .Env.FOO }} ", | ||||
| 			nil, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"unknown variable", | ||||
| 			"{{ .Env.UNKNOWN }}", | ||||
| 			template.ExecError{}, | ||||
| 		}, | ||||
| 		{ | ||||
| 			"other interpolation", | ||||
| 			"{{ .ProjectName }}", | ||||
| 			ExpectedSingleEnvErr{}, | ||||
| 		}, | ||||
| 	} | ||||
| 	for _, tc := range testCases { | ||||
| 		t.Run(tc.name, func(t *testing.T) { | ||||
| 			_, err := New(ctx).ApplySingleEnvOnly(tc.tpl) | ||||
| 			if tc.expectedErr != nil { | ||||
| 				assert.Error(t, err) | ||||
| 			} else { | ||||
| 				assert.NoError(t, err) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestInvalidTemplate(t *testing.T) { | ||||
| 	ctx := context.New(config.Project{}) | ||||
| 	ctx.Git.CurrentTag = "v1.1.1" | ||||
|   | ||||
| @@ -34,11 +34,22 @@ type GiteaURLs struct { | ||||
| } | ||||
|  | ||||
| // Repo represents any kind of repo (github, gitlab, etc). | ||||
| // to upload releases into. | ||||
| type Repo struct { | ||||
| 	Owner string `yaml:",omitempty"` | ||||
| 	Name  string `yaml:",omitempty"` | ||||
| } | ||||
|  | ||||
| // RepoRef represents any kind of repo which may differ | ||||
| // from the one we are building from and may therefore | ||||
| // also require separate authentication | ||||
| // e.g. Homebrew Tap, Scoop bucket. | ||||
| type RepoRef struct { | ||||
| 	Owner string `yaml:",omitempty"` | ||||
| 	Name  string `yaml:",omitempty"` | ||||
| 	Token string `yaml:",omitempty"` | ||||
| } | ||||
|  | ||||
| // HomebrewDependency represents Homebrew dependency. | ||||
| type HomebrewDependency struct { | ||||
| 	Name string `yaml:",omitempty"` | ||||
| @@ -78,7 +89,7 @@ func (r Repo) String() string { | ||||
| // Homebrew contains the brew section. | ||||
| type Homebrew struct { | ||||
| 	Name             string               `yaml:",omitempty"` | ||||
| 	Tap              Repo                 `yaml:",omitempty"` | ||||
| 	Tap              RepoRef              `yaml:",omitempty"` | ||||
| 	CommitAuthor     CommitAuthor         `yaml:"commit_author,omitempty"` | ||||
| 	Folder           string               `yaml:",omitempty"` | ||||
| 	Caveats          string               `yaml:",omitempty"` | ||||
| @@ -105,7 +116,7 @@ type Homebrew struct { | ||||
| // Scoop contains the scoop.sh section. | ||||
| type Scoop struct { | ||||
| 	Name                  string       `yaml:",omitempty"` | ||||
| 	Bucket                Repo         `yaml:",omitempty"` | ||||
| 	Bucket                RepoRef      `yaml:",omitempty"` | ||||
| 	CommitAuthor          CommitAuthor `yaml:"commit_author,omitempty"` | ||||
| 	CommitMessageTemplate string       `yaml:"commit_msg_template,omitempty"` | ||||
| 	Homepage              string       `yaml:",omitempty"` | ||||
|   | ||||
| @@ -44,6 +44,8 @@ brews: | ||||
|     tap: | ||||
|       owner: repo-owner | ||||
|       name: homebrew-tap | ||||
|       # Optionally a token can be provided, if it differs from the token provided to GoReleaser | ||||
|       token: {{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }} | ||||
|  | ||||
|     # Template for the url which is determined by the given Token (github or gitlab) | ||||
|     # Default for github is "https://github.com/<repo_owner>/<repo_name>/releases/download/{{ .Tag }}/{{ .ArtifactName }}" | ||||
|   | ||||
| @@ -21,6 +21,8 @@ scoop: | ||||
|   bucket: | ||||
|     owner: user | ||||
|     name: scoop-bucket | ||||
|     # Optionally a token can be provided, if it differs from the token provided to GoReleaser | ||||
|     token: {{ .Env.SCOOP_BUCKET_GITHUB_TOKEN }} | ||||
|  | ||||
|   # Git author used to commit to the repository. | ||||
|   # Defaults are shown. | ||||
|   | ||||
		Reference in New Issue
	
	Block a user