github.com/joselitofilho/goreleaser@v0.155.1-0.20210123221854-e4891856c593/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/goreleaser/goreleaser/internal/artifact" 14 "github.com/goreleaser/goreleaser/pkg/build" 15 "github.com/goreleaser/goreleaser/pkg/context" 16 ) 17 18 // Template holds data that can be applied to a template string. 19 type Template struct { 20 fields Fields 21 } 22 23 // Fields that will be available to the template engine. 24 type Fields map[string]interface{} 25 26 const ( 27 // general keys. 28 projectName = "ProjectName" 29 version = "Version" 30 rawVersion = "RawVersion" 31 tag = "Tag" 32 branch = "Branch" 33 commit = "Commit" 34 shortCommit = "ShortCommit" 35 fullCommit = "FullCommit" 36 commitDate = "CommitDate" 37 commitTimestamp = "CommitTimestamp" 38 gitURL = "GitURL" 39 major = "Major" 40 minor = "Minor" 41 patch = "Patch" 42 prerelease = "Prerelease" 43 isSnapshot = "IsSnapshot" 44 env = "Env" 45 date = "Date" 46 timestamp = "Timestamp" 47 48 // artifact-only keys. 49 osKey = "Os" 50 arch = "Arch" 51 arm = "Arm" 52 mips = "Mips" 53 binary = "Binary" 54 artifactName = "ArtifactName" 55 artifactPath = "ArtifactPath" 56 57 // gitlab only. 58 artifactUploadHash = "ArtifactUploadHash" 59 60 // build keys. 61 name = "Name" 62 ext = "Ext" 63 path = "Path" 64 target = "Target" 65 ) 66 67 // New Template. 68 func New(ctx *context.Context) *Template { 69 sv := ctx.Semver 70 rawVersionV := fmt.Sprintf("%d.%d.%d", sv.Major, sv.Minor, sv.Patch) 71 72 return &Template{ 73 fields: Fields{ 74 projectName: ctx.Config.ProjectName, 75 version: ctx.Version, 76 rawVersion: rawVersionV, 77 tag: ctx.Git.CurrentTag, 78 branch: ctx.Git.Branch, 79 commit: ctx.Git.Commit, 80 shortCommit: ctx.Git.ShortCommit, 81 fullCommit: ctx.Git.FullCommit, 82 commitDate: ctx.Git.CommitDate.UTC().Format(time.RFC3339), 83 commitTimestamp: ctx.Git.CommitDate.UTC().Unix(), 84 gitURL: ctx.Git.URL, 85 env: ctx.Env, 86 date: ctx.Date.UTC().Format(time.RFC3339), 87 timestamp: ctx.Date.UTC().Unix(), 88 major: ctx.Semver.Major, 89 minor: ctx.Semver.Minor, 90 patch: ctx.Semver.Patch, 91 prerelease: ctx.Semver.Prerelease, 92 isSnapshot: ctx.Snapshot, 93 }, 94 } 95 } 96 97 // WithEnvS overrides template's env field with the given KEY=VALUE list of 98 // environment variables. 99 func (t *Template) WithEnvS(envs []string) *Template { 100 var result = map[string]string{} 101 for _, env := range envs { 102 var parts = strings.SplitN(env, "=", 2) 103 result[parts[0]] = parts[1] 104 } 105 return t.WithEnv(result) 106 } 107 108 // WithEnv overrides template's env field with the given environment map. 109 func (t *Template) WithEnv(e map[string]string) *Template { 110 t.fields[env] = e 111 return t 112 } 113 114 // WithExtraFields allows to add new more custom fields to the template. 115 // It will override fields with the same name. 116 func (t *Template) WithExtraFields(f Fields) *Template { 117 for k, v := range f { 118 t.fields[k] = v 119 } 120 return t 121 } 122 123 // WithArtifact populates Fields from the artifact and replacements. 124 func (t *Template) WithArtifact(a *artifact.Artifact, replacements map[string]string) *Template { 125 var bin = a.Extra[binary] 126 if bin == nil { 127 bin = t.fields[projectName] 128 } 129 t.fields[osKey] = replace(replacements, a.Goos) 130 t.fields[arch] = replace(replacements, a.Goarch) 131 t.fields[arm] = replace(replacements, a.Goarm) 132 t.fields[mips] = replace(replacements, a.Gomips) 133 t.fields[binary] = bin.(string) 134 t.fields[artifactName] = a.Name 135 t.fields[artifactPath] = a.Path 136 if val, ok := a.Extra["ArtifactUploadHash"]; ok { 137 t.fields[artifactUploadHash] = val 138 } else { 139 t.fields[artifactUploadHash] = "" 140 } 141 return t 142 } 143 144 func (t *Template) WithBuildOptions(opts build.Options) *Template { 145 return t.WithExtraFields(buildOptsToFields(opts)) 146 } 147 148 func buildOptsToFields(opts build.Options) Fields { 149 return Fields{ 150 target: opts.Target, 151 ext: opts.Ext, 152 name: opts.Name, 153 path: opts.Path, 154 osKey: opts.Os, 155 arch: opts.Arch, 156 } 157 } 158 159 // Apply applies the given string against the Fields stored in the template. 160 func (t *Template) Apply(s string) (string, error) { 161 var out bytes.Buffer 162 tmpl, err := template.New("tmpl"). 163 Option("missingkey=error"). 164 Funcs(template.FuncMap{ 165 "replace": strings.ReplaceAll, 166 "time": func(s string) string { 167 return time.Now().UTC().Format(s) 168 }, 169 "tolower": strings.ToLower, 170 "toupper": strings.ToUpper, 171 "trim": strings.TrimSpace, 172 "dir": filepath.Dir, 173 "abs": filepath.Abs, 174 }). 175 Parse(s) 176 if err != nil { 177 return "", err 178 } 179 180 err = tmpl.Execute(&out, t.fields) 181 return out.String(), err 182 } 183 184 type ExpectedSingleEnvErr struct{} 185 186 func (e ExpectedSingleEnvErr) Error() string { 187 return "expected {{ .Env.VAR_NAME }} only (no plain-text or other interpolation)" 188 } 189 190 // ApplySingleEnvOnly enforces template to only contain a single environment variable 191 // and nothing else. 192 func (t *Template) ApplySingleEnvOnly(s string) (string, error) { 193 s = strings.TrimSpace(s) 194 if len(s) == 0 { 195 return "", nil 196 } 197 198 // text/template/parse (lexer) could be used here too, 199 // but regexp reduces the complexity and should be sufficient, 200 // given the context is mostly discouraging users from bad practice 201 // of hard-coded credentials, rather than catch all possible cases 202 envOnlyRe := regexp.MustCompile(`^{{\s*\.Env\.[^.\s}]+\s*}}$`) 203 if !envOnlyRe.Match([]byte(s)) { 204 return "", ExpectedSingleEnvErr{} 205 } 206 207 var out bytes.Buffer 208 tmpl, err := template.New("tmpl"). 209 Option("missingkey=error"). 210 Parse(s) 211 if err != nil { 212 return "", err 213 } 214 215 err = tmpl.Execute(&out, t.fields) 216 return out.String(), err 217 } 218 219 func replace(replacements map[string]string, original string) string { 220 result := replacements[original] 221 if result == "" { 222 return original 223 } 224 return result 225 }