From ab8bb7f2f30f2bc31638913946a070ddc4f9968d Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Mon, 6 Jul 2020 21:12:41 +0100 Subject: [PATCH] feat: support custom tokens in Homebrew & Scoop (#1650) --- internal/client/client.go | 33 +++++++++++-- internal/client/config.go | 12 +++++ internal/client/gitea.go | 6 +-- internal/client/gitea_test.go | 2 +- internal/client/github.go | 6 +-- internal/client/github_test.go | 21 +++++---- internal/client/gitlab.go | 5 +- internal/client/gitlab_test.go | 2 +- internal/pipe/brew/brew.go | 29 +++++++++--- internal/pipe/brew/brew_test.go | 16 +++---- internal/pipe/release/release_test.go | 4 +- internal/pipe/scoop/scoop.go | 39 ++++++++++------ internal/pipe/scoop/scoop_test.go | 36 +++++++------- internal/tmpl/tmpl.go | 36 ++++++++++++++ internal/tmpl/tmpl_test.go | 67 +++++++++++++++++++++++++++ pkg/config/config.go | 15 +++++- www/docs/customization/homebrew.md | 2 + www/docs/customization/scoop.md | 2 + 18 files changed, 258 insertions(+), 75 deletions(-) create mode 100644 internal/client/config.go diff --git a/internal/client/client.go b/internal/client/client.go index 35b681fc6..625f664c2 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -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 } diff --git a/internal/client/config.go b/internal/client/config.go new file mode 100644 index 000000000..8cf34fc6f --- /dev/null +++ b/internal/client/config.go @@ -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, + } +} diff --git a/internal/client/gitea.go b/internal/client/gitea.go index 810ad31f4..d81773b68 100644 --- a/internal/client/gitea.go +++ b/internal/client/gitea.go @@ -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, diff --git a/internal/client/gitea_test.go b/internal/client/gitea_test.go index bdfa47aec..0472a760c 100644 --- a/internal/client/gitea_test.go +++ b/internal/client/gitea_test.go @@ -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 := "" diff --git a/internal/client/github.go b/internal/client/github.go index 70b788367..14d5d45bf 100644 --- a/internal/client/github.go +++ b/internal/client/github.go @@ -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, diff --git a/internal/client/github_test.go b/internal/client/github_test.go index 15d63c4cd..55ffeea67 100644 --- a/internal/client/github_test.go +++ b/internal/client/github_test.go @@ -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, "") diff --git a/internal/client/gitlab.go b/internal/client/gitlab.go index c4a526306..54f91c462 100644 --- a/internal/client/gitlab.go +++ b/internal/client/gitlab.go @@ -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 diff --git a/internal/client/gitlab_test.go b/internal/client/gitlab_test.go index 2e4f8d72b..aea118f01 100644 --- a/internal/client/gitlab_test.go +++ b/internal/client/gitlab_test.go @@ -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) diff --git a/internal/pipe/brew/brew.go b/internal/pipe/brew/brew.go index 19dc3930c..99d75bbff 100644 --- a/internal/pipe/brew/brew.go +++ b/internal/pipe/brew/brew.go @@ -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 { diff --git a/internal/pipe/brew/brew_test.go b/internal/pipe/brew/brew_test.go index 5b7f07d81..9f2f6ee35 100644 --- a/internal/pipe/brew/brew_test.go +++ b/internal/pipe/brew/brew_test.go @@ -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 diff --git a/internal/pipe/release/release_test.go b/internal/pipe/release/release_test.go index 2df78c456..cb0273ee3 100644 --- a/internal/pipe/release/release_test.go +++ b/internal/pipe/release/release_test.go @@ -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) diff --git a/internal/pipe/scoop/scoop.go b/internal/pipe/scoop/scoop.go index 26b703003..1e7c77b06 100644 --- a/internal/pipe/scoop/scoop.go +++ b/internal/pipe/scoop/scoop.go @@ -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, diff --git a/internal/pipe/scoop/scoop_test.go b/internal/pipe/scoop/scoop_test.go index 32098ba38..5ba5f04fa 100644 --- a/internal/pipe/scoop/scoop_test.go +++ b/internal/pipe/scoop/scoop_test.go @@ -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 diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go index 24d18e4e6..a8e45002a 100644 --- a/internal/tmpl/tmpl.go +++ b/internal/tmpl/tmpl.go @@ -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 == "" { diff --git a/internal/tmpl/tmpl_test.go b/internal/tmpl/tmpl_test.go index 6c3e78477..3b34720d5 100644 --- a/internal/tmpl/tmpl_test.go +++ b/internal/tmpl/tmpl_test.go @@ -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" diff --git a/pkg/config/config.go b/pkg/config/config.go index f83b5d63a..5009145fd 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -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"` diff --git a/www/docs/customization/homebrew.md b/www/docs/customization/homebrew.md index 15bd20684..12e3306f5 100644 --- a/www/docs/customization/homebrew.md +++ b/www/docs/customization/homebrew.md @@ -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///releases/download/{{ .Tag }}/{{ .ArtifactName }}" diff --git a/www/docs/customization/scoop.md b/www/docs/customization/scoop.md index 3477c1205..b4c95b708 100644 --- a/www/docs/customization/scoop.md +++ b/www/docs/customization/scoop.md @@ -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.