github.com/triarius/goreleaser@v1.12.5/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/triarius/goreleaser/internal/artifact"
    15  	"github.com/triarius/goreleaser/pkg/build"
    16  	"github.com/triarius/goreleaser/pkg/context"
    17  )
    18  
    19  // Template holds data that can be applied to a template string.
    20  type Template struct {
    21  	fields Fields
    22  }
    23  
    24  // Fields that will be available to the template engine.
    25  type Fields map[string]interface{}
    26  
    27  const (
    28  	// general keys.
    29  	projectName     = "ProjectName"
    30  	version         = "Version"
    31  	rawVersion      = "RawVersion"
    32  	tag             = "Tag"
    33  	previousTag     = "PreviousTag"
    34  	branch          = "Branch"
    35  	commit          = "Commit"
    36  	shortCommit     = "ShortCommit"
    37  	fullCommit      = "FullCommit"
    38  	commitDate      = "CommitDate"
    39  	commitTimestamp = "CommitTimestamp"
    40  	gitURL          = "GitURL"
    41  	summary         = "Summary"
    42  	tagSubject      = "TagSubject"
    43  	tagContents     = "TagContents"
    44  	tagBody         = "TagBody"
    45  	releaseURL      = "ReleaseURL"
    46  	major           = "Major"
    47  	minor           = "Minor"
    48  	patch           = "Patch"
    49  	prerelease      = "Prerelease"
    50  	isSnapshot      = "IsSnapshot"
    51  	env             = "Env"
    52  	date            = "Date"
    53  	timestamp       = "Timestamp"
    54  	modulePath      = "ModulePath"
    55  	releaseNotes    = "ReleaseNotes"
    56  	runtimeK        = "Runtime"
    57  
    58  	// artifact-only keys.
    59  	osKey        = "Os"
    60  	amd64        = "Amd64"
    61  	arch         = "Arch"
    62  	arm          = "Arm"
    63  	mips         = "Mips"
    64  	binary       = "Binary"
    65  	artifactName = "ArtifactName"
    66  	artifactExt  = "ArtifactExt"
    67  	artifactPath = "ArtifactPath"
    68  
    69  	// build keys.
    70  	name   = "Name"
    71  	ext    = "Ext"
    72  	path   = "Path"
    73  	target = "Target"
    74  )
    75  
    76  // New Template.
    77  func New(ctx *context.Context) *Template {
    78  	sv := ctx.Semver
    79  	rawVersionV := fmt.Sprintf("%d.%d.%d", sv.Major, sv.Minor, sv.Patch)
    80  
    81  	return &Template{
    82  		fields: Fields{
    83  			projectName:     ctx.Config.ProjectName,
    84  			modulePath:      ctx.ModulePath,
    85  			version:         ctx.Version,
    86  			rawVersion:      rawVersionV,
    87  			tag:             ctx.Git.CurrentTag,
    88  			previousTag:     ctx.Git.PreviousTag,
    89  			branch:          ctx.Git.Branch,
    90  			commit:          ctx.Git.Commit,
    91  			shortCommit:     ctx.Git.ShortCommit,
    92  			fullCommit:      ctx.Git.FullCommit,
    93  			commitDate:      ctx.Git.CommitDate.UTC().Format(time.RFC3339),
    94  			commitTimestamp: ctx.Git.CommitDate.UTC().Unix(),
    95  			gitURL:          ctx.Git.URL,
    96  			summary:         ctx.Git.Summary,
    97  			tagSubject:      ctx.Git.TagSubject,
    98  			tagContents:     ctx.Git.TagContents,
    99  			tagBody:         ctx.Git.TagBody,
   100  			releaseURL:      ctx.ReleaseURL,
   101  			env:             ctx.Env,
   102  			date:            ctx.Date.UTC().Format(time.RFC3339),
   103  			timestamp:       ctx.Date.UTC().Unix(),
   104  			major:           ctx.Semver.Major,
   105  			minor:           ctx.Semver.Minor,
   106  			patch:           ctx.Semver.Patch,
   107  			prerelease:      ctx.Semver.Prerelease,
   108  			isSnapshot:      ctx.Snapshot,
   109  			releaseNotes:    ctx.ReleaseNotes,
   110  			runtimeK:        ctx.Runtime,
   111  		},
   112  	}
   113  }
   114  
   115  // WithEnvS overrides template's env field with the given KEY=VALUE list of
   116  // environment variables.
   117  func (t *Template) WithEnvS(envs []string) *Template {
   118  	result := map[string]string{}
   119  	for _, env := range envs {
   120  		k, v, _ := strings.Cut(env, "=")
   121  		result[k] = v
   122  	}
   123  	return t.WithEnv(result)
   124  }
   125  
   126  // WithEnv overrides template's env field with the given environment map.
   127  func (t *Template) WithEnv(e map[string]string) *Template {
   128  	t.fields[env] = e
   129  	return t
   130  }
   131  
   132  // WithExtraFields allows to add new more custom fields to the template.
   133  // It will override fields with the same name.
   134  func (t *Template) WithExtraFields(f Fields) *Template {
   135  	for k, v := range f {
   136  		t.fields[k] = v
   137  	}
   138  	return t
   139  }
   140  
   141  // WithArtifact populates Fields from the artifact and replacements.
   142  func (t *Template) WithArtifact(a *artifact.Artifact, replacements map[string]string) *Template {
   143  	t.fields[osKey] = replace(replacements, a.Goos)
   144  	t.fields[arch] = replace(replacements, a.Goarch)
   145  	t.fields[arm] = replace(replacements, a.Goarm)
   146  	t.fields[mips] = replace(replacements, a.Gomips)
   147  	t.fields[amd64] = replace(replacements, a.Goamd64)
   148  	t.fields[binary] = artifact.ExtraOr(*a, binary, t.fields[projectName].(string))
   149  	t.fields[artifactName] = a.Name
   150  	t.fields[artifactExt] = artifact.ExtraOr(*a, artifact.ExtraExt, "")
   151  	t.fields[artifactPath] = a.Path
   152  	return t
   153  }
   154  
   155  func (t *Template) WithBuildOptions(opts build.Options) *Template {
   156  	return t.WithExtraFields(buildOptsToFields(opts))
   157  }
   158  
   159  func buildOptsToFields(opts build.Options) Fields {
   160  	return Fields{
   161  		target: opts.Target,
   162  		ext:    opts.Ext,
   163  		name:   opts.Name,
   164  		path:   opts.Path,
   165  		osKey:  opts.Goos,
   166  		arch:   opts.Goarch,
   167  		arm:    opts.Goarm,
   168  		mips:   opts.Gomips,
   169  	}
   170  }
   171  
   172  // Apply applies the given string against the Fields stored in the template.
   173  func (t *Template) Apply(s string) (string, error) {
   174  	var out bytes.Buffer
   175  	tmpl, err := template.New("tmpl").
   176  		Option("missingkey=error").
   177  		Funcs(template.FuncMap{
   178  			"replace": strings.ReplaceAll,
   179  			"split":   strings.Split,
   180  			"time": func(s string) string {
   181  				return time.Now().UTC().Format(s)
   182  			},
   183  			"tolower":       strings.ToLower,
   184  			"toupper":       strings.ToUpper,
   185  			"trim":          strings.TrimSpace,
   186  			"trimprefix":    strings.TrimPrefix,
   187  			"trimsuffix":    strings.TrimSuffix,
   188  			"dir":           filepath.Dir,
   189  			"abs":           filepath.Abs,
   190  			"incmajor":      incMajor,
   191  			"incminor":      incMinor,
   192  			"incpatch":      incPatch,
   193  			"filter":        filter(false),
   194  			"reverseFilter": filter(true),
   195  		}).
   196  		Parse(s)
   197  	if err != nil {
   198  		return "", err
   199  	}
   200  
   201  	err = tmpl.Execute(&out, t.fields)
   202  	return out.String(), err
   203  }
   204  
   205  type ExpectedSingleEnvErr struct{}
   206  
   207  func (e ExpectedSingleEnvErr) Error() string {
   208  	return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)"
   209  }
   210  
   211  // ApplySingleEnvOnly enforces template to only contain a single environment variable
   212  // and nothing else.
   213  func (t *Template) ApplySingleEnvOnly(s string) (string, error) {
   214  	s = strings.TrimSpace(s)
   215  	if len(s) == 0 {
   216  		return "", nil
   217  	}
   218  
   219  	// text/template/parse (lexer) could be used here too,
   220  	// but regexp reduces the complexity and should be sufficient,
   221  	// given the context is mostly discouraging users from bad practice
   222  	// of hard-coded credentials, rather than catch all possible cases
   223  	envOnlyRe := regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`)
   224  	if !envOnlyRe.Match([]byte(s)) {
   225  		return "", ExpectedSingleEnvErr{}
   226  	}
   227  
   228  	var out bytes.Buffer
   229  	tmpl, err := template.New("tmpl").
   230  		Option("missingkey=error").
   231  		Parse(s)
   232  	if err != nil {
   233  		return "", err
   234  	}
   235  
   236  	err = tmpl.Execute(&out, t.fields)
   237  	return out.String(), err
   238  }
   239  
   240  func replace(replacements map[string]string, original string) string {
   241  	result := replacements[original]
   242  	if result == "" {
   243  		return original
   244  	}
   245  	return result
   246  }
   247  
   248  func incMajor(v string) string {
   249  	return prefix(v) + semver.MustParse(v).IncMajor().String()
   250  }
   251  
   252  func incMinor(v string) string {
   253  	return prefix(v) + semver.MustParse(v).IncMinor().String()
   254  }
   255  
   256  func incPatch(v string) string {
   257  	return prefix(v) + semver.MustParse(v).IncPatch().String()
   258  }
   259  
   260  func prefix(v string) string {
   261  	if v != "" && v[0] == 'v' {
   262  		return "v"
   263  	}
   264  	return ""
   265  }
   266  
   267  func filter(reverse bool) func(content, exp string) string {
   268  	return func(content, exp string) string {
   269  		re := regexp.MustCompilePOSIX(exp)
   270  		var lines []string
   271  		for _, line := range strings.Split(content, "\n") {
   272  			if reverse && re.MatchString(line) {
   273  				continue
   274  			}
   275  			if !reverse && !re.MatchString(line) {
   276  				continue
   277  			}
   278  			lines = append(lines, line)
   279  		}
   280  
   281  		return strings.Join(lines, "\n")
   282  	}
   283  }