diff --git a/.goreleaser.yml b/.goreleaser.yml index 7de7fe6d86..1a39855326 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -12,7 +12,7 @@ builds: main: ./cmd/src/ binary: src ldflags: - - -X main.buildTag={{.Version}} + - -X github.com/sourcegraph/src-cli/internal/version.BuildTag={{.Version}} goos: - linux - windows diff --git a/CHANGELOG.md b/CHANGELOG.md index 0666040e9d..83e0219bad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ All notable changes to `src-cli` are documented in this file. ### Added -- `src campaign [apply|preview]` can now make use of Docker volumes, rather than bind-mounting the host filesystem. This is now the default on macOS, as volume mounts have generally better performance there. The optional `-workspace` flag can be used to override the default. [#412](https://github.com/sourcegraph/src-cli/pull/412) +- `src campaign [apply|preview]` can now make use of Docker volumes, rather than bind-mounting the host filesystem. This is now the default on Intel macOS so long as the Docker images used in the campaign steps run as the same user, as volume mounts have generally better performance there. The optional `-workspace` flag can be used to override the default. [#412](https://github.com/sourcegraph/src-cli/pull/412) ### Changed diff --git a/cmd/src/campaigns_common.go b/cmd/src/campaigns_common.go index 64fd032d19..bd50cc0921 100644 --- a/cmd/src/campaigns_common.go +++ b/cmd/src/campaigns_common.go @@ -101,18 +101,9 @@ func newCampaignsApplyFlags(flagSet *flag.FlagSet, cacheDir, tempDir string) *ca "If true, errors encountered while executing steps in a repository won't stop the execution of the campaign spec but only cause that repository to be skipped.", ) - // We default to bind workspaces on everything except ARM64 macOS at - // present. In the future, we'll likely want to switch the default for ARM64 - // macOS as well, but this requires access to the hardware for testing. - var defaultWorkspace string - if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" { - defaultWorkspace = "volume" - } else { - defaultWorkspace = "bind" - } flagSet.StringVar( - &caf.workspace, "workspace", defaultWorkspace, - `Workspace mode to use ("bind" or "volume")`, + &caf.workspace, "workspace", "auto", + `Workspace mode to use ("auto", "bind", or "volume")`, ) flagSet.BoolVar(verbose, "v", false, "print verbose output") @@ -180,36 +171,13 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se return "", "", err } - // Parse flags and build up our service options. - var errs *multierror.Error + // Parse flags and build up our service and executor options. specFile, err := campaignsOpenFileFlag(&flags.file) if err != nil { - errs = multierror.Append(errs, err) - } else { - defer specFile.Close() - } - - opts := campaigns.ExecutorOpts{ - Cache: svc.NewExecutionCache(flags.cacheDir), - Creator: svc.NewWorkspaceCreator(flags.cacheDir), - RepoFetcher: svc.NewRepoFetcher(flags.cacheDir, flags.cleanArchives), - ClearCache: flags.clearCache, - KeepLogs: flags.keepLogs, - Timeout: flags.timeout, - TempDir: flags.tempDir, - } - if flags.parallelism <= 0 { - opts.Parallelism = runtime.GOMAXPROCS(0) - } else { - opts.Parallelism = flags.parallelism - } - out.VerboseLine(output.Linef("🚧", output.StyleSuccess, "Workspace creator: %T", opts.Creator)) - executor := svc.NewExecutor(opts) - - if errs != nil { - return "", "", errs + return "", "", err } + defer specFile.Close() pending := campaignsCreatePending(out, "Parsing campaign spec") campaignSpec, rawSpec, err := campaignsParseSpec(out, svc, specFile) @@ -229,7 +197,7 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se Label: "Preparing container images", Max: 1.0, }}, nil) - err = svc.SetDockerImages(ctx, opts.Creator, campaignSpec, func(perc float64) { + err = svc.SetDockerImages(ctx, campaignSpec, func(perc float64) { imageProgress.SetValue(0, perc) }) if err != nil { @@ -255,6 +223,27 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se campaignsCompletePending(pending, "Resolved repositories") } + pending = campaignsCreatePending(out, "Preparing workspaces") + workspaceCreator := svc.NewWorkspaceCreator(ctx, flags.cacheDir, campaignSpec.Steps) + pending.VerboseLine(output.Linef("🚧", output.StyleSuccess, "Workspace creator: %T", workspaceCreator)) + campaignsCompletePending(pending, "Prepared workspaces") + + opts := campaigns.ExecutorOpts{ + Cache: svc.NewExecutionCache(flags.cacheDir), + Creator: workspaceCreator, + RepoFetcher: svc.NewRepoFetcher(flags.cacheDir, flags.cleanArchives), + ClearCache: flags.clearCache, + KeepLogs: flags.keepLogs, + Timeout: flags.timeout, + TempDir: flags.tempDir, + } + if flags.parallelism <= 0 { + opts.Parallelism = runtime.GOMAXPROCS(0) + } else { + opts.Parallelism = flags.parallelism + } + + executor := svc.NewExecutor(opts) p := newCampaignProgressPrinter(out, *verbose, opts.Parallelism) specs, err := svc.ExecuteCampaignSpec(ctx, repos, executor, campaignSpec, p.PrintStatuses, flags.skipErrors) if err != nil && !flags.skipErrors { diff --git a/cmd/src/version.go b/cmd/src/version.go index a80d7269f3..a2aa6d2aed 100644 --- a/cmd/src/version.go +++ b/cmd/src/version.go @@ -9,13 +9,9 @@ import ( "net/http" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/version" ) -// buildTag is the git tag at the time of build and is used to -// denote the binary's current version. This value is supplied -// as an ldflag at compile time in travis. -var buildTag = "dev" - func init() { usage := ` Examples: @@ -30,7 +26,7 @@ Examples: var apiFlags = api.NewFlags(flagSet) handler := func(args []string) error { - fmt.Printf("Current version: %s\n", buildTag) + fmt.Printf("Current version: %s\n", version.BuildTag) client := cfg.apiClient(apiFlags, flagSet.Output()) recommendedVersion, err := getRecommendedVersion(context.Background(), client) diff --git a/internal/campaigns/bind_workspace.go b/internal/campaigns/bind_workspace.go index 9e978037bf..999eb5586c 100644 --- a/internal/campaigns/bind_workspace.go +++ b/internal/campaigns/bind_workspace.go @@ -30,8 +30,6 @@ func (wc *dockerBindWorkspaceCreator) Create(ctx context.Context, repo *graphql. return w, errors.Wrap(wc.prepareGitRepo(ctx, w), "preparing local git repo") } -func (*dockerBindWorkspaceCreator) DockerImages() []string { return []string{} } - func (*dockerBindWorkspaceCreator) prepareGitRepo(ctx context.Context, w *dockerBindWorkspace) error { if _, err := runGitCmd(ctx, w.dir, "init"); err != nil { return errors.Wrap(err, "git init failed") diff --git a/internal/campaigns/service.go b/internal/campaigns/service.go index 0514f59f99..68fc0d0b65 100644 --- a/internal/campaigns/service.go +++ b/internal/campaigns/service.go @@ -211,8 +211,17 @@ func (svc *Service) NewRepoFetcher(dir string, cleanArchives bool) RepoFetcher { } } -func (svc *Service) NewWorkspaceCreator(dir string) WorkspaceCreator { +func (svc *Service) NewWorkspaceCreator(ctx context.Context, dir string, steps []Step) WorkspaceCreator { + var workspace workspaceCreatorType if svc.workspace == "volume" { + workspace = workspaceCreatorVolume + } else if svc.workspace == "bind" { + workspace = workspaceCreatorBind + } else { + workspace = bestWorkspaceCreator(ctx, steps) + } + + if workspace == workspaceCreatorVolume { return &dockerVolumeWorkspaceCreator{} } return &dockerBindWorkspaceCreator{dir: dir} @@ -243,20 +252,18 @@ func (dis dockerImageSet) add(image string, digestPtr *string) { // SetDockerImages updates the steps within the campaign spec to include the // exact content digest to be used when running each step, and ensures that all -// Docker images are available, including any required by the workspace creator. +// Docker images are available, including any required by the service itself. // // Progress information is reported back to the given progress function: perc // will be a value between 0.0 and 1.0, inclusive. -func (svc *Service) SetDockerImages(ctx context.Context, creator WorkspaceCreator, spec *CampaignSpec, progress func(perc float64)) error { +func (svc *Service) SetDockerImages(ctx context.Context, spec *CampaignSpec, progress func(perc float64)) error { images := dockerImageSet{} for i, step := range spec.Steps { images.add(step.Container, &spec.Steps[i].image) } - // The workspace creator may have its own dependencies. - for _, image := range creator.DockerImages() { - images.add(image, nil) - } + // We also need to ensure we have our own utility images available. + images.add(dockerVolumeWorkspaceImage, nil) progress(0) i := 0 diff --git a/internal/campaigns/volume_workspace.go b/internal/campaigns/volume_workspace.go index 70ef6fa518..bd6b5f1a6e 100644 --- a/internal/campaigns/volume_workspace.go +++ b/internal/campaigns/volume_workspace.go @@ -10,9 +10,9 @@ import ( "github.com/sourcegraph/src-cli/internal/campaigns/graphql" "github.com/sourcegraph/src-cli/internal/exec" + "github.com/sourcegraph/src-cli/internal/version" ) -// dockerVolumeWorkspaceCreator creates dockerVolumeWorkspace instances. type dockerVolumeWorkspaceCreator struct{} var _ WorkspaceCreator = &dockerVolumeWorkspaceCreator{} @@ -31,10 +31,6 @@ func (wc *dockerVolumeWorkspaceCreator) Create(ctx context.Context, repo *graphq return w, errors.Wrap(wc.prepareGitRepo(ctx, w), "preparing local git repo") } -func (*dockerVolumeWorkspaceCreator) DockerImages() []string { - return []string{dockerWorkspaceImage} -} - func (*dockerVolumeWorkspaceCreator) createVolume(ctx context.Context) (string, error) { out, err := exec.CommandContext(ctx, "docker", "volume", "create").CombinedOutput() if err != nil { @@ -77,7 +73,7 @@ func (*dockerVolumeWorkspaceCreator) unzipRepoIntoVolume(ctx context.Context, w "--workdir", "/work", "--mount", "type=bind,source=" + zip + ",target=/tmp/zip,ro", }, common...) - opts = append(opts, dockerWorkspaceImage, "unzip", "/tmp/zip") + opts = append(opts, dockerVolumeWorkspaceImage, "unzip", "/tmp/zip") if out, err := exec.CommandContext(ctx, "docker", opts...).CombinedOutput(); err != nil { return errors.Wrapf(err, "unzip output:\n\n%s\n\n", string(out)) @@ -151,9 +147,19 @@ exec git diff --cached --no-prefix --binary return out, nil } -// dockerWorkspaceImage is the Docker image we'll run our unzip and git commands -// in. This needs to match the name defined in .github/workflows/docker.yml. -const dockerWorkspaceImage = "sourcegraph/src-campaign-volume-workspace" +// dockerVolumeWorkspaceImage is the Docker image we'll run our unzip and git +// commands in. This needs to match the name defined in +// .github/workflows/docker.yml. +var dockerVolumeWorkspaceImage = "sourcegraph/src-campaign-volume-workspace" + +func init() { + dockerTag := version.BuildTag + if version.BuildTag == version.DefaultBuildTag { + dockerTag = "latest" + } + + dockerVolumeWorkspaceImage = dockerVolumeWorkspaceImage + ":" + dockerTag +} // runScript is a utility function to mount the given shell script into a Docker // container started from the dockerWorkspaceImage, then run it and return the @@ -183,7 +189,7 @@ func (w *dockerVolumeWorkspace) runScript(ctx context.Context, target, script st "--workdir", target, "--mount", "type=bind,source=" + name + ",target=/run.sh,ro", }, common...) - opts = append(opts, dockerWorkspaceImage, "sh", "/run.sh") + opts = append(opts, dockerVolumeWorkspaceImage, "sh", "/run.sh") out, err := exec.CommandContext(ctx, "docker", opts...).CombinedOutput() if err != nil { diff --git a/internal/campaigns/volume_workspace_test.go b/internal/campaigns/volume_workspace_test.go index 3732cb6386..fa805acc1a 100644 --- a/internal/campaigns/volume_workspace_test.go +++ b/internal/campaigns/volume_workspace_test.go @@ -51,7 +51,7 @@ func TestVolumeWorkspaceCreator(t *testing.T) { "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/tmp/zip,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "unzip", "/tmp/zip", ), expect.NewGlob( @@ -59,7 +59,7 @@ func TestVolumeWorkspaceCreator(t *testing.T) { "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ), ) @@ -97,7 +97,7 @@ func TestVolumeWorkspaceCreator(t *testing.T) { "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/tmp/zip,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "unzip", "/tmp/zip", ), ) @@ -119,7 +119,7 @@ func TestVolumeWorkspaceCreator(t *testing.T) { "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/tmp/zip,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "unzip", "/tmp/zip", ), expect.NewGlob( @@ -127,7 +127,7 @@ func TestVolumeWorkspaceCreator(t *testing.T) { "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ), ) @@ -232,7 +232,7 @@ M internal/campaigns/volume_workspace_test.go "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ), ) @@ -263,7 +263,7 @@ M internal/campaigns/volume_workspace_test.go "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ), ) @@ -308,7 +308,7 @@ index 06471f4..5f9d3fa 100644 "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ), ) @@ -334,7 +334,7 @@ index 06471f4..5f9d3fa 100644 "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ), ) @@ -362,7 +362,7 @@ func TestVolumeWorkspace_runScript(t *testing.T) { "docker", "run", "--rm", "--init", "--workdir", "/work", "--mount", "type=bind,source=*,target=/run.sh,ro", "--mount", "type=volume,source="+volumeID+",target=/work", - dockerWorkspaceImage, + dockerVolumeWorkspaceImage, "sh", "/run.sh", ) if err := glob(name, arg...); err != nil { diff --git a/internal/campaigns/workspace.go b/internal/campaigns/workspace.go index 45bd9797e1..b0b44b3a89 100644 --- a/internal/campaigns/workspace.go +++ b/internal/campaigns/workspace.go @@ -1,9 +1,14 @@ package campaigns import ( + "bytes" "context" + "runtime" + "strconv" + "strings" "github.com/sourcegraph/src-cli/internal/campaigns/graphql" + "github.com/sourcegraph/src-cli/internal/exec" ) // WorkspaceCreator implementations are used to create workspaces, which manage @@ -12,10 +17,6 @@ import ( type WorkspaceCreator interface { // Create creates a new workspace for the given repository and ZIP file. Create(ctx context.Context, repo *graphql.Repository, zip string) (Workspace, error) - - // DockerImages returns any Docker images required to use workspaces created - // by this creator. - DockerImages() []string } // Workspace implementations manage per-changeset storage when executing @@ -44,3 +45,93 @@ type Workspace interface { // multiple times in the life of a workspace. Diff(ctx context.Context) ([]byte, error) } + +type workspaceCreatorType int + +const ( + workspaceCreatorBind workspaceCreatorType = iota + workspaceCreatorVolume +) + +// bestWorkspaceCreator determines the correct workspace creator to use based on +// the environment and campaign to be executed. +func bestWorkspaceCreator(ctx context.Context, steps []Step) workspaceCreatorType { + // The basic theory here is that we have two options: bind and volume. Bind + // is battle tested and always safe, but can be slow on non-Linux platforms + // because bind mounts are slow. Volume is faster on those platforms, but + // exposes users to UID mismatch issues they'd otherwise be insulated from + // by the semantics of bind mounting on non-Linux platforms: specifically, + // if you have a campaign with steps that run as UID 1000 and then UID 2000, + // you'll get errors when the second step tries to write. + + // For the time being, we're only going to consider volume mode on Intel + // macOS. + if runtime.GOOS != "darwin" || runtime.GOARCH != "amd64" { + return workspaceCreatorBind + } + + return detectBestWorkspaceCreator(ctx, steps) +} + +func detectBestWorkspaceCreator(ctx context.Context, steps []Step) workspaceCreatorType { + // OK, so we're interested in volume mode, but we need to take its + // shortcomings around mixed user environments into account. + // + // To do that, let's iterate over the Docker images that are going to be + // used and get their default UID. This admittedly only gets us so far — + // there's nothing stopping an adventurous user from running su directly in + // their script, or running a setuid binary — but it should be a good enough + // heuristic. (And, if we get this wrong, there's nothing stopping a user + // from providing a workspace type explicitly with the -workspace flag.) + // + // Once we have the UIDs, it's pretty simple: the moment we see more than + // one UID, we should fall back to bind mode. + // + // In theory, we could make this more sensitive and complicated: a non-root + // container that's followed by only root containers would actually be OK, + // but let's keep it simple for now. + uids := make(map[int]struct{}) + + for _, step := range steps { + stdout := new(bytes.Buffer) + + args := []string{ + "run", + "--rm", + "--entrypoint", "/bin/sh", + step.image, + "-c", "id -u", + } + cmd := exec.CommandContext(ctx, "docker", args...) + cmd.Stdout = stdout + + if err := cmd.Run(); err != nil { + // An error here likely indicates that `id` isn't available on the + // path. That's OK: let's not make any assumptions at this point + // about the image, and we'll default to the always safe option. + return workspaceCreatorBind + } + + // POSIX specifies the output of `id -u` as the effective UID, + // terminated by a newline. + raw := strings.TrimSpace(stdout.String()) + uid, err := strconv.Atoi(raw) + if err != nil { + // This is a bit worse than the previous error case: there's an `id` + // command on the path, but it's not returning POSIX compliant + // output. That's weird, but we really don't need it to be terminal; + // let's fall back to bind mode. + // + // TODO: when logging is available at this level, we should log an + // error at verbose level to make this easier to debug. + return workspaceCreatorBind + } + + uids[uid] = struct{}{} + if len(uids) > 1 { + return workspaceCreatorBind + } + } + + return workspaceCreatorVolume +} diff --git a/internal/campaigns/workspace_test.go b/internal/campaigns/workspace_test.go new file mode 100644 index 0000000000..9066381a89 --- /dev/null +++ b/internal/campaigns/workspace_test.go @@ -0,0 +1,115 @@ +package campaigns + +import ( + "context" + "runtime" + "testing" + + "github.com/sourcegraph/src-cli/internal/exec/expect" +) + +func TestBestWorkspaceCreator(t *testing.T) { + ctx := context.Background() + isOverridden := !(runtime.GOOS == "darwin" && runtime.GOARCH == "amd64") + + for name, tc := range map[string]struct { + behaviours map[string]expect.Behaviour + want workspaceCreatorType + }{ + "nil steps": { + behaviours: nil, + want: workspaceCreatorVolume, + }, + "no steps": { + behaviours: map[string]expect.Behaviour{}, + want: workspaceCreatorVolume, + }, + "root": { + behaviours: map[string]expect.Behaviour{ + "foo": {Stdout: []byte("0\n")}, + "bar": {Stdout: []byte("0\n")}, + }, + want: workspaceCreatorVolume, + }, + "same user": { + behaviours: map[string]expect.Behaviour{ + "foo": {Stdout: []byte("1000\n")}, + "bar": {Stdout: []byte("1000\n")}, + }, + want: workspaceCreatorVolume, + }, + "different user": { + behaviours: map[string]expect.Behaviour{ + "foo": {Stdout: []byte("1000\n")}, + "bar": {Stdout: []byte("0\n")}, + }, + want: workspaceCreatorBind, + }, + "invalid id output: string": { + behaviours: map[string]expect.Behaviour{ + "foo": {Stdout: []byte("xxx\n")}, + }, + want: workspaceCreatorBind, + }, + "invalid id output: empty": { + behaviours: map[string]expect.Behaviour{ + "foo": {Stdout: []byte("")}, + }, + want: workspaceCreatorBind, + }, + "error invoking id": { + behaviours: map[string]expect.Behaviour{ + "foo": {ExitCode: 1}, + }, + want: workspaceCreatorBind, + }, + } { + t.Run(name, func(t *testing.T) { + var ( + commands []*expect.Expectation = nil + steps []Step = nil + ) + if tc.behaviours != nil { + commands = []*expect.Expectation{} + steps = []Step{} + for image, behaviour := range tc.behaviours { + commands = append(commands, expect.NewGlob( + behaviour, + "docker", "run", "--rm", "--entrypoint", "/bin/sh", + image, "-c", "id -u", + )) + steps = append(steps, Step{image: image}) + } + } + + if !isOverridden { + // If bestWorkspaceCreator() won't short circuit on this + // platform, we're going to run the Docker commands twice by + // definition. + expect.Commands(t, append(commands, commands...)...) + } else { + expect.Commands(t, commands...) + } + + if isOverridden { + // This is an overridden platform, so the workspace type will + // always be bind from bestWorkspaceCreator(). + if have, want := bestWorkspaceCreator(ctx, steps), workspaceCreatorBind; have != want { + t.Errorf("unexpected creator type on overridden platform: have=%d want=%d", have, want) + } + } else { + if have := bestWorkspaceCreator(ctx, steps); have != tc.want { + t.Errorf("unexpected creator type on non-overridden platform: have=%d want=%d", have, tc.want) + } + } + + // Regardless of what bestWorkspaceCreator() would have done, let's + // test that the right thing happens regardless if detection were to + // actually occur. + have := detectBestWorkspaceCreator(ctx, steps) + if have != tc.want { + t.Errorf("unexpected creator type: have=%d want=%d", have, tc.want) + } + }) + } +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000000..c50b44561a --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,10 @@ +package version + +// DefaultBuildTag is the value BuildTag will be set to if this is not a release +// build. +const DefaultBuildTag = "dev" + +// BuildTag is the git tag at the time of build and is used to +// denote the binary's current version. This value is supplied +// as an ldflag at compile time by the GoReleaser action. +var BuildTag = DefaultBuildTag