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 }