Repository: goreleaser/goreleaser
Pull request #1379 (2020-03-06 03:16:53)

feat: upload source archive
New pipe that uses `git-archive` to create an archive of the current source tree in the commit being released.


closes https://github.com/goreleaser/goreleaser/issues/1359

codecov-io: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/1379?src=pr&el=h1) Report
> Merging [#1379](https://codecov.io/gh/goreleaser/goreleaser/pull/1379?src=pr&el=desc) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/5efb690d1c3eae0605ff4a3a6cd65f470b1c96e9?src=pr&el=desc) will **decrease** coverage by `<.01%`.
> The diff coverage is `83.72%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1379      +/-   ##
==========================================
- Coverage   83.92%   83.92%   -0.01%     
==========================================
  Files          57       58       +1     
  Lines        3329     3371      +42     
==========================================
+ Hits         2794     2829      +35     
- Misses        455      462       +7     
  Partials       80       80
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/1379?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/config/config.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZy9jb25maWcuZ28=) | `100% <ø> (ø)` | :arrow_up: |
| [internal/http/http.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvaHR0cC9odHRwLmdv) | `90.2% <ø> (ø)` | :arrow_up: |
| [internal/pipe/sign/sign.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9zaWduL3NpZ24uZ28=) | `100% <100%> (ø)` | :arrow_up: |
| [internal/pipe/checksums/checksums.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9jaGVja3N1bXMvY2hlY2tzdW1zLmdv) | `100% <100%> (ø)` | :arrow_up: |
| [internal/pipe/release/release.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9yZWxlYXNlL3JlbGVhc2UuZ28=) | `90.62% <100%> (+0.07%)` | :arrow_up: |
| [internal/pipe/sourcearchive/source.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9zb3VyY2VhcmNoaXZlL3NvdXJjZS5nbw==) | `100% <100%> (ø)` | |
| [internal/pipe/blob/openbucket.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvcGlwZS9ibG9iL29wZW5idWNrZXQuZ28=) | `63.46% <100%> (+0.35%)` | :arrow_up: |
| [internal/artifact/artifact.go](https://codecov.io/gh/goreleaser/goreleaser/pull/1379/diff?src=pr&el=tree#diff-aW50ZXJuYWwvYXJ0aWZhY3QvYXJ0aWZhY3QuZ28=) | `79.2% <30%> (-4.43%)` | :arrow_down: |

------

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

stale[bot]: This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.


Patch:
diff --git a/internal/artifact/artifact.go b/internal/artifact/artifact.go
index 8f4f44a9..a5205c8f 100644
--- a/internal/artifact/artifact.go
+++ b/internal/artifact/artifact.go
@@ -47,12 +47,16 @@ const (
 	Checksum
 	// Signature is a signature file
 	Signature
+	// UploadableSourceArchive is the archive with the current commit source code
+	UploadableSourceArchive
 )
 
 func (t Type) String() string {
 	switch t {
 	case UploadableArchive:
 		return "Archive"
+	case UploadableFile:
+		return "File"
 	case UploadableBinary:
 	case Binary:
 		return "Binary"
@@ -63,10 +67,15 @@ func (t Type) String() string {
 	case DockerImage:
 	case PublishableDockerImage:
 		return "Docker Image"
+	case PublishableSnapcraft:
+	case Snapcraft:
+		return "Snap"
 	case Checksum:
 		return "Checksum"
 	case Signature:
 		return "Signature"
+	case UploadableSourceArchive:
+		return "Source"
 	}
 	return "unknown"
 }
@@ -218,8 +227,10 @@ func ByIDs(ids ...string) Filter {
 	for _, id := range ids {
 		id := id
 		filters = append(filters, func(a *Artifact) bool {
-			// checksum are allways for all artifacts, so return always true.
-			return a.Type == Checksum || a.ExtraOr("ID", "") == id
+			// checksum and source archive are always for all artifacts, so return always true.
+			return a.Type == Checksum ||
+				a.Type == UploadableSourceArchive ||
+				a.ExtraOr("ID", "") == id
 		})
 	}
 	return Or(filters...)
diff --git a/internal/http/http.go b/internal/http/http.go
index 7c76adb3..506dc133 100644
--- a/internal/http/http.go
+++ b/internal/http/http.go
@@ -163,6 +163,7 @@ func Upload(ctx *context.Context, uploads []config.Upload, kind string, check Re
 		//	- "binary": Upload only the raw binaries
 		switch v := strings.ToLower(upload.Mode); v {
 		case ModeArchive:
+			// TODO: should we add source archives here too?
 			filters = append(filters,
 				artifact.ByType(artifact.UploadableArchive),
 				artifact.ByType(artifact.LinuxPackage),
diff --git a/internal/pipe/blob/openbucket.go b/internal/pipe/blob/openbucket.go
index cee46c2e..37614a96 100644
--- a/internal/pipe/blob/openbucket.go
+++ b/internal/pipe/blob/openbucket.go
@@ -106,6 +106,7 @@ func (b Bucket) Upload(ctx *context.Context, conf config.Blob) error {
 	var filter = artifact.Or(
 		artifact.ByType(artifact.UploadableArchive),
 		artifact.ByType(artifact.UploadableBinary),
+		artifact.ByType(artifact.UploadableSourceArchive),
 		artifact.ByType(artifact.Checksum),
 		artifact.ByType(artifact.Signature),
 		artifact.ByType(artifact.LinuxPackage),
diff --git a/internal/pipe/checksums/checksums.go b/internal/pipe/checksums/checksums.go
index f1227644..19c7cb16 100644
--- a/internal/pipe/checksums/checksums.go
+++ b/internal/pipe/checksums/checksums.go
@@ -39,6 +39,7 @@ func (Pipe) Run(ctx *context.Context) (err error) {
 		artifact.Or(
 			artifact.ByType(artifact.UploadableArchive),
 			artifact.ByType(artifact.UploadableBinary),
+			artifact.ByType(artifact.UploadableSourceArchive),
 			artifact.ByType(artifact.LinuxPackage),
 		),
 	).List()
diff --git a/internal/pipe/release/release.go b/internal/pipe/release/release.go
index b57da112..572586cf 100644
--- a/internal/pipe/release/release.go
+++ b/internal/pipe/release/release.go
@@ -141,6 +141,7 @@ func doPublish(ctx *context.Context, client client.Client) error {
 	var filters = artifact.Or(
 		artifact.ByType(artifact.UploadableArchive),
 		artifact.ByType(artifact.UploadableBinary),
+		artifact.ByType(artifact.UploadableSourceArchive),
 		artifact.ByType(artifact.Checksum),
 		artifact.ByType(artifact.Signature),
 		artifact.ByType(artifact.LinuxPackage),
diff --git a/internal/pipe/sign/sign.go b/internal/pipe/sign/sign.go
index 4ac4bbc4..0bfa07b4 100644
--- a/internal/pipe/sign/sign.go
+++ b/internal/pipe/sign/sign.go
@@ -69,6 +69,7 @@ func (Pipe) Run(ctx *context.Context) error {
 				filters = append(filters, artifact.Or(
 					artifact.ByType(artifact.UploadableArchive),
 					artifact.ByType(artifact.UploadableBinary),
+					artifact.ByType(artifact.UploadableSourceArchive),
 					artifact.ByType(artifact.Checksum),
 					artifact.ByType(artifact.LinuxPackage),
 				))
diff --git a/internal/pipe/sourcearchive/source.go b/internal/pipe/sourcearchive/source.go
new file mode 100644
index 00000000..d728384f
--- /dev/null
+++ b/internal/pipe/sourcearchive/source.go
@@ -0,0 +1,59 @@
+// Package sourcearchive archives the source of the project using git-archive.
+package sourcearchive
+
+import (
+	"path/filepath"
+
+	"github.com/apex/log"
+	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/git"
+	"github.com/goreleaser/goreleaser/internal/pipe"
+	"github.com/goreleaser/goreleaser/internal/tmpl"
+	"github.com/goreleaser/goreleaser/pkg/context"
+)
+
+// Pipe for cleandis
+type Pipe struct{}
+
+func (Pipe) String() string {
+	return "creating source archive"
+}
+
+// Run the pipe
+func (Pipe) Run(ctx *context.Context) (err error) {
+	if !ctx.Config.Source.Enabled {
+		return pipe.Skip("source pipe is disabled")
+	}
+
+	name, err := tmpl.New(ctx).Apply(ctx.Config.Source.NameTemplate)
+	if err != nil {
+		return err
+	}
+	var filename = name + "." + ctx.Config.Source.Format
+	var path = filepath.Join(ctx.Config.Dist, filename)
+	log.WithField("file", filename).Info("creating source archive")
+	out, err := git.Clean(git.Run("archive", "-o", path, ctx.Git.FullCommit))
+	log.Debug(out)
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Type: artifact.UploadableSourceArchive,
+		Name: filename,
+		Path: path,
+		Extra: map[string]interface{}{
+			"Format": ctx.Config.Source.Format,
+		},
+	})
+	return err
+}
+
+// Default sets the pipe defaults
+func (Pipe) Default(ctx *context.Context) error {
+	var archive = &ctx.Config.Source
+	if archive.Format == "" {
+		archive.Format = "tar.gz"
+	}
+
+	if archive.NameTemplate == "" {
+		archive.NameTemplate = "{{ .ProjectName }}-{{ .Version }}"
+	}
+	return nil
+}
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index 56a19199..55123b34 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/goreleaser/goreleaser/internal/pipe/brew"
 	"github.com/goreleaser/goreleaser/internal/pipe/semver"
+	"github.com/goreleaser/goreleaser/internal/pipe/sourcearchive"
 
 	"github.com/goreleaser/goreleaser/internal/pipe/archive"
 	"github.com/goreleaser/goreleaser/internal/pipe/before"
@@ -48,6 +49,7 @@ var Pipeline = []Piper{
 	changelog.Pipe{},       // builds the release changelog
 	build.Pipe{},           // build
 	archive.Pipe{},         // archive in tar.gz, zip or binary (which does no archiving at all)
+	sourcearchive.Pipe{},   // archive the source code using git-archive
 	nfpm.Pipe{},            // archive via fpm (deb, rpm) using "native" go impl
 	snapcraft.Pipe{},       // archive via snapcraft (snap)
 	checksums.Pipe{},       // checksums of the files
diff --git a/pkg/config/config.go b/pkg/config/config.go
index 13cf9ee9..55fcada0 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -351,6 +351,13 @@ type Upload struct {
 	CustomArtifactName bool     `yaml:"custom_artifact_name,omitempty"`
 }
 
+// Source configuration
+type Source struct {
+	NameTemplate string `yaml:"name_template,omitempty"`
+	Format       string `yaml:",omitempty"`
+	Enabled      bool   `yaml:",omitempty"`
+}
+
 // Project includes all project configuration
 type Project struct {
 	ProjectName   string      `yaml:"project_name,omitempty"`
@@ -374,6 +381,7 @@ type Project struct {
 	Signs         []Sign      `yaml:",omitempty"`
 	EnvFiles      EnvFiles    `yaml:"env_files,omitempty"`
 	Before        Before      `yaml:",omitempty"`
+	Source        Source      `yaml:",omitempty"`
 
 	// this is a hack ¯\_(ツ)_/¯
 	SingleBuild Build `yaml:"build,omitempty"`
diff --git a/pkg/defaults/defaults.go b/pkg/defaults/defaults.go
index 9cefe262..38612ec2 100644
--- a/pkg/defaults/defaults.go
+++ b/pkg/defaults/defaults.go
@@ -19,6 +19,7 @@ import (
 	"github.com/goreleaser/goreleaser/internal/pipe/sign"
 	"github.com/goreleaser/goreleaser/internal/pipe/snapcraft"
 	"github.com/goreleaser/goreleaser/internal/pipe/snapshot"
+	"github.com/goreleaser/goreleaser/internal/pipe/sourcearchive"
 	"github.com/goreleaser/goreleaser/pkg/context"
 )
 
@@ -38,6 +39,7 @@ var Defaulters = []Defaulter{
 	release.Pipe{},
 	project.Pipe{},
 	build.Pipe{},
+	sourcearchive.Pipe{},
 	archive.Pipe{},
 	nfpm.Pipe{},
 	snapcraft.Pipe{},
diff --git a/www/content/source.md b/www/content/source.md
new file mode 100644
index 00000000..9bf7ca19
--- /dev/null
+++ b/www/content/source.md
@@ -0,0 +1,28 @@
+---
+title: Source Archive
+series: customization
+hideFromIndex: true
+weight: 41
+---
+
+You may add the current tag source archive to the release as well. This is particularly
+useful if you want to sign it, for example.
+
+```yml
+# .goreleaser.yml
+source:
+  # Whether this pipe is enabled or not.
+  # Defaults to `false`
+  enabled: true
+
+  # Name template of the final archive.
+  # Defaults to `{{ .ProjectName }}-{{ .Version }}`
+  name_template: '{{ .ProjectName }}'
+
+  # Format of the archive.
+  # Any format git-archive supports, this supports too.
+  # Defaults to `tar.gz`
+  format: 'tar'
+```
+
+> Learn more about the [name template engine](/templates).


Test patch:
diff --git a/internal/pipe/blob/blob_minio_test.go b/internal/pipe/blob/blob_minio_test.go
index b8d673a8..fdb926ed 100644
--- a/internal/pipe/blob/blob_minio_test.go
+++ b/internal/pipe/blob/blob_minio_test.go
@@ -25,8 +25,12 @@ func TestMinioUpload(t *testing.T) {
 	var listen = randomListen(t)
 	folder, err := ioutil.TempDir("", "goreleasertest")
 	assert.NoError(t, err)
+	srcpath := filepath.Join(folder, "source.tar.gz")
 	tgzpath := filepath.Join(folder, "bin.tar.gz")
 	debpath := filepath.Join(folder, "bin.deb")
+	checkpath := filepath.Join(folder, "check.txt")
+	assert.NoError(t, ioutil.WriteFile(checkpath, []byte("fake checksums"), 0744))
+	assert.NoError(t, ioutil.WriteFile(srcpath, []byte("fake\nsrc"), 0744))
 	assert.NoError(t, ioutil.WriteFile(tgzpath, []byte("fake\ntargz"), 0744))
 	assert.NoError(t, ioutil.WriteFile(debpath, []byte("fake\ndeb"), 0744))
 	var ctx = context.New(config.Project{
@@ -43,6 +47,19 @@ func TestMinioUpload(t *testing.T) {
 		},
 	})
 	ctx.Git = context.GitInfo{CurrentTag: "v1.0.0"}
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Type: artifact.Checksum,
+		Name: "checksum.txt",
+		Path: checkpath,
+	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Type: artifact.UploadableSourceArchive,
+		Name: "source.tar.gz",
+		Path: srcpath,
+		Extra: map[string]interface{}{
+			"Format": "tar.gz",
+		},
+	})
 	ctx.Artifacts.Add(&artifact.Artifact{
 		Type: artifact.UploadableArchive,
 		Name: "bin.tar.gz",
@@ -65,6 +82,13 @@ func TestMinioUpload(t *testing.T) {
 	prepareEnv(t, listen)
 	assert.NoError(t, Pipe{}.Default(ctx))
 	assert.NoError(t, Pipe{}.Publish(ctx))
+
+	require.Subset(t, getFiles(t, ctx, ctx.Config.Blobs[0]), []string{
+		"testupload/v1.0.0/bin.deb",
+		"testupload/v1.0.0/bin.tar.gz",
+		"testupload/v1.0.0/checksum.txt",
+		"testupload/v1.0.0/source.tar.gz",
+	})
 }
 
 func TestMinioUploadCustomBucketID(t *testing.T) {
@@ -204,3 +228,21 @@ func stop(t *testing.T, name string) {
 func removeTestData(t *testing.T) {
 	_ = os.RemoveAll("./testdata/data/test/testupload") // dont care if it fails
 }
+
+func getFiles(t *testing.T, ctx *context.Context, blob config.Blob) []string {
+	var bucket = Bucket{}
+	url, err := bucket.url(ctx, blob)
+	require.NoError(t, err)
+	conn, err := bucket.Connect(ctx, url)
+	require.NoError(t, err)
+	var iter = conn.List(nil)
+	var files []string
+	for {
+		file, err := iter.Next(ctx)
+		if err != nil {
+			break
+		}
+		files = append(files, file.Key)
+	}
+	return files
+}
diff --git a/internal/pipe/checksums/checksums_test.go b/internal/pipe/checksums/checksums_test.go
index 4b5314fc..01c58c51 100644
--- a/internal/pipe/checksums/checksums_test.go
+++ b/internal/pipe/checksums/checksums_test.go
@@ -163,3 +163,5 @@ func TestDefaultSet(t *testing.T) {
 	assert.NoError(t, Pipe{}.Default(ctx))
 	assert.Equal(t, "checksums.txt", ctx.Config.Checksum.NameTemplate)
 }
+
+// TODO: add tests for LinuxPackage and UploadableSourceArchive
diff --git a/internal/pipe/release/release_test.go b/internal/pipe/release/release_test.go
index 2f8bfaac..9704a540 100644
--- a/internal/pipe/release/release_test.go
+++ b/internal/pipe/release/release_test.go
@@ -26,6 +26,8 @@ func TestRunPipeWithoutIDsThenDoesNotFilter(t *testing.T) {
 	assert.NoError(t, err)
 	tarfile, err := os.Create(filepath.Join(folder, "bin.tar.gz"))
 	assert.NoError(t, err)
+	srcfile, err := os.Create(filepath.Join(folder, "source.tar.gz"))
+	assert.NoError(t, err)
 	debfile, err := os.Create(filepath.Join(folder, "bin.deb"))
 	assert.NoError(t, err)
 	filteredtarfile, err := os.Create(filepath.Join(folder, "filtered.tar.gz"))
@@ -76,10 +78,19 @@ func TestRunPipeWithoutIDsThenDoesNotFilter(t *testing.T) {
 			"ID": "bar",
 		},
 	})
+	ctx.Artifacts.Add(&artifact.Artifact{
+		Type: artifact.UploadableSourceArchive,
+		Name: "source.tar.gz",
+		Path: srcfile.Name(),
+		Extra: map[string]interface{}{
+			"Format": "tar.gz",
+		},
+	})
 	client := &DummyClient{}
 	assert.NoError(t, doPublish(ctx, client))
 	assert.True(t, client.CreatedRelease)
 	assert.True(t, client.UploadedFile)
+	assert.Contains(t, client.UploadedFileNames, "source.tar.gz")
 	assert.Contains(t, client.UploadedFileNames, "bin.deb")
 	assert.Contains(t, client.UploadedFileNames, "bin.tar.gz")
 	assert.Contains(t, client.UploadedFileNames, "filtered.deb")
diff --git a/internal/pipe/sourcearchive/source_test.go b/internal/pipe/sourcearchive/source_test.go
new file mode 100644
index 00000000..6d1b1408
--- /dev/null
+++ b/internal/pipe/sourcearchive/source_test.go
@@ -0,0 +1,85 @@
+package sourcearchive
+
+import (
+	"io/ioutil"
+	"os"
+	"path/filepath"
+	"testing"
+
+	"github.com/goreleaser/goreleaser/internal/artifact"
+	"github.com/goreleaser/goreleaser/internal/testlib"
+	"github.com/goreleaser/goreleaser/pkg/config"
+	"github.com/goreleaser/goreleaser/pkg/context"
+	"github.com/stretchr/testify/require"
+)
+
+func TestArchive(t *testing.T) {
+	for _, format := range []string{"tar.gz", "tar", "zip"} {
+		t.Run(format, func(t *testing.T) {
+			tmp, back := testlib.Mktmp(t)
+			defer back()
+			require.NoError(t, os.Mkdir("dist", 0744))
+
+			testlib.GitInit(t)
+			require.NoError(t, ioutil.WriteFile("code.txt", []byte("not really code"), 0655))
+			require.NoError(t, ioutil.WriteFile("README.md", []byte("# my dope fake project"), 0655))
+			testlib.GitAdd(t)
+			testlib.GitCommit(t, "feat: first")
+
+			var ctx = context.New(config.Project{
+				ProjectName: "foo",
+				Dist:        "dist",
+				Source: config.Source{
+					Format:  format,
+					Enabled: true,
+				},
+			})
+			ctx.Git.FullCommit = "HEAD"
+			ctx.Version = "1.0.0"
+
+			require.NoError(t, Pipe{}.Default(ctx))
+			require.NoError(t, Pipe{}.Run(ctx))
+
+			var artifacts = ctx.Artifacts.List()
+			require.Len(t, artifacts, 1)
+			require.Equal(t, artifact.Artifact{
+				Type: artifact.UploadableSourceArchive,
+				Name: "foo-1.0.0." + format,
+				Path: "dist/foo-1.0.0." + format,
+				Extra: map[string]interface{}{
+					"Format": format,
+				},
+			}, *artifacts[0])
+			stat, err := os.Stat(filepath.Join(tmp, "dist", "foo-1.0.0."+format))
+			require.NoError(t, err)
+			require.Greater(t, stat.Size(), int64(100))
+		})
+	}
+}
+
+func TestDefault(t *testing.T) {
+	var ctx = context.New(config.Project{})
+	require.NoError(t, Pipe{}.Default(ctx))
+	require.Equal(t, config.Source{
+		NameTemplate: "{{ .ProjectName }}-{{ .Version }}",
+		Format:       "tar.gz",
+	}, ctx.Config.Source)
+}
+
+func TestInvalidNameTemplate(t *testing.T) {
+	var ctx = context.New(config.Project{
+		Source: config.Source{
+			Enabled:      true,
+			NameTemplate: "{{ .foo }-asdda",
+		},
+	})
+	require.EqualError(t, Pipe{}.Run(ctx), "template: tmpl:1: unexpected \"}\" in operand")
+}
+
+func TestDisabled(t *testing.T) {
+	testlib.AssertSkipped(t, Pipe{}.Run(context.New(config.Project{})))
+}
+
+func TestString(t *testing.T) {
+	require.NotEmpty(t, Pipe{}.String())
+}