github.com/goreleaser/goreleaser@v1.25.1/internal/tmpl/tmpl.go (about)

     1  // Package tmpl provides templating utilities for goreleaser.
     2  package tmpl
     3  
     4  import (
     5  	"bytes"
     6  	"fmt"
     7  	"path/filepath"
     8  	"regexp"
     9  	"strings"
    10  	"text/template"
    11  	"time"
    12  
    13  	"github.com/Masterminds/semver/v3"
    14  	"github.com/goreleaser/goreleaser/internal/artifact"
    15  	"github.com/goreleaser/goreleaser/pkg/build"
    16  	"github.com/goreleaser/goreleaser/pkg/context"
    17  	"golang.org/x/text/cases"
    18  	"golang.org/x/text/language"
    19  )
    20  
    21  // Template holds data that can be applied to a template string.
    22  type Template struct {
    23  	fields Fields
    24  }
    25  
    26  // Fields that will be available to the template engine.
    27  type Fields map[string]interface{}
    28  
    29  const (
    30  	// general keys.
    31  	projectName     = "ProjectName"
    32  	version         = "Version"
    33  	rawVersion      = "RawVersion"
    34  	tag             = "Tag"
    35  	previousTag     = "PreviousTag"
    36  	branch          = "Branch"
    37  	commit          = "Commit"
    38  	shortCommit     = "ShortCommit"
    39  	fullCommit      = "FullCommit"
    40  	commitDate      = "CommitDate"
    41  	commitTimestamp = "CommitTimestamp"
    42  	gitURL          = "GitURL"
    43  	summary         = "Summary"
    44  	tagSubject      = "TagSubject"
    45  	tagContents     = "TagContents"
    46  	tagBody         = "TagBody"
    47  	releaseURL      = "ReleaseURL"
    48  	isGitDirty      = "IsGitDirty"
    49  	isGitClean      = "IsGitClean"
    50  	gitTreeState    = "GitTreeState"
    51  	major           = "Major"
    52  	minor           = "Minor"
    53  	patch           = "Patch"
    54  	prerelease      = "Prerelease"
    55  	isSnapshot      = "IsSnapshot"
    56  	isNightly       = "IsNightly"
    57  	isDraft         = "IsDraft"
    58  	env             = "Env"
    59  	date            = "Date"
    60  	now             = "Now"
    61  	timestamp       = "Timestamp"
    62  	modulePath      = "ModulePath"
    63  	releaseNotes    = "ReleaseNotes"
    64  	runtimeK        = "Runtime"
    65  
    66  	// artifact-only keys.
    67  	osKey        = "Os"
    68  	amd64        = "Amd64"
    69  	arch         = "Arch"
    70  	arm          = "Arm"
    71  	mips         = "Mips"
    72  	binary       = "Binary"
    73  	artifactName = "ArtifactName"
    74  	artifactExt  = "ArtifactExt"
    75  	artifactPath = "ArtifactPath"
    76  
    77  	// build keys.
    78  	name   = "Name"
    79  	ext    = "Ext"
    80  	path   = "Path"
    81  	target = "Target"
    82  )
    83  
    84  // New Template.
    85  func New(ctx *context.Context) *Template {
    86  	sv := ctx.Semver
    87  	rawVersionV := fmt.Sprintf("%d.%d.%d", sv.Major, sv.Minor, sv.Patch)
    88  	treeState := "clean"
    89  	if ctx.Git.Dirty {
    90  		treeState = "dirty"
    91  	}
    92  
    93  	fields := map[string]interface{}{}
    94  	for k, v := range map[string]interface{}{
    95  		projectName:     ctx.Config.ProjectName,
    96  		modulePath:      ctx.ModulePath,
    97  		version:         ctx.Version,
    98  		rawVersion:      rawVersionV,
    99  		summary:         ctx.Git.Summary,
   100  		tag:             ctx.Git.CurrentTag,
   101  		previousTag:     ctx.Git.PreviousTag,
   102  		branch:          ctx.Git.Branch,
   103  		commit:          ctx.Git.Commit,
   104  		shortCommit:     ctx.Git.ShortCommit,
   105  		fullCommit:      ctx.Git.FullCommit,
   106  		commitDate:      ctx.Git.CommitDate.UTC().Format(time.RFC3339),
   107  		commitTimestamp: ctx.Git.CommitDate.UTC().Unix(),
   108  		gitURL:          ctx.Git.URL,
   109  		isGitDirty:      ctx.Git.Dirty,
   110  		isGitClean:      !ctx.Git.Dirty,
   111  		gitTreeState:    treeState,
   112  		env:             ctx.Env,
   113  		date:            ctx.Date.UTC().Format(time.RFC3339),
   114  		timestamp:       ctx.Date.UTC().Unix(),
   115  		now:             ctx.Date.UTC(),
   116  		major:           ctx.Semver.Major,
   117  		minor:           ctx.Semver.Minor,
   118  		patch:           ctx.Semver.Patch,
   119  		prerelease:      ctx.Semver.Prerelease,
   120  		isSnapshot:      ctx.Snapshot,
   121  		isNightly:       false,
   122  		isDraft:         ctx.Config.Release.Draft,
   123  		releaseNotes:    ctx.ReleaseNotes,
   124  		releaseURL:      ctx.ReleaseURL,
   125  		tagSubject:      ctx.Git.TagSubject,
   126  		tagContents:     ctx.Git.TagContents,
   127  		tagBody:         ctx.Git.TagBody,
   128  		runtimeK:        ctx.Runtime,
   129  	} {
   130  		fields[k] = v
   131  	}
   132  
   133  	return &Template{
   134  		fields: fields,
   135  	}
   136  }
   137  
   138  // WithEnvS overrides template's env field with the given KEY=VALUE list of
   139  // environment variables.
   140  func (t *Template) WithEnvS(envs []string) *Template {
   141  	result := map[string]string{}
   142  	for _, env := range envs {
   143  		k, v, ok := strings.Cut(env, "=")
   144  		if !ok || k == "" {
   145  			continue
   146  		}
   147  		result[k] = v
   148  	}
   149  	return t.WithEnv(result)
   150  }
   151  
   152  // WithEnv overrides template's env field with the given environment map.
   153  func (t *Template) WithEnv(e map[string]string) *Template {
   154  	t.fields[env] = e
   155  	return t
   156  }
   157  
   158  // WithExtraFields allows to add new more custom fields to the template.
   159  // It will override fields with the same name.
   160  func (t *Template) WithExtraFields(f Fields) *Template {
   161  	for k, v := range f {
   162  		t.fields[k] = v
   163  	}
   164  	return t
   165  }
   166  
   167  // WithArtifact populates Fields from the artifact.
   168  func (t *Template) WithArtifact(a *artifact.Artifact) *Template {
   169  	t.fields[osKey] = a.Goos
   170  	t.fields[arch] = a.Goarch
   171  	t.fields[arm] = a.Goarm
   172  	t.fields[mips] = a.Gomips
   173  	t.fields[amd64] = a.Goamd64
   174  	t.fields[binary] = artifact.ExtraOr(*a, binary, t.fields[projectName].(string))
   175  	t.fields[artifactName] = a.Name
   176  	t.fields[artifactExt] = artifact.ExtraOr(*a, artifact.ExtraExt, "")
   177  	t.fields[artifactPath] = a.Path
   178  	return t
   179  }
   180  
   181  func (t *Template) WithBuildOptions(opts build.Options) *Template {
   182  	return t.WithExtraFields(buildOptsToFields(opts))
   183  }
   184  
   185  func buildOptsToFields(opts build.Options) Fields {
   186  	return Fields{
   187  		target: opts.Target,
   188  		ext:    opts.Ext,
   189  		name:   opts.Name,
   190  		path:   opts.Path,
   191  		osKey:  opts.Goos,
   192  		arch:   opts.Goarch,
   193  		arm:    opts.Goarm,
   194  		amd64:  opts.Goamd64,
   195  		mips:   opts.Gomips,
   196  	}
   197  }
   198  
   199  // Bool Apply the given string, and converts it to a bool.
   200  func (t *Template) Bool(s string) (bool, error) {
   201  	r, err := t.Apply(s)
   202  	return strings.TrimSpace(strings.ToLower(r)) == "true", err
   203  }
   204  
   205  // Apply applies the given string against the Fields stored in the template.
   206  func (t *Template) Apply(s string) (string, error) {
   207  	var out bytes.Buffer
   208  	tmpl, err := template.New("tmpl").
   209  		Option("missingkey=error").
   210  		Funcs(template.FuncMap{
   211  			"replace": strings.ReplaceAll,
   212  			"split":   strings.Split,
   213  			"time": func(s string) string {
   214  				return time.Now().UTC().Format(s)
   215  			},
   216  			"contains":       strings.Contains,
   217  			"tolower":        strings.ToLower,
   218  			"toupper":        strings.ToUpper,
   219  			"trim":           strings.TrimSpace,
   220  			"trimprefix":     strings.TrimPrefix,
   221  			"trimsuffix":     strings.TrimSuffix,
   222  			"title":          cases.Title(language.English).String,
   223  			"dir":            filepath.Dir,
   224  			"base":           filepath.Base,
   225  			"abs":            filepath.Abs,
   226  			"incmajor":       incMajor,
   227  			"incminor":       incMinor,
   228  			"incpatch":       incPatch,
   229  			"filter":         filter(false),
   230  			"reverseFilter":  filter(true),
   231  			"mdv2escape":     mdv2Escape,
   232  			"envOrDefault":   t.envOrDefault,
   233  			"map":            makemap,
   234  			"indexOrDefault": indexOrDefault,
   235  		}).
   236  		Parse(s)
   237  	if err != nil {
   238  		return "", newTmplError(s, err)
   239  	}
   240  
   241  	err = tmpl.Execute(&out, t.fields)
   242  	return out.String(), newTmplError(s, err)
   243  }
   244  
   245  // ApplyAll applies all the given strings against the Fields stored in the
   246  // template. Application stops as soon as an error is encountered.
   247  func (t *Template) ApplyAll(sps ...*string) error {
   248  	for _, sp := range sps {
   249  		s := *sp
   250  		result, err := t.Apply(s)
   251  		if err != nil {
   252  			return newTmplError(s, err)
   253  		}
   254  		*sp = result
   255  	}
   256  	return nil
   257  }
   258  
   259  func (t *Template) envOrDefault(name, value string) string {
   260  	s, ok := t.fields[env].(context.Env)[name]
   261  	if !ok {
   262  		return value
   263  	}
   264  	return s
   265  }
   266  
   267  type ExpectedSingleEnvErr struct{}
   268  
   269  func (e ExpectedSingleEnvErr) Error() string {
   270  	return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)"
   271  }
   272  
   273  var envOnlyRe = regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`)
   274  
   275  // ApplySingleEnvOnly enforces template to only contain a single environment variable
   276  // and nothing else.
   277  func (t *Template) ApplySingleEnvOnly(s string) (string, error) {
   278  	s = strings.TrimSpace(s)
   279  	if len(s) == 0 {
   280  		return "", nil
   281  	}
   282  
   283  	// text/template/parse (lexer) could be used here too,
   284  	// but regexp reduces the complexity and should be sufficient,
   285  	// given the context is mostly discouraging users from bad practice
   286  	// of hard-coded credentials, rather than catch all possible cases
   287  	if !envOnlyRe.MatchString(s) {
   288  		return "", ExpectedSingleEnvErr{}
   289  	}
   290  
   291  	var out bytes.Buffer
   292  	tmpl, err := template.New("tmpl").
   293  		Option("missingkey=error").
   294  		Parse(s)
   295  	if err != nil {
   296  		return "", err
   297  	}
   298  
   299  	err = tmpl.Execute(&out, t.fields)
   300  	return out.String(), err
   301  }
   302  
   303  func incMajor(v string) string {
   304  	return prefix(v) + semver.MustParse(v).IncMajor().String()
   305  }
   306  
   307  func incMinor(v string) string {
   308  	return prefix(v) + semver.MustParse(v).IncMinor().String()
   309  }
   310  
   311  func incPatch(v string) string {
   312  	return prefix(v) + semver.MustParse(v).IncPatch().String()
   313  }
   314  
   315  func prefix(v string) string {
   316  	if v != "" && v[0] == 'v' {
   317  		return "v"
   318  	}
   319  	return ""
   320  }
   321  
   322  func filter(reverse bool) func(content, exp string) string {
   323  	return func(content, exp string) string {
   324  		re := regexp.MustCompilePOSIX(exp)
   325  		var lines []string
   326  		for _, line := range strings.Split(content, "\n") {
   327  			if reverse && re.MatchString(line) {
   328  				continue
   329  			}
   330  			if !reverse && !re.MatchString(line) {
   331  				continue
   332  			}
   333  			lines = append(lines, line)
   334  		}
   335  
   336  		return strings.Join(lines, "\n")
   337  	}
   338  }
   339  
   340  func mdv2Escape(s string) string {
   341  	return strings.NewReplacer(
   342  		"_", "\\_",
   343  		"*", "\\*",
   344  		"[", "\\[",
   345  		"]", "\\]",
   346  		"(", "\\(",
   347  		")", "\\)",
   348  		"~", "\\~",
   349  		"`", "\\`",
   350  		">", "\\>",
   351  		"#", "\\#",
   352  		"+", "\\+",
   353  		"-", "\\-",
   354  		"=", "\\=",
   355  		"|", "\\|",
   356  		"{", "\\{",
   357  		"}", "\\}",
   358  		".", "\\.",
   359  		"!", "\\!",
   360  	).Replace(s)
   361  }
   362  
   363  func makemap(kvs ...string) (map[string]string, error) {
   364  	if len(kvs)%2 != 0 {
   365  		return nil, fmt.Errorf("map expects even number of arguments, got %d", len(kvs))
   366  	}
   367  	m := make(map[string]string)
   368  	for i := 0; i < len(kvs); i += 2 {
   369  		m[kvs[i]] = kvs[i+1]
   370  	}
   371  	return m, nil
   372  }
   373  
   374  func indexOrDefault(m map[string]string, name, value string) string {
   375  	s, ok := m[name]
   376  	if ok {
   377  		return s
   378  	}
   379  	return value
   380  }