You've already forked goreleaser
							
							
				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:
		
				
					committed by
					
						 GitHub
						GitHub
					
				
			
			
				
	
			
			
			
						parent
						
							e2eb95d5cf
						
					
				
				
					commit
					72cf8404c1
				
			| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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"} } | ||||
|  | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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"} } | ||||
|  | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -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()) | ||||
| } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -91,6 +91,7 @@ type Context struct { | ||||
| 	Version            string | ||||
| 	ModulePath         string | ||||
| 	Snapshot           bool | ||||
| 	FailFast           bool | ||||
| 	SkipPostBuildHooks bool | ||||
| 	SkipPublish        bool | ||||
| 	SkipAnnounce       bool | ||||
|   | ||||
		Reference in New Issue
	
	Block a user