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 }