Repository: goreleaser/goreleaser
Pull request #2005 (2021-01-08 08:18:56)

feat(http): resolve custom header template expressions
<!--

Hi, thanks for contributing!

Please make sure you read our CONTRIBUTING guide.

Please fill the fields above:

-->

If applied, this commit will allow the user to employ template expressions in the values of `uploads.custom_headers` map values, e.g.:

```yaml
uploads:
  - name: gitlab
    mode: binary
    target: "{{ .Env.CI_API_V4_URL }}/projects/{{ .Env.CI_PROJECT_ID }}/packages/generic/{{ .ProjectName }}/{{ .Version }}/"
    username: gitlab-ci-token
    method: PUT
    custom_headers:
      JOB-TOKEN: "{{ .Env.CI_JOB_TOKEN }}"
      PRIVATE-TOKEN: "{{ .Env.GITLAB_TOKEN }}"
```

Prior to this change, only literal expressions were supported, and so my docs example in #2002 was actually not functioning.

<!-- # Provide links to any relevant tickets, URLs or other resources -->


vercel[bot]: @sirlatrom is attempting to deploy a commit to the **Goreleaser** Team on [Vercel](https://vercel.com).

A member of the Team first needs to [authorize it](https://vercel.com/git/authorize?team=Goreleaser&type=github&job=%7B%22headInfo%22%3A%7B%22sha%22%3A%22d41d7d067073249efb79155810b55ddc7f118157%22%7D%2C%22id%22%3A%22QmYQwRXM6w3S1YoizPWcXMaabwMXSKvQWFxdfvxTEX7JXn%22%2C%22org%22%3A%22goreleaser%22%2C%22prId%22%3A2005%2C%22repo%22%3A%22goreleaser%22%7D).


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=h1) Report
> Merging [#2005](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=desc) (d41d7d0) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/e8ea231122dc98ec2315eff2df2defc5191764d6?el=desc) (e8ea231) will **decrease** coverage by `0.13%`.
> The diff coverage is `50.00%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2005/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN)](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2005      +/-   ##
==========================================
- Coverage   82.86%   82.72%   -0.14%     
==========================================
  Files          74       74              
  Lines        3974     3989      +15     
==========================================
+ Hits         3293     3300       +7     
- Misses        555      562       +7     
- Partials      126      127       +1     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [internal/http/http.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2005/diff?src=pr&el=tree#diff-aW50ZXJuYWwvaHR0cC9odHRwLmdv) | `85.86% <50.00%> (-3.35%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=footer). Last update [e8ea231...d41d7d0](https://codecov.io/gh/goreleaser/goreleaser/pull/2005?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

caarlos0: Thanks, I didn't notice while reviewing as well 🥴
sirlatrom: Sorry, should have marked as a draft until now - missed the test that validates the happy path, added now in 122d23f.

Patch:
diff --git a/internal/http/http.go b/internal/http/http.go
index 15e7b949..ce1819ee 100644
--- a/internal/http/http.go
+++ b/internal/http/http.go
@@ -242,7 +242,17 @@ func uploadAsset(ctx *context.Context, upload *config.Upload, artifact *artifact
 	var headers = map[string]string{}
 	if upload.CustomHeaders != nil {
 		for name, value := range upload.CustomHeaders {
-			headers[name] = value
+			resolvedValue, err := resolveHeaderTemplate(ctx, upload, artifact, value)
+			if err != nil {
+				msg := fmt.Sprintf("%s: failed to resolve custom_headers template", kind)
+				log.WithError(err).WithFields(log.Fields{
+					"instance":     upload.Name,
+					"header_name":  name,
+					"header_value": value,
+				}).Error(msg)
+				return fmt.Errorf("%s: %w", msg, err)
+			}
+			headers[name] = resolvedValue
 		}
 	}
 	if upload.ChecksumHeader != "" {
@@ -367,3 +377,16 @@ func resolveTargetTemplate(ctx *context.Context, upload *config.Upload, artifact
 		WithArtifact(artifact, replacements).
 		Apply(upload.Target)
 }
+
+// resolveHeaderTemplate returns the resolved custom header template with replaced variables
+// Those variables can be replaced by the given context, goos, goarch, goarm and more.
+func resolveHeaderTemplate(ctx *context.Context, upload *config.Upload, artifact *artifact.Artifact, headerValue string) (string, error) {
+	var replacements = map[string]string{}
+	if upload.Mode == ModeBinary {
+		// TODO: multiple archives here
+		replacements = ctx.Config.Archives[0].Replacements
+	}
+	return tmpl.New(ctx).
+		WithArtifact(artifact, replacements).
+		Apply(headerValue)
+}
diff --git a/www/docs/customization/upload.md b/www/docs/customization/upload.md
index 46cc0557..93f97618 100644
--- a/www/docs/customization/upload.md
+++ b/www/docs/customization/upload.md
@@ -164,7 +164,7 @@ uploads:
     # A map of custom headers e.g. to support required content types or auth schemes.
     # Default is empty.
     custom_headers:
-      JOB-TOKEN: {{ .Env.CI_JOB_TOKEN }}
+      JOB-TOKEN: "{{ .Env.CI_JOB_TOKEN }}"
 
     # Upload checksums (defaults to false)
     checksum: true


Test patch:
diff --git a/internal/http/http_test.go b/internal/http/http_test.go
index 87a3b20a..810755e8 100644
--- a/internal/http/http_test.go
+++ b/internal/http/http_test.go
@@ -484,6 +484,36 @@ func TestUpload(t *testing.T) {
 			},
 			checks(check{"/blah/2.1.0/a.ubi", "u2", "x", content, map[string]string{"x-custom-header-name": "custom-header-value"}}),
 		},
+		{"custom-headers-with-template", true, true, false, false,
+			func(s *httptest.Server) (*context.Context, config.Upload) {
+				return ctx, config.Upload{
+					Mode:     ModeBinary,
+					Name:     "a",
+					Target:   s.URL + "/{{.ProjectName}}/{{.Version}}/",
+					Username: "u2",
+					CustomHeaders: map[string]string{
+						"x-project-name": "{{ .ProjectName }}",
+					},
+					TrustedCerts: cert(s),
+				}
+			},
+			checks(check{"/blah/2.1.0/a.ubi", "u2", "x", content, map[string]string{"x-project-name": "blah"}}),
+		},
+		{"invalid-template-in-custom-headers", true, true, true, true,
+			func(s *httptest.Server) (*context.Context, config.Upload) {
+				return ctx, config.Upload{
+					Mode:     ModeBinary,
+					Name:     "a",
+					Target:   s.URL + "/{{.ProjectName}}/{{.Version}}/",
+					Username: "u2",
+					CustomHeaders: map[string]string{
+						"x-custom-header-name": "{{ .Env.NONEXISTINGVARIABLE and some bad expressions }}",
+					},
+					TrustedCerts: cert(s),
+				}
+			},
+			checks(),
+		},
 	}
 
 	uploadAndCheck := func(t *testing.T, setup func(*httptest.Server) (*context.Context, config.Upload), wantErrPlain, wantErrTLS bool, check func(r []*h.Request) error, srv *httptest.Server) {