mirror of
https://github.com/goreleaser/goreleaser.git
synced 2024-12-31 01:53:50 +02:00
feat: add healthcheck cmd (#3826)
here's an idea: `goreleaser healthcheck` It'll check if the needed dependencies (docker, git, etc) are available in the path... this way users can preemptively run it before releasing or to debug issues. What do you think? Here's how it looks like: <img width="1007" alt="CleanShot 2023-03-02 at 23 24 26@2x" src="https://user-images.githubusercontent.com/245435/222615682-d9cd0733-d900-43d1-9166-23b2be589b3a.png"> --------- Signed-off-by: Carlos A Becker <caarlos0@users.noreply.github.com>
This commit is contained in:
parent
b6dd26c091
commit
874d698564
107
cmd/healthcheck.go
Normal file
107
cmd/healthcheck.go
Normal file
@ -0,0 +1,107 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
"sync"
|
||||
|
||||
"github.com/caarlos0/ctrlc"
|
||||
"github.com/caarlos0/log"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/goreleaser/goreleaser/internal/middleware/skip"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/defaults"
|
||||
"github.com/goreleaser/goreleaser/pkg/context"
|
||||
"github.com/goreleaser/goreleaser/pkg/healthcheck"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type healthcheckCmd struct {
|
||||
cmd *cobra.Command
|
||||
config string
|
||||
quiet bool
|
||||
deprecated bool
|
||||
}
|
||||
|
||||
func newHealthcheckCmd() *healthcheckCmd {
|
||||
root := &healthcheckCmd{}
|
||||
cmd := &cobra.Command{
|
||||
Use: "healthcheck",
|
||||
Aliases: []string{"hc"},
|
||||
Short: "Checks if needed tools are installed",
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
Args: cobra.NoArgs,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if root.quiet {
|
||||
log.Log = log.New(io.Discard)
|
||||
}
|
||||
|
||||
cfg, err := loadConfig(root.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := context.New(cfg)
|
||||
ctx.Deprecated = root.deprecated
|
||||
|
||||
if err := ctrlc.Default.Run(ctx, func() error {
|
||||
log.Info(boldStyle.Render("checking tools..."))
|
||||
|
||||
err := defaults.Pipe{}.Run(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.IncreasePadding()
|
||||
defer log.ResetPadding()
|
||||
|
||||
var errs []error
|
||||
for _, hc := range healthcheck.Healthcheckers {
|
||||
_ = skip.Maybe(hc, func(ctx *context.Context) error {
|
||||
for _, tool := range hc.Dependencies(ctx) {
|
||||
if err := checkPath(tool); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})(ctx)
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("one or more needed tools are not present")
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof(boldStyle.Render("done!"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&root.config, "config", "f", "", "Configuration file")
|
||||
cmd.Flags().BoolVarP(&root.quiet, "quiet", "q", false, "Quiet mode: no output")
|
||||
cmd.Flags().BoolVar(&root.deprecated, "deprecated", false, "Force print the deprecation message - tests only")
|
||||
_ = cmd.Flags().MarkHidden("deprecated")
|
||||
|
||||
root.cmd = cmd
|
||||
return root
|
||||
}
|
||||
|
||||
var toolsChecked = &sync.Map{}
|
||||
|
||||
func checkPath(tool string) error {
|
||||
if _, ok := toolsChecked.LoadOrStore(tool, true); ok {
|
||||
return nil
|
||||
}
|
||||
if _, err := exec.LookPath(tool); err != nil {
|
||||
st := log.Styles[log.ErrorLevel]
|
||||
log.Warnf("%s %s - %s", st.Render("⚠"), codeStyle.Render(tool), st.Render("not present in path"))
|
||||
return err
|
||||
}
|
||||
st := lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true)
|
||||
log.Infof("%s %s", st.Render("✓"), codeStyle.Render(tool))
|
||||
return nil
|
||||
}
|
31
cmd/healthcheck_test.go
Normal file
31
cmd/healthcheck_test.go
Normal file
@ -0,0 +1,31 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestHealthcheckSystem(t *testing.T) {
|
||||
cmd := newHealthcheckCmd()
|
||||
cmd.cmd.SetArgs([]string{"-f", "testdata/good.yml"})
|
||||
require.NoError(t, cmd.cmd.Execute())
|
||||
}
|
||||
|
||||
func TestHealthcheckConfigThatDoesNotExist(t *testing.T) {
|
||||
cmd := newHealthcheckCmd()
|
||||
cmd.cmd.SetArgs([]string{"-f", "testdata/nope.yml"})
|
||||
require.EqualError(t, cmd.cmd.Execute(), "open testdata/nope.yml: no such file or directory")
|
||||
}
|
||||
|
||||
func TestHealthcheckMissingTool(t *testing.T) {
|
||||
cmd := newHealthcheckCmd()
|
||||
cmd.cmd.SetArgs([]string{"-f", "testdata/missing_tool.yml"})
|
||||
require.EqualError(t, cmd.cmd.Execute(), "one or more needed tools are not present")
|
||||
}
|
||||
|
||||
func TestHealthcheckQuier(t *testing.T) {
|
||||
cmd := newHealthcheckCmd()
|
||||
cmd.cmd.SetArgs([]string{"-f", "testdata/good.yml", "--quiet"})
|
||||
require.NoError(t, cmd.cmd.Execute())
|
||||
}
|
@ -12,7 +12,10 @@ import (
|
||||
cobracompletefig "github.com/withfig/autocomplete-tools/integrations/cobra"
|
||||
)
|
||||
|
||||
var boldStyle = lipgloss.NewStyle().Bold(true)
|
||||
var (
|
||||
boldStyle = lipgloss.NewStyle().Bold(true)
|
||||
codeStyle = lipgloss.NewStyle().Italic(true)
|
||||
)
|
||||
|
||||
func Execute(version string, exit func(int), args []string) {
|
||||
newRootCmd(version, exit).Execute(args)
|
||||
@ -79,6 +82,7 @@ Check out our website for more information, examples and documentation: https://
|
||||
newBuildCmd().cmd,
|
||||
newReleaseCmd().cmd,
|
||||
newCheckCmd().cmd,
|
||||
newHealthcheckCmd().cmd,
|
||||
newInitCmd().cmd,
|
||||
newDocsCmd().cmd,
|
||||
newManCmd().cmd,
|
||||
|
2
cmd/testdata/missing_tool.yml
vendored
Normal file
2
cmd/testdata/missing_tool.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
signs:
|
||||
- cmd: cosignd
|
@ -30,8 +30,9 @@ var cmd cmder = stdCmd{}
|
||||
// Pipe for chocolatey packaging.
|
||||
type Pipe struct{}
|
||||
|
||||
func (Pipe) String() string { return "chocolatey packages" }
|
||||
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Chocolateys) == 0 }
|
||||
func (Pipe) String() string { return "chocolatey packages" }
|
||||
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Chocolateys) == 0 }
|
||||
func (Pipe) Dependencies(ctx *context.Context) []string { return []string{"choco"} }
|
||||
|
||||
// Default sets the pipe defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
|
@ -301,6 +301,10 @@ func TestPublish(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
require.Equal(t, []string{"choco"}, Pipe{}.Dependencies(nil))
|
||||
}
|
||||
|
||||
type fakeCmd struct {
|
||||
execFn func() ([]byte, error)
|
||||
}
|
||||
|
@ -31,6 +31,18 @@ type Pipe struct{}
|
||||
func (Pipe) String() string { return "docker images" }
|
||||
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Dockers) == 0 || ctx.SkipDocker }
|
||||
|
||||
func (Pipe) Dependencies(ctx *context.Context) []string {
|
||||
var cmds []string
|
||||
for _, s := range ctx.Config.Dockers {
|
||||
switch s.Use {
|
||||
case useDocker, useBuildx:
|
||||
cmds = append(cmds, "docker")
|
||||
// TODO: how to check if buildx is installed
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Default sets the pipe defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
ids := ids.New("dockers")
|
||||
|
@ -1397,3 +1397,20 @@ func TestWithDigest(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
ctx := testctx.NewWithCfg(config.Project{
|
||||
Dockers: []config.Docker{
|
||||
{Use: useBuildx},
|
||||
{Use: useDocker},
|
||||
{Use: "nope"},
|
||||
},
|
||||
DockerManifests: []config.DockerManifest{
|
||||
{Use: useBuildx},
|
||||
{Use: useDocker},
|
||||
{Use: "nope"},
|
||||
},
|
||||
})
|
||||
require.Equal(t, []string{"docker", "docker"}, Pipe{}.Dependencies(ctx))
|
||||
require.Equal(t, []string{"docker", "docker"}, ManifestPipe{}.Dependencies(ctx))
|
||||
}
|
||||
|
@ -24,6 +24,18 @@ func (ManifestPipe) Skip(ctx *context.Context) bool {
|
||||
return len(ctx.Config.DockerManifests) == 0 || ctx.SkipDocker
|
||||
}
|
||||
|
||||
func (ManifestPipe) Dependencies(ctx *context.Context) []string {
|
||||
var cmds []string
|
||||
for _, s := range ctx.Config.DockerManifests {
|
||||
switch s.Use {
|
||||
case useDocker, useBuildx:
|
||||
cmds = append(cmds, "docker")
|
||||
// TODO: check buildx
|
||||
}
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Default sets the pipe defaults.
|
||||
func (ManifestPipe) Default(ctx *context.Context) error {
|
||||
ids := ids.New("docker_manifests")
|
||||
|
@ -31,6 +31,14 @@ func (Pipe) Skip(ctx *context.Context) bool {
|
||||
return ctx.SkipSBOMCataloging || len(ctx.Config.SBOMs) == 0
|
||||
}
|
||||
|
||||
func (Pipe) Dependencies(ctx *context.Context) []string {
|
||||
var cmds []string
|
||||
for _, s := range ctx.Config.SBOMs {
|
||||
cmds = append(cmds, s.Cmd)
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Default sets the Pipes defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
ids := ids.New("sboms")
|
||||
|
@ -750,3 +750,13 @@ func Test_templateNames(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
ctx := testctx.NewWithCfg(config.Project{
|
||||
SBOMs: []config.SBOM{
|
||||
{Cmd: "syft"},
|
||||
{Cmd: "foobar"},
|
||||
},
|
||||
})
|
||||
require.Equal(t, []string{"syft", "foobar"}, Pipe{}.Dependencies(ctx))
|
||||
}
|
||||
|
@ -27,6 +27,14 @@ type Pipe struct{}
|
||||
func (Pipe) String() string { return "signing artifacts" }
|
||||
func (Pipe) Skip(ctx *context.Context) bool { return ctx.SkipSign || len(ctx.Config.Signs) == 0 }
|
||||
|
||||
func (Pipe) Dependencies(ctx *context.Context) []string {
|
||||
var cmds []string
|
||||
for _, s := range ctx.Config.Signs {
|
||||
cmds = append(cmds, s.Cmd)
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Default sets the Pipes defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
ids := ids.New("signs")
|
||||
|
@ -19,6 +19,14 @@ func (DockerPipe) Skip(ctx *context.Context) bool {
|
||||
return ctx.SkipSign || len(ctx.Config.DockerSigns) == 0
|
||||
}
|
||||
|
||||
func (DockerPipe) Dependencies(ctx *context.Context) []string {
|
||||
var cmds []string
|
||||
for _, s := range ctx.Config.DockerSigns {
|
||||
cmds = append(cmds, s.Cmd)
|
||||
}
|
||||
return cmds
|
||||
}
|
||||
|
||||
// Default sets the Pipes defaults.
|
||||
func (DockerPipe) Default(ctx *context.Context) error {
|
||||
ids := ids.New("docker_signs")
|
||||
|
@ -228,3 +228,13 @@ func TestDockerSkip(t *testing.T) {
|
||||
require.False(t, DockerPipe{}.Skip(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDockerDependencies(t *testing.T) {
|
||||
ctx := testctx.NewWithCfg(config.Project{
|
||||
DockerSigns: []config.Sign{
|
||||
{Cmd: "cosign"},
|
||||
{Cmd: "gpg2"},
|
||||
},
|
||||
})
|
||||
require.Equal(t, []string{"cosign", "gpg2"}, DockerPipe{}.Dependencies(ctx))
|
||||
}
|
||||
|
@ -723,3 +723,13 @@ func TestSkip(t *testing.T) {
|
||||
require.False(t, Pipe{}.Skip(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
ctx := testctx.NewWithCfg(config.Project{
|
||||
Signs: []config.Sign{
|
||||
{Cmd: "cosign"},
|
||||
{Cmd: "gpg2"},
|
||||
},
|
||||
})
|
||||
require.Equal(t, []string{"cosign", "gpg2"}, Pipe{}.Dependencies(ctx))
|
||||
}
|
||||
|
@ -99,8 +99,9 @@ const defaultNameTemplate = `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arc
|
||||
// Pipe for snapcraft packaging.
|
||||
type Pipe struct{}
|
||||
|
||||
func (Pipe) String() string { return "snapcraft packages" }
|
||||
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Snapcrafts) == 0 }
|
||||
func (Pipe) String() string { return "snapcraft packages" }
|
||||
func (Pipe) Skip(ctx *context.Context) bool { return len(ctx.Config.Snapcrafts) == 0 }
|
||||
func (Pipe) Dependencies(ctx *context.Context) []string { return []string{"snapcraft"} }
|
||||
|
||||
// Default sets the pipe defaults.
|
||||
func (Pipe) Default(ctx *context.Context) error {
|
||||
|
@ -697,6 +697,15 @@ func TestSkip(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
ctx := testctx.NewWithCfg(config.Project{
|
||||
Snapcrafts: []config.Snapcraft{
|
||||
{},
|
||||
},
|
||||
})
|
||||
require.Equal(t, []string{"snapcraft"}, Pipe{}.Dependencies(ctx))
|
||||
}
|
||||
|
||||
func requireEqualFileConents(tb testing.TB, a, b string) {
|
||||
tb.Helper()
|
||||
eq, err := gio.EqualFileContents(a, b)
|
||||
|
40
pkg/healthcheck/healthcheck.go
Normal file
40
pkg/healthcheck/healthcheck.go
Normal file
@ -0,0 +1,40 @@
|
||||
// Package healthcheck checks for missing binaries that the user needs to
|
||||
// install.
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/chocolatey"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/docker"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/sbom"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/sign"
|
||||
"github.com/goreleaser/goreleaser/internal/pipe/snapcraft"
|
||||
"github.com/goreleaser/goreleaser/pkg/context"
|
||||
)
|
||||
|
||||
// Healthchecker should be implemented by pipes that want checks.
|
||||
type Healthchecker interface {
|
||||
fmt.Stringer
|
||||
|
||||
// Dependencies return the binaries of the dependencies needed.
|
||||
Dependencies(ctx *context.Context) []string
|
||||
}
|
||||
|
||||
// Healthcheckers is the list of healthchekers.
|
||||
// nolint: gochecknoglobals
|
||||
var Healthcheckers = []Healthchecker{
|
||||
system{},
|
||||
snapcraft.Pipe{},
|
||||
sign.Pipe{},
|
||||
sign.DockerPipe{},
|
||||
sbom.Pipe{},
|
||||
docker.Pipe{},
|
||||
docker.ManifestPipe{},
|
||||
chocolatey.Pipe{},
|
||||
}
|
||||
|
||||
type system struct{}
|
||||
|
||||
func (system) String() string { return "system" }
|
||||
func (system) Dependencies(ctx *context.Context) []string { return []string{"git", "go"} }
|
17
pkg/healthcheck/healthcheck_test.go
Normal file
17
pkg/healthcheck/healthcheck_test.go
Normal file
@ -0,0 +1,17 @@
|
||||
package healthcheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goreleaser/goreleaser/internal/testctx"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDependencies(t *testing.T) {
|
||||
ctx := testctx.New()
|
||||
require.Equal(t, []string{"git", "go"}, system{}.Dependencies(ctx))
|
||||
}
|
||||
|
||||
func TestStringer(t *testing.T) {
|
||||
require.NotEmpty(t, system{}.String())
|
||||
}
|
Loading…
Reference in New Issue
Block a user