Repository: goreleaser/goreleaser
Pull request #1641 (2020-07-02 02:00:34)

feat: deterministic / reproducible build support
<!--

Hi, thanks for contributing!

Please make sure you read our CONTRIBUTING guide.

Please fill the fields above:

-->

<!-- If applied, this commit will... -->
This is a proposed change to make mod times on binaries (and for the ldflags `.Date`, etc.) deterministic based on the commit time of the git commit for the build. This also changes the way sum files are written slightly to also ensure the ordering of the lines is deterministic so that signatures are reproducible.

<!-- Why is this change being made? -->
We've run in to issues where downstream steps in our release process fails, and we want to be able to rerun the pipeline, but doing so generates new sums and a new signature, confusing users as to thinking there is a change when there is not.

<!-- # Provide links to any relevant tickets, URLs or other resources -->
See https://www.kernel.org/doc/html/latest/kbuild/reproducible-builds.html#timestamps among many other references on how to do this.

Alternative we could zero the timestamps, but I figured since we have the git info available, this was the best option.

I'm on Windows, so I was unable to run some of the tests, etc, but I did manually test this a bit. This is more of a proposal than a finished PR as the testing is not in there yet, just wanted to gauge your receptiveness to the change.


vercel[bot]: 
This pull request is being automatically deployed with Vercel ([learn more](https://vercel.link/github-learn-more)).
To see the status of your deployment, click on the icon next to each commit.
	
paultyng: I reworked this to not branch based on the `Deterministic` flag, and instead just allow template overrides as suggested. I also added docs instructions for how to make your builds reproducible.

I did change GoReleaser's own config to be reproducible as well, but happy to back that out.
paultyng: Here is a quick example output from releasing GoReleaser (I commented out docker, snap, etc). You can see the hashes and the checksum file itself is consistent, even with a clean Go build cache.

```powershell
PS C:\...\goreleaser> go clean -cache
PS C:\...\goreleaser> ~/go/bin/goreleaser --snapshot --rm-dist

   • releasing...     
   • loading config file       file=.goreleaser.yml
   • running before hooks
      • running go mod download
   • loading environment variables
   • getting and validating git state
      • releasing v0.138.0, commit 9642c7de1898aef1200409945f5044e58d15dd2d
      • pipe skipped              error=disabled during snapshot mode
   • parsing tag
   • setting defaults 
      • snapshotting
      • github/gitlab/gitea releases
      • project name     
      • building binaries
      • creating source archive
      • archives
      • linux packages
      • snapcraft packages
      • calculating checksums
      • signing artifacts
      • docker images
      • artifactory
      • blobs
      • homebrew tap formula
      • scoop manifests
   • snapshotting     
   • checking ./dist
   • writing effective config file
      • writing                   config=dist\config.yaml
   • generating changelog
      • pipe skipped              error=not available for snapshots
   • building binaries
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_386\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_arm64\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_amd64\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_windows_amd64\goreleaser.exe
      • building                  binary=C:\...\goreleaser\dist\goreleaser_darwin_amd64\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_windows_386\goreleaser.exe
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_arm_6\goreleaser
   • archives
      • creating                  archive=dist\goreleaser_Linux_x86_64.tar.gz
      • creating                  archive=dist\goreleaser_Windows_i386.zip
      • creating                  archive=dist\goreleaser_Linux_i386.tar.gz
      • creating                  archive=dist\goreleaser_Darwin_x86_64.tar.gz
      • creating                  archive=dist\goreleaser_Windows_x86_64.zip
      • creating                  archive=dist\goreleaser_Linux_arm64.tar.gz
      • creating                  archive=dist\goreleaser_Linux_armv6.tar.gz
   • creating source archive
      • pipe skipped              error=source pipe is disabled
   • linux packages
   • snapcraft packages
   • calculating checksums
      • checksumming              file=goreleaser_Linux_arm64.tar.gz
      • checksumming              file=goreleaser_Linux_x86_64.tar.gz
      • checksumming              file=goreleaser_Windows_i386.zip
      • checksumming              file=goreleaser_Windows_x86_64.zip
      • checksumming              file=goreleaser_Darwin_x86_64.tar.gz
      • checksumming              file=goreleaser_Linux_i386.tar.gz
      • checksumming              file=goreleaser_Linux_armv6.tar.gz
   • signing artifacts
   • docker images
      • pipe skipped              error=docker section is not configured
   • publishing       
      • blobs
         • pipe skipped              error=blobs section is not configured
      • http upload
         • pipe skipped              error=uploads section is not configured
      • custom publisher 
         • pipe skipped              error=publishers section is not configured
      • artifactory      
         • pipe skipped              error=artifactory section is not configured
      • docker images    
         • pipe skipped              error=publishing is disabled
      • snapcraft packages
         • pipe skipped              error=publishing is disabled
      • github/gitlab/gitea releases
         • pipe skipped              error=publishing is disabled
      • homebrew tap formula
         • token type                type=
      • scoop manifests  
         • pipe skipped              error=publishing is disabled
   • release succeeded after 634.93s

PS C:\...\goreleaser> cat .\dist\goreleaser_checksums.txt
07328fd1ab9bb23dc7d61399f56a56637d42adaeb2f904d88fb96724dc009718  goreleaser_Linux_x86_64.tar.gz
18cf5a30f9d6de8c8fff22112bc28cde359ae5ff20264a03e1b36bc7b2453155  goreleaser_Darwin_x86_64.tar.gz
225fc912ba45d81f3a27b9e395ac473d9586f667e97dc6b49229eff66e125afa  goreleaser_Linux_arm64.tar.gz
4785fcebb838ebb8381ab6cae204f993007a5bb1fee8e69e24c4fbb97eaea8e1  goreleaser_Windows_i386.zip
950f0a7cf8258842abf464414c63653859ee8be25948b35dc384a4542ede35b3  goreleaser_Linux_i386.tar.gz
bc398a56906d9f10edb6ab682721e304a91f7dbde8e28f9ed0f5c505a121c1d5  goreleaser_Windows_x86_64.zip
df22835901a0d331e08940e5d9372fb7e96e7e2576b415b841f880dd3b879880  goreleaser_Linux_armv6.tar.gz
PS C:\...\goreleaser> go clean -cache
PS C:\...\goreleaser> ~/go/bin/goreleaser --snapshot --rm-dist

   • releasing...     
   • loading config file       file=.goreleaser.yml
   • running before hooks
      • running go mod download
   • loading environment variables
   • getting and validating git state
      • releasing v0.138.0, commit 9642c7de1898aef1200409945f5044e58d15dd2d
      • pipe skipped              error=disabled during snapshot mode
   • parsing tag
   • setting defaults
      • snapshotting     
      • github/gitlab/gitea releases
      • project name     
      • building binaries
      • creating source archive
      • archives
      • linux packages
      • snapcraft packages
      • calculating checksums
      • signing artifacts
      • docker images
      • artifactory
      • blobs
      • homebrew tap formula
      • scoop manifests
   • snapshotting
   • checking ./dist
      • --rm-dist is set, cleaning it up
   • writing effective config file
      • writing                   config=dist\config.yaml
   • generating changelog
      • pipe skipped              error=not available for snapshots
   • building binaries
      • building                  binary=C:\...\goreleaser\dist\goreleaser_windows_amd64\goreleaser.exe
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_386\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_arm_6\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_darwin_amd64\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_amd64\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_linux_arm64\goreleaser
      • building                  binary=C:\...\goreleaser\dist\goreleaser_windows_386\goreleaser.exe
   • archives
      • creating                  archive=dist\goreleaser_Windows_x86_64.zip
      • creating                  archive=dist\goreleaser_Linux_armv6.tar.gz
      • creating                  archive=dist\goreleaser_Linux_i386.tar.gz
      • creating                  archive=dist\goreleaser_Windows_i386.zip
      • creating                  archive=dist\goreleaser_Darwin_x86_64.tar.gz
      • creating                  archive=dist\goreleaser_Linux_arm64.tar.gz
      • creating                  archive=dist\goreleaser_Linux_x86_64.tar.gz
   • creating source archive
      • pipe skipped              error=source pipe is disabled
   • linux packages
   • snapcraft packages
   • calculating checksums
      • checksumming              file=goreleaser_Linux_x86_64.tar.gz
      • checksumming              file=goreleaser_Windows_i386.zip
      • checksumming              file=goreleaser_Linux_i386.tar.gz
      • checksumming              file=goreleaser_Linux_armv6.tar.gz
      • checksumming              file=goreleaser_Linux_arm64.tar.gz
      • checksumming              file=goreleaser_Windows_x86_64.zip
      • checksumming              file=goreleaser_Darwin_x86_64.tar.gz
   • signing artifacts
   • docker images
      • pipe skipped              error=docker section is not configured
   • publishing
      • blobs
         • pipe skipped              error=blobs section is not configured
      • http upload
         • pipe skipped              error=uploads section is not configured
      • custom publisher
         • pipe skipped              error=publishers section is not configured
      • artifactory
         • pipe skipped              error=artifactory section is not configured
      • docker images
         • pipe skipped              error=publishing is disabled
      • snapcraft packages
         • pipe skipped              error=publishing is disabled
      • github/gitlab/gitea releases
         • pipe skipped              error=publishing is disabled
      • homebrew tap formula
         • token type                type=
      • scoop manifests  
         • pipe skipped              error=publishing is disabled
   • release succeeded after 642.77s

PS C:\...\goreleaser> cat .\dist\goreleaser_checksums.txt
07328fd1ab9bb23dc7d61399f56a56637d42adaeb2f904d88fb96724dc009718  goreleaser_Linux_x86_64.tar.gz
18cf5a30f9d6de8c8fff22112bc28cde359ae5ff20264a03e1b36bc7b2453155  goreleaser_Darwin_x86_64.tar.gz
225fc912ba45d81f3a27b9e395ac473d9586f667e97dc6b49229eff66e125afa  goreleaser_Linux_arm64.tar.gz
4785fcebb838ebb8381ab6cae204f993007a5bb1fee8e69e24c4fbb97eaea8e1  goreleaser_Windows_i386.zip
950f0a7cf8258842abf464414c63653859ee8be25948b35dc384a4542ede35b3  goreleaser_Linux_i386.tar.gz
bc398a56906d9f10edb6ab682721e304a91f7dbde8e28f9ed0f5c505a121c1d5  goreleaser_Windows_x86_64.zip
df22835901a0d331e08940e5d9372fb7e96e7e2576b415b841f880dd3b879880  goreleaser_Linux_armv6.tar.gz
PS C:\...\goreleaser>
```
caarlos0: let me know if I can help here @paultyng 🙏
paultyng: Will try to wrap it up today. I attempted to do some of the testing on Windows but was very painful, flipping over to linux to finish it.
codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1641?src=pr&el=h1) Report
> Merging [#1641](https://codecov.io/gh/goreleaser/goreleaser/pull/1641?src=pr&el=desc) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/345b91703a755595b032ad2189fe1f529e9fdeca&el=desc) will **decrease** coverage by `0.20%`.
> The diff coverage is `79.41%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1641      +/-   ##
==========================================
- Coverage   85.32%   85.12%   -0.21%     
==========================================
  Files          70       70              
  Lines        3891     3932      +41     
==========================================
+ Hits         3320     3347      +27     
- Misses        467      474       +7     
- Partials      104      111       +7     
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/1641?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1641/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `94.44% <ø> (ø)` | |
| [internal/pipe/git/git.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1641/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9naXQvZ2l0Lmdv) | `89.01% <50.00%> (-8.33%)` | :arrow_down: |
| [internal/builders/golang/build.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1641/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYnVpbGRlcnMvZ29sYW5nL2J1aWxkLmdv) | `94.66% <53.84%> (-3.88%)` | :arrow_down: |
| [internal/pipe/checksums/checksums.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1641/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9jaGVja3N1bXMvY2hlY2tzdW1zLmdv) | `100.00% <100.00%> (ø)` | |
| [internal/tmpl/tmpl.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1641/diff?src=pr&el=tree#diff-aW50ZXJuYWwvdG1wbC90bXBsLmdv) | `100.00% <100.00%> (ø)` | |
| [pkg/context/context.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1641/diff?src=pr&el=tree#diff-cGtnL2NvbnRleHQvY29udGV4dC5nbw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1641?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/1641?src=pr&el=footer). Last update [345b917...203d113](https://codecov.io/gh/goreleaser/goreleaser/pull/1641?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

caarlos0: > Will try to wrap it up today. I attempted to do some of the testing on Windows but was very painful, flipping over to linux to finish it.

I'm very confident that our build won't work on windows (at least, parts of it won't)... thanks for the extra effort! We should eventually fix all this and test on windows as well... 
paultyng: I think this is ready for a final review.

Patch:
diff --git a/.goreleaser.yml b/.goreleaser.yml
index 2d22dd00..8f16f0e3 100644
--- a/.goreleaser.yml
+++ b/.goreleaser.yml
@@ -19,6 +19,11 @@ builds:
   ignore:
     - goos: darwin
       goarch: 386
+  mod_timestamp: '{{ .CommitTimestamp }}'
+  flags:
+    - -trimpath
+  ldflags:
+    - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{ .CommitDate }} -X main.builtBy=goreleaser
 checksum:
   name_template: '{{ .ProjectName }}_checksums.txt'
 changelog:
diff --git a/internal/builders/golang/build.go b/internal/builders/golang/build.go
index 41537820..9f9ea1cf 100644
--- a/internal/builders/golang/build.go
+++ b/internal/builders/golang/build.go
@@ -8,7 +8,9 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/artifact"
@@ -119,6 +121,23 @@ func (*Builder) Build(ctx *context.Context, build config.Build, options api.Opti
 	if err := run(ctx, cmd, env, build.Dir); err != nil {
 		return errors.Wrapf(err, "failed to build for %s", options.Target)
 	}
+
+	if build.ModTimestamp != "" {
+		modTimestamp, err := tmpl.New(ctx).WithEnvS(env).WithArtifact(artifact, map[string]string{}).Apply(build.ModTimestamp)
+		if err != nil {
+			return err
+		}
+		modUnix, err := strconv.ParseInt(modTimestamp, 10, 64)
+		if err != nil {
+			return err
+		}
+		modTime := time.Unix(modUnix, 0)
+		err = os.Chtimes(options.Path, modTime, modTime)
+		if err != nil {
+			return errors.Wrapf(err, "failed to change times for %s", options.Target)
+		}
+	}
+
 	ctx.Artifacts.Add(artifact)
 	return nil
 }
diff --git a/internal/git/git.go b/internal/git/git.go
index 00ab0180..4e721018 100644
--- a/internal/git/git.go
+++ b/internal/git/git.go
@@ -15,8 +15,8 @@ func IsRepo() bool {
 	return err == nil && strings.TrimSpace(out) == "true"
 }
 
-// Run runs a git command and returns its output or errors.
-func Run(args ...string) (string, error) {
+// RunEnv runs a git command with the specified env vars and returns its output or errors.
+func RunEnv(env map[string]string, args ...string) (string, error) {
 	// TODO: use exex.CommandContext here and refactor.
 	var extraArgs = []string{
 		"-c", "log.showSignature=false",
@@ -24,6 +24,14 @@ func Run(args ...string) (string, error) {
 	args = append(extraArgs, args...)
 	/* #nosec */
 	var cmd = exec.Command("git", args...)
+
+	if env != nil {
+		cmd.Env = []string{}
+		for k, v := range env {
+			cmd.Env = append(cmd.Env, k+"="+v)
+		}
+	}
+
 	log.WithField("args", args).Debug("running git")
 	bts, err := cmd.CombinedOutput()
 	log.WithField("output", string(bts)).
@@ -34,6 +42,11 @@ func Run(args ...string) (string, error) {
 	return string(bts), nil
 }
 
+// Run runs a git command and returns its output or errors.
+func Run(args ...string) (string, error) {
+	return RunEnv(nil, args...)
+}
+
 // Clean the output.
 func Clean(output string, err error) (string, error) {
 	output = strings.Replace(strings.Split(output, "\n")[0], "'", "", -1)
diff --git a/internal/pipe/checksums/checksums.go b/internal/pipe/checksums/checksums.go
index d4b7e59b..6a2df9bd 100644
--- a/internal/pipe/checksums/checksums.go
+++ b/internal/pipe/checksums/checksums.go
@@ -4,9 +4,10 @@ package checksums
 
 import (
 	"fmt"
-	"io"
 	"os"
 	"path/filepath"
+	"sort"
+	"strings"
 
 	"github.com/apex/log"
 	"github.com/goreleaser/goreleaser/internal/artifact"
@@ -47,6 +48,26 @@ func (Pipe) Run(ctx *context.Context) (err error) {
 		return nil
 	}
 
+	var g = semerrgroup.New(ctx.Parallelism)
+	sumLines := make([]string, len(artifactList))
+	for i, artifact := range artifactList {
+		i := i
+		artifact := artifact
+		g.Go(func() error {
+			sumLine, err := checksums(ctx.Config.Checksum.Algorithm, artifact)
+			if err != nil {
+				return err
+			}
+			sumLines[i] = sumLine
+			return nil
+		})
+	}
+
+	err = g.Wait()
+	if err != nil {
+		return err
+	}
+
 	filename, err := tmpl.New(ctx).Apply(ctx.Config.Checksum.NameTemplate)
 	if err != nil {
 		return err
@@ -61,29 +82,23 @@ func (Pipe) Run(ctx *context.Context) (err error) {
 	}
 	defer file.Close()
 
-	var g = semerrgroup.New(ctx.Parallelism)
-	for _, artifact := range artifactList {
-		artifact := artifact
-		g.Go(func() error {
-			return checksums(ctx.Config.Checksum.Algorithm, file, artifact)
-		})
-	}
 	ctx.Artifacts.Add(&artifact.Artifact{
 		Type: artifact.Checksum,
 		Path: file.Name(),
 		Name: filename,
 	})
-	return g.Wait()
+
+	// sort to ensure the signature is deterministic downstream
+	sort.Strings(sumLines)
+	_, err = file.WriteString(strings.Join(sumLines, ""))
+	return err
 }
 
-func checksums(algorithm string, w io.Writer, artifact *artifact.Artifact) error {
+func checksums(algorithm string, artifact *artifact.Artifact) (string, error) {
 	log.WithField("file", artifact.Name).Info("checksumming")
 	sha, err := artifact.Checksum(algorithm)
 	if err != nil {
-		return err
+		return "", err
 	}
-	// TODO: could change the signature to io.StringWriter, but will break
-	// compatibility with go versions bellow 1.12
-	_, err = io.WriteString(w, fmt.Sprintf("%v  %v\n", sha, artifact.Name))
-	return err
+	return fmt.Sprintf("%v  %v\n", sha, artifact.Name), nil
 }
diff --git a/internal/pipe/git/git.go b/internal/pipe/git/git.go
index 8bd21622..705e5bba 100644
--- a/internal/pipe/git/git.go
+++ b/internal/pipe/git/git.go
@@ -3,7 +3,9 @@ package git
 import (
 	"os"
 	"os/exec"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/apex/log"
 	"github.com/pkg/errors"
@@ -71,6 +73,10 @@ func getGitInfo() (context.GitInfo, error) {
 	if err != nil {
 		return context.GitInfo{}, errors.Wrap(err, "couldn't get current commit")
 	}
+	date, err := getCommitDate()
+	if err != nil {
+		return context.GitInfo{}, errors.Wrap(err, "couldn't get commit date")
+	}
 	url, err := getURL()
 	if err != nil {
 		return context.GitInfo{}, errors.Wrap(err, "couldn't get remote URL")
@@ -81,6 +87,7 @@ func getGitInfo() (context.GitInfo, error) {
 			Commit:      full,
 			FullCommit:  full,
 			ShortCommit: short,
+			CommitDate:  date,
 			URL:         url,
 			CurrentTag:  "v0.0.0",
 		}, ErrNoTag
@@ -90,6 +97,7 @@ func getGitInfo() (context.GitInfo, error) {
 		Commit:      full,
 		FullCommit:  full,
 		ShortCommit: short,
+		CommitDate:  date,
 		URL:         url,
 	}, nil
 }
@@ -115,6 +123,22 @@ func validate(ctx *context.Context) error {
 	return nil
 }
 
+func getCommitDate() (time.Time, error) {
+	ct, err := git.Clean(git.Run("show", "--format='%ct'", "HEAD", "--quiet"))
+	if err != nil {
+		return time.Time{}, err
+	}
+	if ct == "" {
+		return time.Time{}, nil
+	}
+	i, err := strconv.ParseInt(ct, 10, 64)
+	if err != nil {
+		return time.Time{}, err
+	}
+	t := time.Unix(i, 0).UTC()
+	return t, nil
+}
+
 func getShortCommit() (string, error) {
 	return git.Clean(git.Run("show", "--format='%h'", "HEAD", "--quiet"))
 }
diff --git a/internal/tmpl/tmpl.go b/internal/tmpl/tmpl.go
index 78b93757..24d18e4e 100644
--- a/internal/tmpl/tmpl.go
+++ b/internal/tmpl/tmpl.go
@@ -24,22 +24,24 @@ type Fields map[string]interface{}
 
 const (
 	// general keys
-	projectName = "ProjectName"
-	version     = "Version"
-	rawVersion  = "RawVersion"
-	tag         = "Tag"
-	commit      = "Commit"
-	shortCommit = "ShortCommit"
-	fullCommit  = "FullCommit"
-	gitURL      = "GitURL"
-	major       = "Major"
-	minor       = "Minor"
-	patch       = "Patch"
-	prerelease  = "Prerelease"
-	isSnapshot  = "IsSnapshot"
-	env         = "Env"
-	date        = "Date"
-	timestamp   = "Timestamp"
+	projectName     = "ProjectName"
+	version         = "Version"
+	rawVersion      = "RawVersion"
+	tag             = "Tag"
+	commit          = "Commit"
+	shortCommit     = "ShortCommit"
+	fullCommit      = "FullCommit"
+	commitDate      = "CommitDate"
+	commitTimestamp = "CommitTimestamp"
+	gitURL          = "GitURL"
+	major           = "Major"
+	minor           = "Minor"
+	patch           = "Patch"
+	prerelease      = "Prerelease"
+	isSnapshot      = "IsSnapshot"
+	env             = "Env"
+	date            = "Date"
+	timestamp       = "Timestamp"
 
 	// artifact-only keys
 	osKey        = "Os"
@@ -67,22 +69,24 @@ func New(ctx *context.Context) *Template {
 
 	return &Template{
 		fields: Fields{
-			projectName: ctx.Config.ProjectName,
-			version:     ctx.Version,
-			rawVersion:  rawVersionV,
-			tag:         ctx.Git.CurrentTag,
-			commit:      ctx.Git.Commit,
-			shortCommit: ctx.Git.ShortCommit,
-			fullCommit:  ctx.Git.FullCommit,
-			gitURL:      ctx.Git.URL,
-			env:         ctx.Env,
-			date:        time.Now().UTC().Format(time.RFC3339),
-			timestamp:   time.Now().UTC().Unix(),
-			major:       ctx.Semver.Major,
-			minor:       ctx.Semver.Minor,
-			patch:       ctx.Semver.Patch,
-			prerelease:  ctx.Semver.Prerelease,
-			isSnapshot:  ctx.Snapshot,
+			projectName:     ctx.Config.ProjectName,
+			version:         ctx.Version,
+			rawVersion:      rawVersionV,
+			tag:             ctx.Git.CurrentTag,
+			commit:          ctx.Git.Commit,
+			shortCommit:     ctx.Git.ShortCommit,
+			fullCommit:      ctx.Git.FullCommit,
+			commitDate:      ctx.Git.CommitDate.UTC().Format(time.RFC3339),
+			commitTimestamp: ctx.Git.CommitDate.UTC().Unix(),
+			gitURL:          ctx.Git.URL,
+			env:             ctx.Env,
+			date:            ctx.Date.UTC().Format(time.RFC3339),
+			timestamp:       ctx.Date.UTC().Unix(),
+			major:           ctx.Semver.Major,
+			minor:           ctx.Semver.Minor,
+			patch:           ctx.Semver.Patch,
+			prerelease:      ctx.Semver.Prerelease,
+			isSnapshot:      ctx.Snapshot,
 		},
 	}
 }
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 08f66c58..f83b5d63 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -171,24 +171,25 @@ func (a *FlagArray) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 // Build contains the build configuration section.
 type Build struct {
-	ID       string         `yaml:",omitempty"`
-	Goos     []string       `yaml:",omitempty"`
-	Goarch   []string       `yaml:",omitempty"`
-	Goarm    []string       `yaml:",omitempty"`
-	Gomips   []string       `yaml:",omitempty"`
-	Targets  []string       `yaml:",omitempty"`
-	Ignore   []IgnoredBuild `yaml:",omitempty"`
-	Dir      string         `yaml:",omitempty"`
-	Main     string         `yaml:",omitempty"`
-	Ldflags  StringArray    `yaml:",omitempty"`
-	Flags    FlagArray      `yaml:",omitempty"`
-	Binary   string         `yaml:",omitempty"`
-	Hooks    HookConfig     `yaml:",omitempty"`
-	Env      []string       `yaml:",omitempty"`
-	Lang     string         `yaml:",omitempty"`
-	Asmflags StringArray    `yaml:",omitempty"`
-	Gcflags  StringArray    `yaml:",omitempty"`
-	Skip     bool           `yaml:",omitempty"`
+	ID           string         `yaml:",omitempty"`
+	Goos         []string       `yaml:",omitempty"`
+	Goarch       []string       `yaml:",omitempty"`
+	Goarm        []string       `yaml:",omitempty"`
+	Gomips       []string       `yaml:",omitempty"`
+	Targets      []string       `yaml:",omitempty"`
+	Ignore       []IgnoredBuild `yaml:",omitempty"`
+	Dir          string         `yaml:",omitempty"`
+	Main         string         `yaml:",omitempty"`
+	Ldflags      StringArray    `yaml:",omitempty"`
+	Flags        FlagArray      `yaml:",omitempty"`
+	Binary       string         `yaml:",omitempty"`
+	Hooks        HookConfig     `yaml:",omitempty"`
+	Env          []string       `yaml:",omitempty"`
+	Lang         string         `yaml:",omitempty"`
+	Asmflags     StringArray    `yaml:",omitempty"`
+	Gcflags      StringArray    `yaml:",omitempty"`
+	ModTimestamp string         `yaml:"mod_timestamp,omitempty"`
+	Skip         bool           `yaml:",omitempty"`
 }
 
 type HookConfig struct {
diff --git a/pkg/context/context.go b/pkg/context/context.go
index 063d1a8d..517f47d1 100644
--- a/pkg/context/context.go
+++ b/pkg/context/context.go
@@ -22,6 +22,7 @@ type GitInfo struct {
 	Commit      string
 	ShortCommit string
 	FullCommit  string
+	CommitDate  time.Time
 	URL         string
 }
 
@@ -68,6 +69,7 @@ type Context struct {
 	Token              string
 	TokenType          TokenType
 	Git                GitInfo
+	Date               time.Time
 	Artifacts          artifact.Artifacts
 	ReleaseNotes       string
 	ReleaseHeader      string
@@ -113,6 +115,7 @@ func Wrap(ctx ctx.Context, config config.Project) *Context {
 		Env:         splitEnv(append(os.Environ(), config.Env...)),
 		Parallelism: 4,
 		Artifacts:   artifact.New(),
+		Date:        time.Now(),
 	}
 }
 
diff --git a/www/docs/customization/build.md b/www/docs/customization/build.md
index 0446f4df..97647554 100644
--- a/www/docs/customization/build.md
+++ b/www/docs/customization/build.md
@@ -101,6 +101,12 @@ builds:
       - goarm: mips64
         gomips: hardfloat
 
+    # Set the modified timestamp on the output binary, typically
+    # you would do this to ensure a build was reproducible. Pass
+    # empty string to skip modifying the output.
+    # Default is empty string.
+    mod_timestamp: '{{ .CommitTimestamp }}'
+
     # Hooks can be used to customize the final binary,
     # for example, to run generators.
     # Those fields allow templates.
@@ -248,3 +254,12 @@ Environment variables are inherited and overridden in the following order:
 GoReleaser uses `git describe` to get the build tag. You can set
 a different build tag using the environment variable `GORELEASER_CURRENT_TAG`.
 This is useful in scenarios where two tags point to the same commit.
+
+## Reproducible Builds
+
+To make your releases, checksums, and signatures reproducible, you will need to make some (if not all) of the following modifications to the build defaults in GoReleaser:
+
+* Modify `ldflags`: by default `main.Date` is set to the time GoReleaser is run (`{{.Date}}`), you can set this to `{{.CommitDate}}` or just not pass the variable.
+* Modify `mod_timestamp`: by default this is empty string, set to `{{.CommitTimestamp}}` or a constant value instead.
+* If you do not run your builds from a consistent directory structure, pass `-trimpath` to `flags`.
+* Remove uses of the `time` template function. This function returns a new value on every call and is not deterministic.
diff --git a/www/docs/customization/templates.md b/www/docs/customization/templates.md
index 3078ce8e..9e40d23b 100644
--- a/www/docs/customization/templates.md
+++ b/www/docs/customization/templates.md
@@ -10,24 +10,26 @@ templating is available.
 
 On fields that support templating, this fields are always available:
 
-| Key            | Description                                                                                                                  |
-|----------------|------------------------------------------------------------------------------------------------------------------------------|
-| `.ProjectName` | the project name                                                                                                             |
-| `.Version`     | the version being released (`v` prefix stripped),<br>or `{{ .Tag }}-SNAPSHOT-{{ .ShortCommit }}` in case of snapshot release |
-| `.Tag`         | the current git tag                                                                                                          |
-| `.ShortCommit` | the git commit short hash                                                                                                    |
-| `.FullCommit`  | the git commit full hash                                                                                                     |
-| `.Commit`      | the git commit hash (deprecated)                                                                                             |
-| `.GitURL`      | the git remote url                                                                                                           |
-| `.Major`       | the major part of the version (assuming `Tag` is a valid semver, else `0`)                                                   |
-| `.Minor`       | the minor part of the version (assuming `Tag` is a valid semver, else `0`)                                                   |
-| `.Patch`       | the patch part of the version (assuming `Tag` is a valid semver, else `0`)                                                   |
-| `.Prerelease`  | the prerelease part of the version, e.g. `beta` (assuming `Tag` is a valid semver)                                           |
-| `.RawVersion`  | Major.Minor.Patch (assuming `Tag` is a valid semver, else `0.0.0`)                                                           |
-| `.IsSnapshot`  | `true` if a snapshot is being released, `false` otherwise                                                                    |
-| `.Env`         | a map with system's environment variables                                                                                    |
-| `.Date`        | current UTC date in RFC3339 format                                                                                           |
-| `.Timestamp`   | current UTC time in Unix format                                                                                              |
+| Key                | Description                                                                                                                  |
+|--------------------|------------------------------------------------------------------------------------------------------------------------------|
+| `.ProjectName`     | the project name                                                                                                             |
+| `.Version`         | the version being released (`v` prefix stripped),<br>or `{{ .Tag }}-SNAPSHOT-{{ .ShortCommit }}` in case of snapshot release |
+| `.Tag`             | the current git tag                                                                                                          |
+| `.ShortCommit`     | the git commit short hash                                                                                                    |
+| `.FullCommit`      | the git commit full hash                                                                                                     |
+| `.Commit`          | the git commit hash (deprecated)                                                                                             |
+| `.CommitDate`      | the UTC commit date in RFC 3339 format                                                                                       |
+| `.CommitTimestamp` | the UTC commit date in Unix format                                                                                           |
+| `.GitURL`          | the git remote url                                                                                                           |
+| `.Major`           | the major part of the version (assuming `Tag` is a valid semver, else `0`)                                                   |
+| `.Minor`           | the minor part of the version (assuming `Tag` is a valid semver, else `0`)                                                   |
+| `.Patch`           | the patch part of the version (assuming `Tag` is a valid semver, else `0`)                                                   |
+| `.Prerelease`      | the prerelease part of the version, e.g. `beta` (assuming `Tag` is a valid semver)                                           |
+| `.RawVersion`      | Major.Minor.Patch (assuming `Tag` is a valid semver, else `0.0.0`)                                                           |
+| `.IsSnapshot`      | `true` if a snapshot is being released, `false` otherwise                                                                    |
+| `.Env`             | a map with system's environment variables                                                                                    |
+| `.Date`            | current UTC date in RFC 3339 format                                                                                          |
+| `.Timestamp`       | current UTC time in Unix format                                                                                              |
 
 On fields that are related to a single artifact (e.g., the binary name), you
 may have some extra fields:
@@ -54,7 +56,7 @@ On all fields, you have these available functions:
 | Usage                   | Description                                                                                                                    |
 |-------------------------|--------------------------------------------------------------------------------------------------------------------------------|
 | `replace "v1.2" "v" ""` | replaces all matches. See [ReplaceAll](https://golang.org/pkg/strings/#ReplaceAll)                                             |
-| `time "01/02/2006"`     | current UTC time in the specified format                                                                                       |
+| `time "01/02/2006"`     | current UTC time in the specified format (this is not deterministic, a new time for every call)                                |
 | `tolower "V1.2"`        | makes input string lowercase. See [ToLower](https://golang.org/pkg/strings/#ToLower)                                           |
 | `toupper "v1.2"`        | makes input string uppercase. See [ToUpper](https://golang.org/pkg/strings/#ToUpper)                                           |
 | `trim " v1.2  "`        | removes all leading and trailing white space. See [TrimSpace](https://golang.org/pkg/strings/#TrimSpace)                       |


Test patch:
diff --git a/internal/builders/golang/build_test.go b/internal/builders/golang/build_test.go
index ed459323..68fada0c 100644
--- a/internal/builders/golang/build_test.go
+++ b/internal/builders/golang/build_test.go
@@ -122,6 +122,10 @@ func TestBuild(t *testing.T) {
 		}
 		bin, terr := tmpl.New(ctx).Apply(build.Binary)
 		require.NoError(t, terr)
+
+		// injecting some delay here to force inconsistent mod times on bins
+		time.Sleep(2 * time.Second)
+
 		var err = Default.Build(ctx, build, api.Options{
 			Target: target,
 			Name:   bin + ext,
@@ -219,6 +223,24 @@ func TestBuild(t *testing.T) {
 			},
 		},
 	})
+
+	modTimes := map[time.Time]bool{}
+	for _, bin := range ctx.Artifacts.List() {
+		if bin.Type != artifact.Binary {
+			continue
+		}
+
+		fi, err := os.Stat(bin.Path)
+		assert.NoError(t, err)
+
+		// make this a suitable map key, per docs: https://golang.org/pkg/time/#Time
+		modTime := fi.ModTime().UTC().Round(0)
+
+		if modTimes[modTime] {
+			t.Fatal("duplicate modified time found, times should be different by default")
+		}
+		modTimes[modTime] = true
+	}
 }
 
 func TestBuildCodeInSubdir(t *testing.T) {
@@ -481,26 +503,32 @@ func TestRunPipeWithMainFuncNotInMainGoFile(t *testing.T) {
 }
 
 func TestLdFlagsFullTemplate(t *testing.T) {
+	run := time.Now().UTC()
+	commit := time.Now().AddDate(-1, 0, 0)
+
 	var ctx = &context.Context{
 		Git: context.GitInfo{
 			CurrentTag: "v1.2.3",
 			Commit:     "123",
+			CommitDate: commit,
 		},
+		Date:    run,
 		Version: "1.2.3",
 		Env:     map[string]string{"FOO": "123"},
 	}
 	var artifact = &artifact.Artifact{Goarch: "amd64"}
 	flags, err := tmpl.New(ctx).WithArtifact(artifact, map[string]string{}).
-		Apply(`-s -w -X main.version={{.Version}} -X main.tag={{.Tag}} -X main.date={{.Date}} -X main.commit={{.Commit}} -X "main.foo={{.Env.FOO}}" -X main.time={{ time "20060102" }} -X main.arch={{.Arch}}`)
+		Apply(`-s -w -X main.version={{.Version}} -X main.tag={{.Tag}} -X main.date={{.Date}} -X main.commit={{.Commit}} -X "main.foo={{.Env.FOO}}" -X main.time={{ time "20060102" }} -X main.arch={{.Arch}} -X main.commitDate={{.CommitDate}}`)
 	assert.NoError(t, err)
 	assert.Contains(t, flags, "-s -w")
 	assert.Contains(t, flags, "-X main.version=1.2.3")
 	assert.Contains(t, flags, "-X main.tag=v1.2.3")
 	assert.Contains(t, flags, "-X main.commit=123")
-	assert.Contains(t, flags, fmt.Sprintf("-X main.date=%d", time.Now().Year()))
-	assert.Contains(t, flags, fmt.Sprintf("-X main.time=%d", time.Now().Year()))
+	assert.Contains(t, flags, fmt.Sprintf("-X main.date=%d", run.Year()))
+	assert.Contains(t, flags, fmt.Sprintf("-X main.time=%d", run.Year()))
 	assert.Contains(t, flags, `-X "main.foo=123"`)
 	assert.Contains(t, flags, `-X main.arch=amd64`)
+	assert.Contains(t, flags, fmt.Sprintf("-X main.commitDate=%d", commit.Year()))
 }
 
 func TestInvalidTemplate(t *testing.T) {
@@ -589,6 +617,74 @@ func TestJoinLdFlags(t *testing.T) {
 	}
 }
 
+func TestBuildModTimestamp(t *testing.T) {
+	// round to seconds since this will be a unix timestamp
+	modTime := time.Now().AddDate(-1, 0, 0).Round(1 * time.Second).UTC()
+
+	folder, back := testlib.Mktmp(t)
+	defer back()
+	writeGoodMain(t, folder)
+
+	var config = config.Project{
+		Builds: []config.Build{
+			{
+				ID:     "foo",
+				Env:    []string{"GO111MODULE=off"},
+				Binary: "bin/foo-{{ .Version }}",
+				Targets: []string{
+					"linux_amd64",
+					"darwin_amd64",
+					"windows_amd64",
+					"linux_arm_6",
+					"js_wasm",
+					"linux_mips_softfloat",
+					"linux_mips64le_softfloat",
+				},
+				Asmflags:     []string{".=", "all="},
+				Gcflags:      []string{"all="},
+				Flags:        []string{"{{.Env.GO_FLAGS}}"},
+				ModTimestamp: fmt.Sprintf("%d", modTime.Unix()),
+			},
+		},
+	}
+	var ctx = context.New(config)
+	ctx.Env["GO_FLAGS"] = "-v"
+	ctx.Git.CurrentTag = "5.6.7"
+	ctx.Version = "v" + ctx.Git.CurrentTag
+	var build = ctx.Config.Builds[0]
+	for _, target := range build.Targets {
+		var ext string
+		if strings.HasPrefix(target, "windows") {
+			ext = ".exe"
+		} else if target == "js_wasm" {
+			ext = ".wasm"
+		}
+		bin, terr := tmpl.New(ctx).Apply(build.Binary)
+		require.NoError(t, terr)
+
+		// injecting some delay here to force inconsistent mod times on bins
+		time.Sleep(2 * time.Second)
+
+		var err = Default.Build(ctx, build, api.Options{
+			Target: target,
+			Name:   bin + ext,
+			Path:   filepath.Join(folder, "dist", target, bin+ext),
+			Ext:    ext,
+		})
+		assert.NoError(t, err)
+	}
+
+	for _, bin := range ctx.Artifacts.List() {
+		if bin.Type != artifact.Binary {
+			continue
+		}
+
+		fi, err := os.Stat(bin.Path)
+		assert.NoError(t, err)
+		assert.True(t, modTime.Equal(fi.ModTime()), "inconsistent mod times found when specifying ModTimestamp")
+	}
+}
+
 //
 // Helpers
 //
diff --git a/internal/pipe/checksums/checksums_test.go b/internal/pipe/checksums/checksums_test.go
index 01c58c51..6216dc2d 100644
--- a/internal/pipe/checksums/checksums_test.go
+++ b/internal/pipe/checksums/checksums_test.go
@@ -80,6 +80,11 @@ func TestPipeFileNotExist(t *testing.T) {
 }
 
 func TestPipeInvalidNameTemplate(t *testing.T) {
+	binFile, err := ioutil.TempFile("", "goreleasertest-bin")
+	assert.NoError(t, err)
+	_, err = binFile.WriteString("fake artifact")
+	assert.NoError(t, err)
+
 	for template, eerr := range map[string]string{
 		"{{ .Pro }_checksums.txt": `template: tmpl:1: unexpected "}" in operand`,
 		"{{.Env.NOPE}}":           `template: tmpl:1:6: executing "tmpl" at <.Env.NOPE>: map has no entry for key "NOPE"`,
@@ -93,6 +98,7 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
 					ProjectName: "name",
 					Checksum: config.Checksum{
 						NameTemplate: template,
+						Algorithm:    "sha256",
 					},
 				},
 			)
@@ -100,6 +106,7 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
 			ctx.Artifacts.Add(&artifact.Artifact{
 				Name: "whatever",
 				Type: artifact.UploadableBinary,
+				Path: binFile.Name(),
 			})
 			err = Pipe{}.Run(ctx)
 			assert.Error(tt, err)
@@ -109,6 +116,11 @@ func TestPipeInvalidNameTemplate(t *testing.T) {
 }
 
 func TestPipeCouldNotOpenChecksumsTxt(t *testing.T) {
+	binFile, err := ioutil.TempFile("", "goreleasertest-bin")
+	assert.NoError(t, err)
+	_, err = binFile.WriteString("fake artifact")
+	assert.NoError(t, err)
+
 	folder, err := ioutil.TempDir("", "goreleasertest")
 	assert.NoError(t, err)
 	var file = filepath.Join(folder, "checksums.txt")
@@ -118,6 +130,7 @@ func TestPipeCouldNotOpenChecksumsTxt(t *testing.T) {
 			Dist: folder,
 			Checksum: config.Checksum{
 				NameTemplate: "checksums.txt",
+				Algorithm:    "sha256",
 			},
 		},
 	)
@@ -125,6 +138,7 @@ func TestPipeCouldNotOpenChecksumsTxt(t *testing.T) {
 	ctx.Artifacts.Add(&artifact.Artifact{
 		Name: "whatever",
 		Type: artifact.UploadableBinary,
+		Path: binFile.Name(),
 	})
 	err = Pipe{}.Run(ctx)
 	assert.Error(t, err)
diff --git a/internal/pipe/git/git_test.go b/internal/pipe/git/git_test.go
index 1d4b1921..ceb54daa 100644
--- a/internal/pipe/git/git_test.go
+++ b/internal/pipe/git/git_test.go
@@ -5,6 +5,7 @@ import (
 	"os"
 	"path/filepath"
 	"testing"
+	"time"
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -229,3 +230,21 @@ func TestTagFromCI(t *testing.T) {
 		}
 	}
 }
+
+func TestCommitDate(t *testing.T) {
+	// round to seconds since this is expressed in a Unix timestamp
+	commitDate := time.Now().AddDate(-1, 0, 0).Round(1 * time.Second)
+
+	_, back := testlib.Mktmp(t)
+	defer back()
+	testlib.GitInit(t)
+	testlib.GitRemoteAdd(t, "git@github.com:foo/bar.git")
+	testlib.GitCommitWithDate(t, "commit1", commitDate)
+	testlib.GitTag(t, "v0.0.1")
+	var ctx = &context.Context{
+		Config: config.Project{},
+	}
+	assert.NoError(t, Pipe{}.Run(ctx))
+	assert.Equal(t, "v0.0.1", ctx.Git.CurrentTag)
+	assert.True(t, commitDate.Equal(ctx.Git.CommitDate), "commit date does not match expected")
+}
diff --git a/internal/testlib/git.go b/internal/testlib/git.go
index ea89534c..4407c472 100644
--- a/internal/testlib/git.go
+++ b/internal/testlib/git.go
@@ -2,6 +2,7 @@ package testlib
 
 import (
 	"testing"
+	"time"
 
 	"github.com/goreleaser/goreleaser/internal/git"
 	"github.com/stretchr/testify/assert"
@@ -24,7 +25,18 @@ func GitRemoteAdd(t *testing.T, url string) {
 
 // GitCommit creates a git commits.
 func GitCommit(t *testing.T, msg string) {
-	out, err := fakeGit("commit", "--allow-empty", "-m", msg)
+	GitCommitWithDate(t, msg, time.Time{})
+}
+
+// GitCommitWithDate creates a git commit with a commit date.
+func GitCommitWithDate(t *testing.T, msg string, commitDate time.Time) {
+	env := (map[string]string)(nil)
+	if !commitDate.IsZero() {
+		env = map[string]string{
+			"GIT_COMMITTER_DATE": commitDate.Format(time.RFC1123Z),
+		}
+	}
+	out, err := fakeGitEnv(env, "commit", "--allow-empty", "-m", msg)
 	assert.NoError(t, err)
 	assert.Contains(t, out, "master", msg)
 }
@@ -43,7 +55,7 @@ func GitAdd(t *testing.T) {
 	assert.Empty(t, out)
 }
 
-func fakeGit(args ...string) (string, error) {
+func fakeGitEnv(env map[string]string, args ...string) (string, error) {
 	var allArgs = []string{
 		"-c", "user.name='GoReleaser'",
 		"-c", "user.email='test@goreleaser.github.com'",
@@ -51,7 +63,11 @@ func fakeGit(args ...string) (string, error) {
 		"-c", "log.showSignature=false",
 	}
 	allArgs = append(allArgs, args...)
-	return git.Run(allArgs...)
+	return git.RunEnv(env, allArgs...)
+}
+
+func fakeGit(args ...string) (string, error) {
+	return fakeGitEnv(nil, args...)
 }
 
 // GitCheckoutBranch allows us to change the active branch that we're using.