1
0
mirror of https://github.com/goreleaser/goreleaser.git synced 2025-10-30 23:58:09 +02:00

feat: continue on error (#4127)

closes #3989

Basically, when some of these pipes fail, the error will be memorized,
and all errors will be thrown in the end.

Meaning: the exit code will still be 1, but it'll not have stopped in
the first error.

Thinking of maybe adding a `--fail-fast` flag to disable this behavior
as well 🤔

---------

Signed-off-by: Carlos Alexandro Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
Carlos Alexandro Becker
2023-06-20 09:33:59 -03:00
committed by GitHub
parent e2eb95d5cf
commit 72cf8404c1
24 changed files with 174 additions and 35 deletions

View File

@@ -31,6 +31,7 @@ type releaseOpts struct {
releaseFooterTmpl string
autoSnapshot bool
snapshot bool
failFast bool
skipPublish bool
skipSign bool
skipValidate bool
@@ -83,6 +84,7 @@ func newReleaseCmd() *releaseCmd {
_ = cmd.MarkFlagFilename("release-footer-tmpl", "md", "mkd", "markdown")
cmd.Flags().BoolVar(&root.opts.autoSnapshot, "auto-snapshot", false, "Automatically sets --snapshot if the repository is dirty")
cmd.Flags().BoolVar(&root.opts.snapshot, "snapshot", false, "Generate an unversioned snapshot release, skipping all validations and without publishing any artifacts (implies --skip-publish, --skip-announce and --skip-validate)")
cmd.Flags().BoolVar(&root.opts.failFast, "fail-fast", false, "Whether to abort the release publishing on the first error")
cmd.Flags().BoolVar(&root.opts.skipPublish, "skip-publish", false, "Skips publishing artifacts (implies --skip-announce)")
cmd.Flags().BoolVar(&root.opts.skipAnnounce, "skip-announce", false, "Skips announcing releases (implies --skip-validate)")
cmd.Flags().BoolVar(&root.opts.skipSign, "skip-sign", false, "Skips signing artifacts")
@@ -144,6 +146,7 @@ func setupReleaseContext(ctx *context.Context, options releaseOpts) {
ctx.ReleaseFooterFile = options.releaseFooterFile
ctx.ReleaseFooterTmpl = options.releaseFooterTmpl
ctx.Snapshot = options.snapshot
ctx.FailFast = options.failFast
if options.autoSnapshot && git.CheckDirty(ctx) != nil {
log.Info("git repository is dirty and --auto-snapshot is set, implying --snapshot")
ctx.Snapshot = true

View File

@@ -43,11 +43,15 @@ func (m *Memo) Wrap(action middleware.Action) middleware.Action {
if err == nil {
return nil
}
if pipe.IsSkip(err) {
log.WithField("reason", err.Error()).Warn("pipe skipped")
return nil
}
m.err = multierror.Append(m.err, err)
m.Memorize(err)
return nil
}
}
func (m *Memo) Memorize(err error) {
if pipe.IsSkip(err) {
log.WithField("reason", err.Error()).Warn("pipe skipped")
return
}
m.err = multierror.Append(m.err, err)
}

View File

@@ -32,6 +32,7 @@ var ErrNoArchivesFound = errors.New("no linux archives found")
type Pipe struct{}
func (Pipe) String() string { return "arch user repositories" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.AURs) == 0 }
func (Pipe) Default(ctx *context.Context) error {

View File

@@ -18,6 +18,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -45,6 +45,7 @@ func (e ErrNoArchivesFound) Error() string {
type Pipe struct{}
func (Pipe) String() string { return "homebrew tap formula" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Brews) == 0 }
func (Pipe) Default(ctx *context.Context) error {

View File

@@ -17,6 +17,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -31,6 +31,7 @@ var cmd cmder = stdCmd{}
type Pipe struct{}
func (Pipe) String() string { return "chocolatey packages" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Chocolateys) == 0 }
func (Pipe) Dependencies(_ *context.Context) []string { return []string{"choco"} }

View File

@@ -16,6 +16,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -39,6 +39,7 @@ var ErrNoArchivesFound = errors.New("no archives found")
type Pipe struct{}
func (Pipe) String() string { return "krew plugin manifest" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Krews) == 0 }
func (Pipe) Default(ctx *context.Context) error {

View File

@@ -20,6 +20,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -16,6 +16,7 @@ const defaultNameTemplate = "{{ .Tag }}"
type Pipe struct{}
func (Pipe) String() string { return "milestones" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Milestones) == 0 }
// Default sets the pipe defaults.

View File

@@ -10,6 +10,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDefaultWithRepoConfig(t *testing.T) {
testlib.Mktmp(t)
testlib.GitInit(t)

View File

@@ -56,6 +56,7 @@ type Pipe struct {
}
func (Pipe) String() string { return "nixpkgs" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Dependencies(_ *context.Context) []string { return []string{"nix-prefetch-url"} }
func (p Pipe) Skip(ctx *context.Context) bool {
return len(ctx.Config.Nix) == 0 || !p.prefetcher.Available()

View File

@@ -16,6 +16,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestString(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -35,38 +35,45 @@ type Publisher interface {
Publish(ctx *context.Context) error
}
// nolint: gochecknoglobals
var publishers = []Publisher{
blob.Pipe{},
upload.Pipe{},
artifactory.Pipe{},
custompublishers.Pipe{},
docker.Pipe{},
docker.ManifestPipe{},
ko.Pipe{},
sign.DockerPipe{},
snapcraft.Pipe{},
// This should be one of the last steps
release.Pipe{},
// brew et al use the release URL, so, they should be last
nix.NewPublish(),
winget.Pipe{},
brew.Pipe{},
aur.Pipe{},
krew.Pipe{},
scoop.Pipe{},
chocolatey.Pipe{},
milestone.Pipe{},
// New publish pipeline.
func New() Pipe {
return Pipe{
pipeline: []Publisher{
blob.Pipe{},
upload.Pipe{},
artifactory.Pipe{},
custompublishers.Pipe{},
docker.Pipe{},
docker.ManifestPipe{},
ko.Pipe{},
sign.DockerPipe{},
snapcraft.Pipe{},
// This should be one of the last steps
release.Pipe{},
// brew et al use the release URL, so, they should be last
nix.NewPublish(),
winget.Pipe{},
brew.Pipe{},
aur.Pipe{},
krew.Pipe{},
scoop.Pipe{},
chocolatey.Pipe{},
milestone.Pipe{},
},
}
}
// Pipe that publishes artifacts.
type Pipe struct{}
type Pipe struct {
pipeline []Publisher
}
func (Pipe) String() string { return "publishing" }
func (Pipe) Skip(ctx *context.Context) bool { return ctx.SkipPublish }
func (Pipe) Run(ctx *context.Context) error {
for _, publisher := range publishers {
func (p Pipe) Run(ctx *context.Context) error {
memo := errhandler.Memo{}
for _, publisher := range p.pipeline {
if err := skip.Maybe(
publisher,
logging.PadLog(
@@ -74,8 +81,16 @@ func (Pipe) Run(ctx *context.Context) error {
errhandler.Handle(publisher.Publish),
),
)(ctx); err != nil {
if ig, ok := publisher.(Continuable); ok && ig.ContinueOnError() && !ctx.FailFast {
memo.Memorize(fmt.Errorf("%s: %w", publisher.String(), err))
continue
}
return fmt.Errorf("%s: failed to publish artifacts: %w", publisher.String(), err)
}
}
return nil
return memo.Error()
}
type Continuable interface {
ContinueOnError() bool
}

View File

@@ -1,10 +1,14 @@
package publish
import (
"fmt"
"testing"
"github.com/goreleaser/goreleaser/internal/pipe"
"github.com/goreleaser/goreleaser/internal/testctx"
"github.com/goreleaser/goreleaser/pkg/config"
"github.com/goreleaser/goreleaser/pkg/context"
"github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/require"
)
@@ -16,7 +20,54 @@ func TestPublish(t *testing.T) {
ctx := testctx.NewWithCfg(config.Project{
Release: config.Release{Disable: "true"},
}, testctx.GitHubTokenType)
require.NoError(t, Pipe{}.Run(ctx))
require.NoError(t, New().Run(ctx))
}
func TestPublishSuccess(t *testing.T) {
ctx := testctx.New()
lastStep := &testPublisher{}
err := Pipe{
pipeline: []Publisher{
&testPublisher{},
&testPublisher{shouldSkip: true},
&testPublisher{
shouldErr: true,
continuable: true,
},
&testPublisher{shouldSkip: true},
&testPublisher{},
&testPublisher{shouldSkip: true},
lastStep,
},
}.Run(ctx)
require.Error(t, err)
merr := &multierror.Error{}
require.ErrorAs(t, err, &merr)
require.Equal(t, merr.Len(), 1)
require.True(t, lastStep.ran)
}
func TestPublishError(t *testing.T) {
ctx := testctx.New()
lastStep := &testPublisher{}
err := Pipe{
pipeline: []Publisher{
&testPublisher{},
&testPublisher{shouldSkip: true},
&testPublisher{
shouldErr: true,
continuable: true,
},
&testPublisher{},
&testPublisher{shouldSkip: true},
&testPublisher{},
&testPublisher{shouldErr: true},
lastStep,
},
}.Run(ctx)
require.Error(t, err)
require.EqualError(t, err, "test: failed to publish artifacts: errored")
require.False(t, lastStep.ran)
}
func TestSkip(t *testing.T) {
@@ -29,3 +80,23 @@ func TestSkip(t *testing.T) {
require.False(t, Pipe{}.Skip(testctx.New()))
})
}
type testPublisher struct {
shouldErr bool
shouldSkip bool
continuable bool
ran bool
}
func (t *testPublisher) ContinueOnError() bool { return t.continuable }
func (t *testPublisher) String() string { return "test" }
func (t *testPublisher) Publish(_ *context.Context) error {
if t.shouldSkip {
return pipe.Skip("skipped")
}
if t.shouldErr {
return fmt.Errorf("errored")
}
t.ran = true
return nil
}

View File

@@ -59,7 +59,8 @@ const scoopConfigExtra = "ScoopConfig"
// Pipe that builds and publishes scoop manifests.
type Pipe struct{}
func (Pipe) String() string { return "scoop manifests" }
func (Pipe) String() string { return "scoop manifests" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool {
return ctx.Config.Scoop.Repository.Name == "" && len(ctx.Config.Scoops) == 0
}

View File

@@ -16,6 +16,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -103,6 +103,7 @@ const defaultNameTemplate = `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arc
type Pipe struct{}
func (Pipe) String() string { return "snapcraft packages" }
func (Pipe) ContinueOnError() bool { return true }
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Snapcrafts) == 0 }
func (Pipe) Dependencies(_ *context.Context) []string { return []string{"snapcraft"} }

View File

@@ -17,6 +17,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestDescription(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -43,7 +43,8 @@ const wingetConfigExtra = "WingetConfig"
type Pipe struct{}
func (Pipe) String() string { return "winget" }
func (Pipe) String() string { return "winget" }
func (Pipe) ContinueOnError() bool { return true }
func (p Pipe) Skip(ctx *context.Context) bool {
return len(ctx.Config.Winget) == 0
}

View File

@@ -16,6 +16,10 @@ import (
"github.com/stretchr/testify/require"
)
func TestContinueOnError(t *testing.T) {
require.True(t, Pipe{}.ContinueOnError())
}
func TestString(t *testing.T) {
require.NotEmpty(t, Pipe{}.String())
}

View File

@@ -128,7 +128,7 @@ var Pipeline = append(
// create and push docker images
docker.Pipe{},
// publishes artifacts
publish.Pipe{},
publish.New(),
// creates a metadata.json and an artifacts.json files in the dist folder
metadata.Pipe{},
// announce releases

View File

@@ -91,6 +91,7 @@ type Context struct {
Version string
ModulePath string
Snapshot bool
FailFast bool
SkipPostBuildHooks bool
SkipPublish bool
SkipAnnounce bool