Repository: goreleaser/goreleaser
Pull request #2179 (2021-04-21 17:31:36)

feat: build single target and/or build id
a rather simple way to implement a "single target build" approach.

- `goreleaser build --single-target`: builds only `runtime`'s `GOOS`/`GOARCH`
- `GOOS=linux GOARCH=darwin goreleaser build --single-target`: builds for specified `GOOS`/`GOARCH` environment variables
- `goreleaser build --single-target --id foo`: builds only `runtime`'s `GOOS`/`GOARCH` and build id `foo`

Cool things about this:
- no need to mess with anything but the command line
- known interface (GOOS/GOARCH envs)

Notes:
- if `--id` is passed but we have either 0 (default config) or 1 build configs, it is ignored (but warns)

What do you think?

--

closes https://github.com/goreleaser/goreleaser/pull/1987
closes https://github.com/goreleaser/goreleaser/issues/1852
closes https://github.com/goreleaser/goreleaser/pull/2032

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 below or on the icon next to each commit.

🔍 Inspect: [https://vercel.com/goreleaser/goreleaser/AJiFcMaNLqFneZ57DZQ1yq7tirQ8](https://vercel.com/goreleaser/goreleaser/AJiFcMaNLqFneZ57DZQ1yq7tirQ8)  
✅ Preview: [https://goreleaser-git-build-single-goreleaser.vercel.app](https://goreleaser-git-build-single-goreleaser.vercel.app)


codecov[bot]: # [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) Report
> Merging [#2179](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (35a570d) into [master](https://codecov.io/gh/goreleaser/goreleaser/commit/24d411c6ef26e2df809540202305817bffb72a8c?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) (24d411c) will **increase** coverage by `0.05%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/goreleaser/goreleaser/pull/2179/graphs/tree.svg?width=650&height=150&src=pr&token=3bJvv6b7hN&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)

```diff
@@            Coverage Diff             @@
##           master    #2179      +/-   ##
==========================================
+ Coverage   82.65%   82.71%   +0.05%     
==========================================
  Files          75       75              
  Lines        4123     4136      +13     
==========================================
+ Hits         3408     3421      +13     
  Misses        581      581              
  Partials      134      134              
```


| [Impacted Files](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser) | Coverage Δ | |
|---|---|---|
| [cmd/build.go](https://codecov.io/gh/goreleaser/goreleaser/pull/2179/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser#diff-Y21kL2J1aWxkLmdv) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Last update [24d411c...35a570d](https://codecov.io/gh/goreleaser/goreleaser/pull/2179?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=goreleaser).

till: This looks great!

Patch:
diff --git a/cmd/build.go b/cmd/build.go
index 90a233d3..1e7993cc 100644
--- a/cmd/build.go
+++ b/cmd/build.go
@@ -1,6 +1,8 @@
 package cmd
 
 import (
+	"fmt"
+	"os"
 	"runtime"
 	"time"
 
@@ -9,6 +11,7 @@ import (
 	"github.com/fatih/color"
 	"github.com/goreleaser/goreleaser/internal/middleware"
 	"github.com/goreleaser/goreleaser/internal/pipeline"
+	"github.com/goreleaser/goreleaser/pkg/config"
 	"github.com/goreleaser/goreleaser/pkg/context"
 	"github.com/spf13/cobra"
 )
@@ -20,6 +23,7 @@ type buildCmd struct {
 
 type buildOpts struct {
 	config        string
+	id            string
 	snapshot      bool
 	skipValidate  bool
 	skipPostHooks bool
@@ -27,12 +31,13 @@ type buildOpts struct {
 	deprecated    bool
 	parallelism   int
 	timeout       time.Duration
+	singleTarget  bool
 }
 
 func newBuildCmd() *buildCmd {
-	var root = &buildCmd{}
+	root := &buildCmd{}
 	// nolint: dupl
-	var cmd = &cobra.Command{
+	cmd := &cobra.Command{
 		Use:           "build",
 		Aliases:       []string{"b"},
 		Short:         "Builds the current project",
@@ -65,6 +70,8 @@ func newBuildCmd() *buildCmd {
 	cmd.Flags().BoolVar(&root.opts.rmDist, "rm-dist", false, "Remove the dist folder before building")
 	cmd.Flags().IntVarP(&root.opts.parallelism, "parallelism", "p", runtime.NumCPU(), "Amount tasks to run concurrently")
 	cmd.Flags().DurationVar(&root.opts.timeout, "timeout", 30*time.Minute, "Timeout to the entire build process")
+	cmd.Flags().BoolVar(&root.opts.singleTarget, "single-target", false, "Builds only for current GOOS and GOARCH")
+	cmd.Flags().StringVar(&root.opts.id, "id", "", "Builds only the specified build id")
 	cmd.Flags().BoolVar(&root.opts.deprecated, "deprecated", false, "Force print the deprecation message - tests only")
 	_ = cmd.Flags().MarkHidden("deprecated")
 
@@ -79,7 +86,9 @@ func buildProject(options buildOpts) (*context.Context, error) {
 	}
 	ctx, cancel := context.NewWithTimeout(cfg, options.timeout)
 	defer cancel()
-	setupBuildContext(ctx, options)
+	if err := setupBuildContext(ctx, options); err != nil {
+		return nil, err
+	}
 	return ctx, ctrlc.Default.Run(ctx, func() error {
 		for _, pipe := range pipeline.BuildPipeline {
 			if err := middleware.Logging(
@@ -94,7 +103,7 @@ func buildProject(options buildOpts) (*context.Context, error) {
 	})
 }
 
-func setupBuildContext(ctx *context.Context, options buildOpts) *context.Context {
+func setupBuildContext(ctx *context.Context, options buildOpts) error {
 	ctx.Parallelism = options.parallelism
 	log.Debugf("parallelism: %v", ctx.Parallelism)
 	ctx.Snapshot = options.snapshot
@@ -103,7 +112,59 @@ func setupBuildContext(ctx *context.Context, options buildOpts) *context.Context
 	ctx.RmDist = options.rmDist
 	ctx.SkipTokenCheck = true
 
+	if options.singleTarget {
+		setupBuildSingleTarget(ctx)
+	}
+
+	if options.id != "" {
+		if err := setupBuildID(ctx, options.id); err != nil {
+			return err
+		}
+	}
+
 	// test only
 	ctx.Deprecated = options.deprecated
-	return ctx
+	return nil
+}
+
+func setupBuildSingleTarget(ctx *context.Context) {
+	goos := os.Getenv("GOOS")
+	if goos == "" {
+		goos = runtime.GOOS
+	}
+	goarch := os.Getenv("GOARCH")
+	if goarch == "" {
+		goarch = runtime.GOARCH
+	}
+	log.Infof("building only for %s/%s", goos, goarch)
+	if len(ctx.Config.Builds) == 0 {
+		ctx.Config.Builds = append(ctx.Config.Builds, config.Build{})
+	}
+	for i := range ctx.Config.Builds {
+		build := &ctx.Config.Builds[i]
+		build.Goos = []string{goos}
+		build.Goarch = []string{goarch}
+	}
+}
+
+func setupBuildID(ctx *context.Context, id string) error {
+	if len(ctx.Config.Builds) < 2 {
+		log.Warn("single build in config, '--id' ignored")
+		return nil
+	}
+
+	var keep []config.Build
+	for _, build := range ctx.Config.Builds {
+		if build.ID == id {
+			keep = append(keep, build)
+			break
+		}
+	}
+
+	if len(keep) == 0 {
+		return fmt.Errorf("no builds with id '%s'", id)
+	}
+
+	ctx.Config.Builds = keep
+	return nil
 }
diff --git a/internal/client/github.go b/internal/client/github.go
index 39e1d33a..dd0b316b 100644
--- a/internal/client/github.go
+++ b/internal/client/github.go
@@ -144,8 +144,11 @@ func (c *githubClient) CreateRelease(ctx *context.Context, body string) (string,
 		Body:                   github.String(body),
 		Draft:                  github.Bool(ctx.Config.Release.Draft),
 		Prerelease:             github.Bool(ctx.PreRelease),
-		DiscussionCategoryName: github.String(ctx.Config.Release.DiscussionCategoryName),
 	}
+	if ctx.Config.Release.DiscussionCategoryName != "" {
+		data.DiscussionCategoryName = github.String(ctx.Config.Release.DiscussionCategoryName)
+	}
+	
 	release, _, err = c.client.Repositories.GetReleaseByTag(
 		ctx,
 		ctx.Config.Release.GitHub.Owner,


Test patch:
diff --git a/cmd/build_test.go b/cmd/build_test.go
index 97882c86..41414fc3 100644
--- a/cmd/build_test.go
+++ b/cmd/build_test.go
@@ -1,6 +1,8 @@
 package cmd
 
 import (
+	"os"
+	"runtime"
 	"testing"
 
 	"github.com/goreleaser/goreleaser/pkg/config"
@@ -10,15 +12,22 @@ import (
 
 func TestBuild(t *testing.T) {
 	setup(t)
-	var cmd = newBuildCmd()
+	cmd := newBuildCmd()
 	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2", "--deprecated"})
 	require.NoError(t, cmd.cmd.Execute())
 }
 
+func TestBuildSingleTarget(t *testing.T) {
+	setup(t)
+	cmd := newBuildCmd()
+	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2", "--deprecated", "--single-target"})
+	require.NoError(t, cmd.cmd.Execute())
+}
+
 func TestBuildInvalidConfig(t *testing.T) {
 	setup(t)
 	createFile(t, "goreleaser.yml", "foo: bar")
-	var cmd = newBuildCmd()
+	cmd := newBuildCmd()
 	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2", "--deprecated"})
 	require.EqualError(t, cmd.cmd.Execute(), "yaml: unmarshal errors:\n  line 1: field foo not found in type config.Project")
 }
@@ -26,18 +35,20 @@ func TestBuildInvalidConfig(t *testing.T) {
 func TestBuildBrokenProject(t *testing.T) {
 	setup(t)
 	createFile(t, "main.go", "not a valid go file")
-	var cmd = newBuildCmd()
+	cmd := newBuildCmd()
 	cmd.cmd.SetArgs([]string{"--snapshot", "--timeout=1m", "--parallelism=2"})
 	require.EqualError(t, cmd.cmd.Execute(), "failed to parse dir: .: main.go:1:1: expected 'package', found not")
 }
 
 func TestBuildFlags(t *testing.T) {
-	var setup = func(opts buildOpts) *context.Context {
-		return setupBuildContext(context.New(config.Project{}), opts)
+	setup := func(opts buildOpts) *context.Context {
+		ctx := context.New(config.Project{})
+		require.NoError(t, setupBuildContext(ctx, opts))
+		return ctx
 	}
 
 	t.Run("snapshot", func(t *testing.T) {
-		var ctx = setup(buildOpts{
+		ctx := setup(buildOpts{
 			snapshot: true,
 		})
 		require.True(t, ctx.Snapshot)
@@ -46,7 +57,7 @@ func TestBuildFlags(t *testing.T) {
 	})
 
 	t.Run("skips", func(t *testing.T) {
-		var ctx = setup(buildOpts{
+		ctx := setup(buildOpts{
 			skipValidate:  true,
 			skipPostHooks: true,
 		})
@@ -66,4 +77,82 @@ func TestBuildFlags(t *testing.T) {
 			rmDist: true,
 		}).RmDist)
 	})
+
+	t.Run("single-target", func(t *testing.T) {
+		opts := buildOpts{
+			singleTarget: true,
+		}
+
+		t.Run("runtime", func(t *testing.T) {
+			result := setup(opts)
+			require.Equal(t, []string{runtime.GOOS}, result.Config.Builds[0].Goos)
+			require.Equal(t, []string{runtime.GOARCH}, result.Config.Builds[0].Goarch)
+		})
+
+		t.Run("from env", func(t *testing.T) {
+			os.Setenv("GOOS", "linux")
+			os.Setenv("GOARCH", "arm64")
+			t.Cleanup(func() {
+				os.Unsetenv("GOOS")
+				os.Unsetenv("GOARCH")
+			})
+			result := setup(opts)
+			require.Equal(t, []string{"linux"}, result.Config.Builds[0].Goos)
+			require.Equal(t, []string{"arm64"}, result.Config.Builds[0].Goarch)
+		})
+	})
+
+	t.Run("id", func(t *testing.T) {
+		t.Run("match", func(t *testing.T) {
+			ctx := context.New(config.Project{
+				Builds: []config.Build{
+					{
+						ID: "default",
+					},
+					{
+						ID: "foo",
+					},
+				},
+			})
+			require.NoError(t, setupBuildContext(ctx, buildOpts{
+				id: "foo",
+			}))
+		})
+
+		t.Run("dont match", func(t *testing.T) {
+			ctx := context.New(config.Project{
+				Builds: []config.Build{
+					{
+						ID: "foo",
+					},
+					{
+						ID: "bazz",
+					},
+				},
+			})
+			require.EqualError(t, setupBuildContext(ctx, buildOpts{
+				id: "bar",
+			}), "no builds with id 'bar'")
+		})
+
+		t.Run("default config", func(t *testing.T) {
+			ctx := context.New(config.Project{})
+			require.NoError(t, setupBuildContext(ctx, buildOpts{
+				id: "aaa",
+			}))
+		})
+
+		t.Run("single build config", func(t *testing.T) {
+			ctx := context.New(config.Project{
+				Builds: []config.Build{
+					{
+						ID: "foo",
+					},
+				},
+			})
+			require.NoError(t, setupBuildContext(ctx, buildOpts{
+				id: "not foo but doesnt matter",
+			}))
+		})
+	})
 }