diff --git a/.binny.yaml b/.binny.yaml index 988846e..8b2cb5e 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -9,11 +9,11 @@ tools: module: . entrypoint: cmd/binny ldflags: - - -X main.version={{ .Version }} - - -X main.gitCommit={{ .Version }} - - -X main.gitDescription={{ .Version }} - # note: sprig functions are available: http://masterminds.github.io/sprig/ - - -X main.buildDate={{ now | date "2006-01-02T15:04:05Z07:00" }} + - -X main.version={{ .Version }} + - -X main.gitCommit={{ .Version }} + - -X main.gitDescription={{ .Version }} + # note: sprig functions are available: http://masterminds.github.io/sprig/ + - -X main.buildDate={{ now | date "2006-01-02T15:04:05Z07:00" }} - name: gh version: diff --git a/cmd/binny/cli/cli.go b/cmd/binny/cli/cli.go index 3a01748..df04194 100644 --- a/cmd/binny/cli/cli.go +++ b/cmd/binny/cli/cli.go @@ -41,6 +41,7 @@ func New(id clio.Identification) clio.Application { root.AddCommand( clio.VersionCommand(id), + command.Add(app), command.Install(app), command.Check(app), command.Run(app), diff --git a/cmd/binny/cli/command/add.go b/cmd/binny/cli/command/add.go new file mode 100644 index 0000000..fbd265c --- /dev/null +++ b/cmd/binny/cli/command/add.go @@ -0,0 +1,21 @@ +package command + +import ( + "github.com/spf13/cobra" + + "github.com/anchore/clio" +) + +func Add(app clio.Application) *cobra.Command { + cmd := app.SetupCommand(&cobra.Command{ + Use: "add", + Short: "Add a new tool to the configuration", + }) + + cmd.AddCommand( + AddGoInstall(app), + AddGithubRelease(app), + ) + + return cmd +} diff --git a/cmd/binny/cli/command/add_github_release.go b/cmd/binny/cli/command/add_github_release.go new file mode 100644 index 0000000..c3ca789 --- /dev/null +++ b/cmd/binny/cli/command/add_github_release.go @@ -0,0 +1,100 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/scylladb/go-set/strset" + "github.com/spf13/cobra" + + "github.com/anchore/binny/cmd/binny/cli/internal/yamlpatch" + "github.com/anchore/binny/cmd/binny/cli/option" + "github.com/anchore/binny/internal/log" + "github.com/anchore/binny/tool/githubrelease" + "github.com/anchore/clio" +) + +type AddGithubReleaseConfig struct { + Config string `json:"config" yaml:"config" mapstructure:"config"` + option.Core `json:"" yaml:",inline" mapstructure:",squash"` + + VersionResolution option.VersionResolution `json:"version-resolver" yaml:"version-resolver" mapstructure:"version-resolver"` +} + +func AddGithubRelease(app clio.Application) *cobra.Command { + cfg := &AddGithubReleaseConfig{ + Core: option.DefaultCore(), + } + + return app.SetupCommand(&cobra.Command{ + Use: "github-release OWNER/REPO@VERSION", + Short: "Add a new tool configuration that sources binaries from GitHub releases", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if !strings.Contains(args[0], "/") { + return fmt.Errorf("invalid 'owner/project@version' format: %q", args[0]) + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runGithubReleaseConfig(*cfg, args[0]) + }, + }, cfg) +} + +func runGithubReleaseConfig(cmdCfg AddGithubReleaseConfig, repoVersion string) error { + fields := strings.Split(repoVersion, "@") + var repo, name, version string + + switch len(fields) { + case 1: + repo = repoVersion + version = "latest" + case 2: + repo = fields[0] + version = fields[1] + default: + return fmt.Errorf("invalid owner/project@version format: %s", repoVersion) + } + + fields = strings.Split(repo, "/") + if len(fields) != 2 { + return fmt.Errorf("invalid owner/project format: %s", repo) + } + + name = fields[1] + + if strset.New(cmdCfg.Tools.Names()...).Has(name) { + // TODO: should this be an error? + log.Warnf("tool %q already configured", name) + return nil + } + + vCfg := cmdCfg.VersionResolution + + coreInstallParams := githubrelease.InstallerParameters{ + Repo: repo, + } + + installParamMap, err := toMap(coreInstallParams) + if err != nil { + return fmt.Errorf("unable to encode install params: %w", err) + } + + installMethod := githubrelease.InstallMethod + + log.WithFields("name", name, "version", version, "method", installMethod).Info("adding tool") + + toolCfg := option.Tool{ + Name: name, + Version: option.ToolVersionConfig{ + Want: version, + Constraint: vCfg.Constraint, + ResolveMethod: vCfg.Method, + }, + InstallMethod: installMethod, + Parameters: installParamMap, + } + + return yamlpatch.Write(cmdCfg.Config, yamlToolAppender{toolCfg: toolCfg}) +} diff --git a/cmd/binny/cli/command/add_go_install.go b/cmd/binny/cli/command/add_go_install.go new file mode 100644 index 0000000..ad1c92b --- /dev/null +++ b/cmd/binny/cli/command/add_go_install.go @@ -0,0 +1,106 @@ +package command + +import ( + "fmt" + "strings" + + "github.com/google/shlex" + "github.com/scylladb/go-set/strset" + "github.com/spf13/cobra" + + "github.com/anchore/binny/cmd/binny/cli/internal/yamlpatch" + "github.com/anchore/binny/cmd/binny/cli/option" + "github.com/anchore/binny/internal/log" + "github.com/anchore/binny/tool/goinstall" + "github.com/anchore/clio" +) + +type AddGoInstallConfig struct { + Config string `json:"config" yaml:"config" mapstructure:"config"` + option.Core `json:"" yaml:",inline" mapstructure:",squash"` + + // CLI options + Install struct { + GoInstall option.GoInstall `json:"go-install" yaml:"go-install" mapstructure:"go-install"` + } `json:"install" yaml:"install" mapstructure:"install"` + + VersionResolution option.VersionResolution `json:"version-resolver" yaml:"version-resolver" mapstructure:"version-resolver"` +} + +func AddGoInstall(app clio.Application) *cobra.Command { + cfg := &AddGoInstallConfig{ + Core: option.DefaultCore(), + } + + return app.SetupCommand(&cobra.Command{ + Use: "go-install NAME@VERSION --module GOMODULE [--entrypoint PATH] [--ldflags FLAGS]", + Short: "Add a new tool configuration from 'go install ...' invocations", + Args: cobra.ExactArgs(1), + PreRunE: func(cmd *cobra.Command, args []string) error { + if cfg.Install.GoInstall.Module == "" { + return fmt.Errorf("go-install configuration requires '--module' option") + } + return nil + }, + RunE: func(cmd *cobra.Command, args []string) error { + return runAddGoInstallConfig(*cfg, args[0]) + }, + }, cfg) +} + +func runAddGoInstallConfig(cmdCfg AddGoInstallConfig, nameVersion string) error { + fields := strings.Split(nameVersion, "@") + var name, version string + + switch len(fields) { + case 1: + name = nameVersion + case 2: + name = fields[0] + version = fields[1] + default: + return fmt.Errorf("invalid name@version format: %s", nameVersion) + } + + if strset.New(cmdCfg.Tools.Names()...).Has(name) { + // TODO: should this be an error? + log.Warnf("tool %q already configured", name) + return nil + } + + iCfg := cmdCfg.Install.GoInstall + vCfg := cmdCfg.VersionResolution + + ldFlagsList, err := shlex.Split(iCfg.LDFlags) + if err != nil { + return fmt.Errorf("invalid ldflags: %w", err) + } + + coreInstallParams := goinstall.InstallerParameters{ + Module: iCfg.Module, + Entrypoint: iCfg.Entrypoint, + LDFlags: ldFlagsList, + } + + installParamMap, err := toMap(coreInstallParams) + if err != nil { + return fmt.Errorf("unable to encode install params: %w", err) + } + + installMethod := goinstall.InstallMethod + + log.WithFields("name", name, "version", version, "method", installMethod).Info("adding tool") + + toolCfg := option.Tool{ + Name: name, + Version: option.ToolVersionConfig{ + Want: version, + Constraint: vCfg.Constraint, + ResolveMethod: vCfg.Method, + }, + InstallMethod: installMethod, + Parameters: installParamMap, + } + + return yamlpatch.Write(cmdCfg.Config, yamlToolAppender{toolCfg: toolCfg}) +} diff --git a/cmd/binny/cli/command/check.go b/cmd/binny/cli/command/check.go index c021f55..b8deba7 100644 --- a/cmd/binny/cli/command/check.go +++ b/cmd/binny/cli/command/check.go @@ -14,13 +14,13 @@ import ( ) type CheckConfig struct { - Config string `json:"config" yaml:"config" mapstructure:"config"` - option.AppConfig `json:"" yaml:",inline" mapstructure:",squash"` + Config string `json:"config" yaml:"config" mapstructure:"config"` + option.Core `json:"" yaml:",inline" mapstructure:",squash"` } func Check(app clio.Application) *cobra.Command { cfg := &CheckConfig{ - AppConfig: option.DefaultAppConfig(), + Core: option.DefaultCore(), } var names []string diff --git a/cmd/binny/cli/command/install.go b/cmd/binny/cli/command/install.go index ba57f98..1a6b22c 100644 --- a/cmd/binny/cli/command/install.go +++ b/cmd/binny/cli/command/install.go @@ -14,13 +14,13 @@ import ( ) type InstallConfig struct { - Config string `json:"config" yaml:"config" mapstructure:"config"` - option.AppConfig `json:"" yaml:",inline" mapstructure:",squash"` + Config string `json:"config" yaml:"config" mapstructure:"config"` + option.Core `json:"" yaml:",inline" mapstructure:",squash"` } func Install(app clio.Application) *cobra.Command { cfg := &InstallConfig{ - AppConfig: option.DefaultAppConfig(), + Core: option.DefaultCore(), } var names []string diff --git a/cmd/binny/cli/command/run.go b/cmd/binny/cli/command/run.go index d9e3dfe..b9c2259 100644 --- a/cmd/binny/cli/command/run.go +++ b/cmd/binny/cli/command/run.go @@ -21,13 +21,13 @@ import ( ) type RunConfig struct { - Config string `json:"config" yaml:"config" mapstructure:"config"` - option.AppConfig `json:"" yaml:",inline" mapstructure:",squash"` + Config string `json:"config" yaml:"config" mapstructure:"config"` + option.Core `json:"" yaml:",inline" mapstructure:",squash"` } func Run(app clio.Application) *cobra.Command { cfg := &RunConfig{ - AppConfig: option.DefaultAppConfig(), + Core: option.DefaultCore(), } return app.SetupCommand(&cobra.Command{ diff --git a/cmd/binny/cli/command/update_lock.go b/cmd/binny/cli/command/update_lock.go index 2f7fe4b..0dd16d1 100644 --- a/cmd/binny/cli/command/update_lock.go +++ b/cmd/binny/cli/command/update_lock.go @@ -1,20 +1,13 @@ package command import ( - "bytes" "fmt" - "io" - "os" - "runtime" - "github.com/chainguard-dev/yam/pkg/yam/formatted" - "github.com/google/yamlfmt" - "github.com/google/yamlfmt/engine" - "github.com/google/yamlfmt/formatters/basic" "github.com/scylladb/go-set/strset" "github.com/spf13/cobra" "gopkg.in/yaml.v3" + "github.com/anchore/binny/cmd/binny/cli/internal/yamlpatch" "github.com/anchore/binny/cmd/binny/cli/option" "github.com/anchore/binny/internal/log" "github.com/anchore/clio" @@ -22,13 +15,13 @@ import ( ) type UpdateConfig struct { - Config string `json:"config" yaml:"config" mapstructure:"config"` - option.AppConfig `json:"" yaml:",inline" mapstructure:",squash"` + Config string `json:"config" yaml:"config" mapstructure:"config"` + option.Core `json:"" yaml:",inline" mapstructure:",squash"` } func UpdateLock(app clio.Application) *cobra.Command { cfg := &UpdateConfig{ - AppConfig: option.DefaultAppConfig(), + Core: option.DefaultCore(), } var names []string @@ -48,15 +41,33 @@ func UpdateLock(app clio.Application) *cobra.Command { } func runUpdate(cfg UpdateConfig, names []string) error { - newCfg, err := getUpdatedConfig(cfg.AppConfig, names) + newCfg, err := getUpdatedConfig(cfg.Core, names) if err != nil { return err } + if newCfg == nil { + return fmt.Errorf("no command config found") + } + + return yamlpatch.Write(cfg.Config, updateLockYamlPatcher{cfg: *newCfg}) +} + +var _ yamlpatch.Patcher = (*updateLockYamlPatcher)(nil) - return writeBackConfig(cfg.Config, *newCfg) +type updateLockYamlPatcher struct { + cfg option.Core +} + +func (p updateLockYamlPatcher) PatchYaml(node *yaml.Node) error { + toolsNode := yamlpatch.FindToolsSequenceNode(node) + for _, toolCfg := range p.cfg.Tools { + toolVersionWantNode := yamlpatch.FindToolVersionWantNode(toolsNode, toolCfg.Name) + toolVersionWantNode.Value = toolCfg.Version.Want + } + return nil } -func getUpdatedConfig(cfg option.AppConfig, names []string) (*option.AppConfig, error) { +func getUpdatedConfig(cfg option.Core, names []string) (*option.Core, error) { nameSet := strset.New() if len(names) == 0 { nameSet.Add(cfg.Tools.Names()...) @@ -113,187 +124,3 @@ func getUpdatedConfig(cfg option.AppConfig, names []string) (*option.AppConfig, return &newCfg, nil } - -func writeBackConfig(path string, cfg option.AppConfig) error { - fh, err := os.Open(path) - if err != nil { - return err - } - - contents, err := io.ReadAll(fh) - if err != nil { - return err - } - - if err := fh.Close(); err != nil { - return err - } - - var n yaml.Node - err = yaml.Unmarshal(contents, &n) - if err != nil { - return err - } - - switch len(n.Content) { - case 0: - return fmt.Errorf("no documents found in config file") - case 1: - // continue - default: - return fmt.Errorf("multiple documents found in config file (expected 1)") - } - - // take the first document - doc := n.Content[0] - - patchYamlNode(doc, cfg) - - out, err := yaml.Marshal(doc) - if err != nil { - return err - } - - document := n.HeadComment + "\n" + string(out) + "\n" + n.FootComment + "\n" - - document, err = formatYaml(document) - if err != nil { - return err - } - - fh, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) - if err != nil { - return err - } - - defer fh.Close() - - _, err = fh.WriteString(document) - - if err != nil { - return err - } - - return nil -} - -func patchYamlNode(node *yaml.Node, cfg option.AppConfig) { - toolsNode := findToolsSequenceNode(node) - for _, toolCfg := range cfg.Tools { - toolNode := findToolNode(toolsNode, toolCfg.Name) - toolVersionNode := findToolVersionNode(toolNode) - toolVersionWantNode := findToolVersionWantNode(toolVersionNode) - toolVersionWantNode.Value = toolCfg.Version.Want - } -} - -func findToolsSequenceNode(node *yaml.Node) *yaml.Node { - for idx, v := range node.Content { - var next *yaml.Node - if idx+1 < len(node.Content) { - next = node.Content[idx+1] - } else { - break - } - if v.Value == "tools" && next.Tag == "!!seq" { - return next - } - } - return nil -} - -func findToolNode(toolSequenceNode *yaml.Node, name string) *yaml.Node { - for _, v := range toolSequenceNode.Content { - if v.Tag != "!!map" { - continue - } - var candidateName string - // each element in the sequence is a map - for idx, v2 := range v.Content { - if idx%2 == 0 && v2.Value == "name" { - candidateName = v.Content[idx+1].Value - break - } - } - - if candidateName == name { - return v - } - } - return nil -} - -func findToolVersionNode(toolNode *yaml.Node) *yaml.Node { - // each element is the k=v pair in a map - for idx, v := range toolNode.Content { - if idx%2 == 0 && v.Value == "version" { - return toolNode.Content[idx+1] - } - } - return nil -} - -func findToolVersionWantNode(toolVersionNode *yaml.Node) *yaml.Node { - // each element is the k=v pair in a map - for idx, v := range toolVersionNode.Content { - if idx%2 == 0 && v.Value == "want" { - return toolVersionNode.Content[idx+1] - } - } - return nil -} - -func formatYaml(contents string) (string, error) { - registry := yamlfmt.NewFormatterRegistry(&basic.BasicFormatterFactory{}) - - factory, err := registry.GetDefaultFactory() - if err != nil { - return "", fmt.Errorf("unable to get default YAML formatter factory: %w", err) - } - - formatter, err := factory.NewFormatter(nil) - if err != nil { - return "", fmt.Errorf("unable to create YAML formatter: %w", err) - } - - breakStyle := yamlfmt.LineBreakStyleLF - if runtime.GOOS == "windows" { - breakStyle = yamlfmt.LineBreakStyleCRLF - } - - lineSepChar, err := breakStyle.Separator() - if err != nil { - return "", err - } - - eng := &engine.ConsecutiveEngine{ - LineSepCharacter: lineSepChar, - Formatter: formatter, - Quiet: true, - ContinueOnError: false, - } - - out, err := eng.FormatContent([]byte(contents)) - if err != nil { - return "", fmt.Errorf("unable to format YAML: %w", err) - } - - var node yaml.Node - if err = yaml.Unmarshal(out, &node); err != nil { - return "", fmt.Errorf("unable to unmarshal formatted YAML: %w", err) - } - - var buf bytes.Buffer - enc := formatted.NewEncoder(&buf) - enc, err = enc.SetGapExpressions(".tools") - if err != nil { - return "", fmt.Errorf("unable to set gap expressions: %w", err) - } - - err = enc.Encode(&node) - if err != nil { - return "", fmt.Errorf("unable to format YAML: %w", err) - } - - return buf.String(), nil -} diff --git a/cmd/binny/cli/command/utils.go b/cmd/binny/cli/command/utils.go new file mode 100644 index 0000000..3f88bd7 --- /dev/null +++ b/cmd/binny/cli/command/utils.go @@ -0,0 +1,57 @@ +package command + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" + "gopkg.in/yaml.v3" + + "github.com/anchore/binny/cmd/binny/cli/internal/yamlpatch" + "github.com/anchore/binny/cmd/binny/cli/option" +) + +func toMap(s any) (map[string]any, error) { + var m map[string]any + err := mapstructure.Decode(s, &m) + if err != nil { + return nil, fmt.Errorf("unable to create map from struct: %w", err) + } + + for k, v := range m { + switch vv := v.(type) { + case string: + if vv == "" { + delete(m, k) + } + case []string: + if len(vv) == 0 { + delete(m, k) + } + default: + if vv == nil { + delete(m, k) + } + } + } + + return m, nil +} + +var _ yamlpatch.Patcher = (*yamlToolAppender)(nil) + +type yamlToolAppender struct { + toolCfg option.Tool +} + +func (p yamlToolAppender) PatchYaml(node *yaml.Node) error { + patchNode, err := yamlpatch.GetYamlNode(p.toolCfg) + if err != nil { + return fmt.Errorf("unable to create new tool yaml config: %w", err) + } + + toolsNode := yamlpatch.FindToolsSequenceNode(node) + + toolsNode.Content = append(toolsNode.Content, patchNode.Content[0]) + + return nil +} diff --git a/cmd/binny/cli/internal/yamlpatch/helpers.go b/cmd/binny/cli/internal/yamlpatch/helpers.go new file mode 100644 index 0000000..bc4c77d --- /dev/null +++ b/cmd/binny/cli/internal/yamlpatch/helpers.go @@ -0,0 +1,83 @@ +package yamlpatch + +import ( + "gopkg.in/yaml.v3" +) + +func FindToolVersionWantNode(toolsNode *yaml.Node, toolName string) *yaml.Node { + toolNode := FindToolNode(toolsNode, toolName) + toolVersionNode := findToolVersionNode(toolNode) + toolVersionWantNode := findToolVersionWantNode(toolVersionNode) + return toolVersionWantNode +} + +func FindToolsSequenceNode(node *yaml.Node) *yaml.Node { + for idx, v := range node.Content { + var next *yaml.Node + if idx+1 < len(node.Content) { + next = node.Content[idx+1] + } else { + break + } + if v.Value == "tools" && next.Tag == "!!seq" { + return next + } + } + return nil +} + +func FindToolNode(toolSequenceNode *yaml.Node, name string) *yaml.Node { + for _, v := range toolSequenceNode.Content { + if v.Tag != "!!map" { + continue + } + var candidateName string + // each element in the sequence is a map + for idx, v2 := range v.Content { + if idx%2 == 0 && v2.Value == "name" { + candidateName = v.Content[idx+1].Value + break + } + } + + if candidateName == name { + return v + } + } + return nil +} + +func findToolVersionNode(toolNode *yaml.Node) *yaml.Node { + // each element is the k=v pair in a map + for idx, v := range toolNode.Content { + if idx%2 == 0 && v.Value == "version" { + return toolNode.Content[idx+1] + } + } + return nil +} + +func findToolVersionWantNode(toolVersionNode *yaml.Node) *yaml.Node { + // each element is the k=v pair in a map + for idx, v := range toolVersionNode.Content { + if idx%2 == 0 && v.Value == "want" { + return toolVersionNode.Content[idx+1] + } + } + return nil +} + +func GetYamlNode(s any) (*yaml.Node, error) { + var n yaml.Node + + by, err := yaml.Marshal(s) + if err != nil { + return nil, err + } + + err = yaml.Unmarshal(by, &n) + if err != nil { + return nil, err + } + return &n, nil +} diff --git a/cmd/binny/cli/internal/yamlpatch/patcher.go b/cmd/binny/cli/internal/yamlpatch/patcher.go new file mode 100644 index 0000000..f449379 --- /dev/null +++ b/cmd/binny/cli/internal/yamlpatch/patcher.go @@ -0,0 +1,139 @@ +package yamlpatch + +import ( + "bytes" + "fmt" + "io" + "os" + "runtime" + + "github.com/chainguard-dev/yam/pkg/yam/formatted" + "github.com/google/yamlfmt" + "github.com/google/yamlfmt/engine" + "github.com/google/yamlfmt/formatters/basic" + "gopkg.in/yaml.v3" +) + +type Patcher interface { + PatchYaml(node *yaml.Node) error +} + +func Write(path string, patcher Patcher) error { + fh, err := os.Open(path) + if err != nil { + return err + } + + contents, err := io.ReadAll(fh) + if err != nil { + return err + } + + if err := fh.Close(); err != nil { + return err + } + + var n yaml.Node + err = yaml.Unmarshal(contents, &n) + if err != nil { + return err + } + + switch len(n.Content) { + case 0: + return fmt.Errorf("no documents found in config file") + case 1: + // continue + default: + return fmt.Errorf("multiple documents found in config file (expected 1)") + } + + // take the first document + doc := n.Content[0] + + if err := patcher.PatchYaml(doc); err != nil { + return fmt.Errorf("unabl to patch yaml: %w", err) + } + + out, err := yaml.Marshal(doc) + if err != nil { + return err + } + + document := n.HeadComment + "\n" + string(out) + "\n" + n.FootComment + "\n" + + document, err = formatYaml(document) + if err != nil { + return err + } + + fh, err = os.OpenFile(path, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0644) + if err != nil { + return err + } + + defer fh.Close() + + _, err = fh.WriteString(document) + + if err != nil { + return err + } + + return nil +} + +func formatYaml(contents string) (string, error) { + registry := yamlfmt.NewFormatterRegistry(&basic.BasicFormatterFactory{}) + + factory, err := registry.GetDefaultFactory() + if err != nil { + return "", fmt.Errorf("unable to get default YAML formatter factory: %w", err) + } + + formatter, err := factory.NewFormatter(nil) + if err != nil { + return "", fmt.Errorf("unable to create YAML formatter: %w", err) + } + + breakStyle := yamlfmt.LineBreakStyleLF + if runtime.GOOS == "windows" { + breakStyle = yamlfmt.LineBreakStyleCRLF + } + + lineSepChar, err := breakStyle.Separator() + if err != nil { + return "", err + } + + eng := &engine.ConsecutiveEngine{ + LineSepCharacter: lineSepChar, + Formatter: formatter, + Quiet: true, + ContinueOnError: false, + } + + out, err := eng.FormatContent([]byte(contents)) + if err != nil { + return "", fmt.Errorf("unable to format YAML: %w", err) + } + + var node yaml.Node + if err = yaml.Unmarshal(out, &node); err != nil { + return "", fmt.Errorf("unable to unmarshal formatted YAML: %w", err) + } + + var buf bytes.Buffer + enc := formatted.NewEncoder(&buf) + enc, err = enc.SetGapExpressions(".tools") + if err != nil { + return "", fmt.Errorf("unable to set gap expressions: %w", err) + } + + err = enc.Encode(&node) + if err != nil { + return "", fmt.Errorf("unable to format YAML: %w", err) + } + + return buf.String(), nil +} diff --git a/cmd/binny/cli/option/app_config.go b/cmd/binny/cli/option/core.go similarity index 63% rename from cmd/binny/cli/option/app_config.go rename to cmd/binny/cli/option/core.go index e21b7d0..a1ba5c1 100644 --- a/cmd/binny/cli/option/app_config.go +++ b/cmd/binny/cli/option/core.go @@ -1,13 +1,14 @@ package option -type AppConfig struct { +// Core options make up the static application configuration on disk. +type Core struct { Store `json:"" yaml:",inline" mapstructure:",squash"` - Tools Tools `json:"tools" yaml:"tools" mapstructure:"tools"` Check `json:"" yaml:",inline" mapstructure:",squash"` + Tools Tools `json:"tools" yaml:"tools" mapstructure:"tools"` } -func DefaultAppConfig() AppConfig { - return AppConfig{ +func DefaultCore() Core { + return Core{ Store: DefaultStore(), } } diff --git a/cmd/binny/cli/option/go_install.go b/cmd/binny/cli/option/go_install.go new file mode 100644 index 0000000..025b0fd --- /dev/null +++ b/cmd/binny/cli/option/go_install.go @@ -0,0 +1,15 @@ +package option + +import "github.com/anchore/clio" + +type GoInstall struct { + Module string `json:"module" yaml:"module" mapstructure:"module"` + Entrypoint string `json:"entrypoint" yaml:"entrypoint" mapstructure:"entrypoint"` + LDFlags string `json:"ld-flags" yaml:"ld-flags" mapstructure:"ld-flags"` +} + +func (o *GoInstall) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.Module, "module", "m", "Go module (e.g. github.com/anchore/syft)") + flags.StringVarP(&o.Entrypoint, "entrypoint", "e", "Entrypoint within the go module (e.g. cmd/syft)") + flags.StringVarP(&o.LDFlags, "ld-flags", "l", "LD flags to pass to the go install command (e.g. -ldflags \"-X main.version=1.0.0\")") +} diff --git a/cmd/binny/cli/option/version_resolution.go b/cmd/binny/cli/option/version_resolution.go new file mode 100644 index 0000000..1a6df21 --- /dev/null +++ b/cmd/binny/cli/option/version_resolution.go @@ -0,0 +1,18 @@ +package option + +import ( + "fmt" + + "github.com/anchore/binny/tool" + "github.com/anchore/clio" +) + +type VersionResolution struct { + Constraint string `json:"constraint" yaml:"constraint" mapstructure:"constraint"` + Method string `json:"method" yaml:"method" mapstructure:"method"` +} + +func (o *VersionResolution) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.Constraint, "constraint", "", "Version constraint (e.g. '<2.0' or '>=1.0.0')") + flags.StringVarP(&o.Method, "version-from", "f", fmt.Sprintf("The method to use to resolve the version (available: %+v)", tool.VersionResolverMethods())) +} diff --git a/store_test.go b/store_test.go index b407033..739c5c3 100644 --- a/store_test.go +++ b/store_test.go @@ -29,59 +29,65 @@ func TestStore_GetByName(t *testing.T) { }, { name: "empty request", - storeRoot: "testdata/store/valid", + storeRoot: "testdata/store/valid-sha256-only", toolName: "", }, { name: "hit by name only", - storeRoot: "testdata/store/valid", + storeRoot: "testdata/store/valid-sha256-only", toolName: "golangci-lint", want: []StoreEntry{ { Name: "golangci-lint", InstalledVersion: "v1.54.2", - Digests: "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", - PathInRoot: "golangci-lint", + Digests: map[string]string{ + "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", + }, + PathInRoot: "golangci-lint", }, }, }, { name: "hit by name and exact version", - storeRoot: "testdata/store/valid", + storeRoot: "testdata/store/valid-sha256-only", toolName: "golangci-lint", versions: []string{"v1.54.2"}, want: []StoreEntry{ { Name: "golangci-lint", InstalledVersion: "v1.54.2", - Digests: "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", - PathInRoot: "golangci-lint", + Digests: map[string]string{ + "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", + }, + PathInRoot: "golangci-lint", }, }, }, { name: "hit by name and multiple versions", - storeRoot: "testdata/store/valid", + storeRoot: "testdata/store/valid-sha256-only", toolName: "golangci-lint", versions: []string{"v1.54.1", "v1.54.2", "v1.54.3"}, want: []StoreEntry{ { Name: "golangci-lint", InstalledVersion: "v1.54.2", - Digests: "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", - PathInRoot: "golangci-lint", + Digests: map[string]string{ + "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", + }, + PathInRoot: "golangci-lint", }, }, }, { name: "miss by bad version", - storeRoot: "testdata/store/valid", + storeRoot: "testdata/store/valid-sha256-only", toolName: "golangci-lint", versions: []string{"v1.54.3"}, }, { name: "miss", - storeRoot: "testdata/store/valid", + storeRoot: "testdata/store/valid-sha256-only", toolName: "best-tool", // bogus }, } @@ -112,64 +118,80 @@ func TestStore_Entries(t *testing.T) { storeRoot: "testdata/store/missing", }, { - name: "valid store", - storeRoot: "testdata/store/valid", + name: "valid store (sha256 only)", + storeRoot: "testdata/store/valid-sha256-only", want: []StoreEntry{ { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "quill", InstalledVersion: "v0.4.1", - Digests: "56656877b8b0e0c06a96e83df12157565b91bb8f6b55c4051c0466edf0f08b85", - PathInRoot: "quill", + Digests: map[string]string{ + "sha256": "56656877b8b0e0c06a96e83df12157565b91bb8f6b55c4051c0466edf0f08b85", + }, + PathInRoot: "quill", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "chronicle", InstalledVersion: "v0.7.0", - Digests: "e011590e5d55188e03a2fd58524853ddacd23ec2e5d58535e061339777c4043f", - PathInRoot: "chronicle", + Digests: map[string]string{ + "sha256": "e011590e5d55188e03a2fd58524853ddacd23ec2e5d58535e061339777c4043f", + }, + PathInRoot: "chronicle", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "gosimports", InstalledVersion: "v0.3.8", - Digests: "9e5837236320efadb7a94675866cbd95e7a9716d635f3863603859698a37591a", - PathInRoot: "gosimports", + Digests: map[string]string{ + "sha256": "9e5837236320efadb7a94675866cbd95e7a9716d635f3863603859698a37591a", + }, + PathInRoot: "gosimports", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "glow", InstalledVersion: "v1.5.1", - Digests: "c6f05b9383f97fbb6fb2bb84b87b3b99ed7a1708d8a1634ff66d5bff8180f3b0", - PathInRoot: "glow", + Digests: map[string]string{ + "sha256": "c6f05b9383f97fbb6fb2bb84b87b3b99ed7a1708d8a1634ff66d5bff8180f3b0", + }, + PathInRoot: "glow", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "goreleaser", InstalledVersion: "v1.20.0", - Digests: "307dd15253ab292a57dff221671659f3133593df485cc08fdd8158d63222bb16", - PathInRoot: "goreleaser", + Digests: map[string]string{ + "sha256": "307dd15253ab292a57dff221671659f3133593df485cc08fdd8158d63222bb16", + }, + PathInRoot: "goreleaser", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "golangci-lint", InstalledVersion: "v1.54.2", - Digests: "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", - PathInRoot: "golangci-lint", + Digests: map[string]string{ + "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", + }, + PathInRoot: "golangci-lint", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "bouncer", InstalledVersion: "v0.4.0", - Digests: "de42a2453c8e9b2587358c1f244a5cc0091c71385126f0fa3c0b3aec0feeaa4d", - PathInRoot: "bouncer", + Digests: map[string]string{ + "sha256": "de42a2453c8e9b2587358c1f244a5cc0091c71385126f0fa3c0b3aec0feeaa4d", + }, + PathInRoot: "bouncer", }, { - root: "testdata/store/valid", + root: "testdata/store/valid-sha256-only", Name: "task", InstalledVersion: "v3.29.1", - Digests: "8d92c81f07960c5363a1f424e88dd4b64a1dd4251378d53873fa65ea1aab271b", - PathInRoot: "task", + Digests: map[string]string{ + "sha256": "8d92c81f07960c5363a1f424e88dd4b64a1dd4251378d53873fa65ea1aab271b", + }, + PathInRoot: "task", }, }, }, @@ -264,7 +286,7 @@ func TestStore_AddTool(t *testing.T) { } func TestStore_Entries_IsACopy(t *testing.T) { - store, err := NewStore("testdata/store/valid") + store, err := NewStore("testdata/store/valid-sha256-only") require.NoError(t, err) gotEntries := store.Entries() diff --git a/testdata/store/valid-sha256-only/.binny.state.json b/testdata/store/valid-sha256-only/.binny.state.json new file mode 100644 index 0000000..4174f55 --- /dev/null +++ b/testdata/store/valid-sha256-only/.binny.state.json @@ -0,0 +1,68 @@ +{ + "entries": [ + { + "name": "quill", + "version": "v0.4.1", + "digests": { + "sha256": "56656877b8b0e0c06a96e83df12157565b91bb8f6b55c4051c0466edf0f08b85" + }, + "path": "quill" + }, + { + "name": "chronicle", + "version": "v0.7.0", + "digests": { + "sha256": "e011590e5d55188e03a2fd58524853ddacd23ec2e5d58535e061339777c4043f" + }, + "path": "chronicle" + }, + { + "name": "gosimports", + "version": "v0.3.8", + "digests": { + "sha256": "9e5837236320efadb7a94675866cbd95e7a9716d635f3863603859698a37591a" + }, + "path": "gosimports" + }, + { + "name": "glow", + "version": "v1.5.1", + "digests": { + "sha256": "c6f05b9383f97fbb6fb2bb84b87b3b99ed7a1708d8a1634ff66d5bff8180f3b0" + }, + "path": "glow" + }, + { + "name": "goreleaser", + "version": "v1.20.0", + "digests": { + "sha256": "307dd15253ab292a57dff221671659f3133593df485cc08fdd8158d63222bb16" + }, + "path": "goreleaser" + }, + { + "name": "golangci-lint", + "version": "v1.54.2", + "digests": { + "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d" + }, + "path": "golangci-lint" + }, + { + "name": "bouncer", + "version": "v0.4.0", + "digests": { + "sha256": "de42a2453c8e9b2587358c1f244a5cc0091c71385126f0fa3c0b3aec0feeaa4d" + }, + "path": "bouncer" + }, + { + "name": "task", + "version": "v3.29.1", + "digests": { + "sha256": "8d92c81f07960c5363a1f424e88dd4b64a1dd4251378d53873fa65ea1aab271b" + }, + "path": "task" + } + ] +} diff --git a/testdata/store/valid/bouncer b/testdata/store/valid-sha256-only/bouncer similarity index 100% rename from testdata/store/valid/bouncer rename to testdata/store/valid-sha256-only/bouncer diff --git a/testdata/store/valid/chronicle b/testdata/store/valid-sha256-only/chronicle similarity index 100% rename from testdata/store/valid/chronicle rename to testdata/store/valid-sha256-only/chronicle diff --git a/testdata/store/valid/glow b/testdata/store/valid-sha256-only/glow similarity index 100% rename from testdata/store/valid/glow rename to testdata/store/valid-sha256-only/glow diff --git a/testdata/store/valid/golangci-lint b/testdata/store/valid-sha256-only/golangci-lint similarity index 100% rename from testdata/store/valid/golangci-lint rename to testdata/store/valid-sha256-only/golangci-lint diff --git a/testdata/store/valid/goreleaser b/testdata/store/valid-sha256-only/goreleaser similarity index 100% rename from testdata/store/valid/goreleaser rename to testdata/store/valid-sha256-only/goreleaser diff --git a/testdata/store/valid/gosimports b/testdata/store/valid-sha256-only/gosimports similarity index 100% rename from testdata/store/valid/gosimports rename to testdata/store/valid-sha256-only/gosimports diff --git a/testdata/store/valid/quill b/testdata/store/valid-sha256-only/quill similarity index 100% rename from testdata/store/valid/quill rename to testdata/store/valid-sha256-only/quill diff --git a/testdata/store/valid/task b/testdata/store/valid-sha256-only/task similarity index 100% rename from testdata/store/valid/task rename to testdata/store/valid-sha256-only/task diff --git a/testdata/store/valid/.binny.state.json b/testdata/store/valid/.binny.state.json deleted file mode 100644 index 2f7619d..0000000 --- a/testdata/store/valid/.binny.state.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "entries": [ - { - "name": "quill", - "version": "v0.4.1", - "sha256": "56656877b8b0e0c06a96e83df12157565b91bb8f6b55c4051c0466edf0f08b85", - "path": "quill" - }, - { - "name": "chronicle", - "version": "v0.7.0", - "sha256": "e011590e5d55188e03a2fd58524853ddacd23ec2e5d58535e061339777c4043f", - "path": "chronicle" - }, - { - "name": "gosimports", - "version": "v0.3.8", - "sha256": "9e5837236320efadb7a94675866cbd95e7a9716d635f3863603859698a37591a", - "path": "gosimports" - }, - { - "name": "glow", - "version": "v1.5.1", - "sha256": "c6f05b9383f97fbb6fb2bb84b87b3b99ed7a1708d8a1634ff66d5bff8180f3b0", - "path": "glow" - }, - { - "name": "goreleaser", - "version": "v1.20.0", - "sha256": "307dd15253ab292a57dff221671659f3133593df485cc08fdd8158d63222bb16", - "path": "goreleaser" - }, - { - "name": "golangci-lint", - "version": "v1.54.2", - "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", - "path": "golangci-lint" - }, - { - "name": "bouncer", - "version": "v0.4.0", - "sha256": "de42a2453c8e9b2587358c1f244a5cc0091c71385126f0fa3c0b3aec0feeaa4d", - "path": "bouncer" - }, - { - "name": "task", - "version": "v3.29.1", - "sha256": "8d92c81f07960c5363a1f424e88dd4b64a1dd4251378d53873fa65ea1aab271b", - "path": "task" - } - ] -} diff --git a/tool/check_test.go b/tool/check_test.go index 1a2ad51..04d2973 100644 --- a/tool/check_test.go +++ b/tool/check_test.go @@ -8,7 +8,7 @@ import ( "github.com/anchore/binny" ) -func Test_check(t *testing.T) { +func Test_check_sha256(t *testing.T) { tests := []struct { name string storeRoot string @@ -18,8 +18,8 @@ func Test_check(t *testing.T) { wantErr require.ErrorAssertionFunc }{ { - name: "valid", - storeRoot: "testdata/store/valid", + name: "valid (sha256)", + storeRoot: "testdata/store/valid-sha256-only", resolvedVersion: "v1.54.2", verifyDigest: true, toolName: "golangci-lint", @@ -57,7 +57,10 @@ func Test_check(t *testing.T) { store, err := binny.NewStore(tt.storeRoot) require.NoError(t, err) - tt.wantErr(t, Check(tt.toolName, tt.resolvedVersion, store, tt.verifyDigest)) + tt.wantErr(t, Check(tt.toolName, tt.resolvedVersion, store, VerifyConfig{ + VerifyXXH64Digest: false, + VerifySHA256Digest: tt.verifyDigest, + })) }) } } diff --git a/tool/config.go b/tool/config.go index e2a4e1a..1fcd09e 100644 --- a/tool/config.go +++ b/tool/config.go @@ -128,19 +128,6 @@ func getResolver(method string, params any) (resolver binny.VersionResolver, err return resolver, nil } -func defaultVersionResolverConfig(installMethod string, installParams any) (method string, parameters any, err error) { - switch { - case goinstall.IsInstallMethod(installMethod): - return goinstall.DefaultVersionResolverConfig(installParams) - case hostedshell.IsInstallMethod(installMethod): - return hostedshell.DefaultVersionResolverConfig(installParams) - case githubrelease.IsInstallMethod(installMethod): - return githubrelease.DefaultVersionResolverConfig(installParams) - } - - return "", nil, nil -} - func (c compositeTool) Name() string { return c.config.Name } @@ -156,3 +143,16 @@ func (c compositeTool) ID() string { return fmt.Sprintf("%016x", f) } + +func defaultVersionResolverConfig(installMethod string, installParams any) (method string, parameters any, err error) { + switch { + case goinstall.IsInstallMethod(installMethod): + return goinstall.DefaultVersionResolverConfig(installParams) + case hostedshell.IsInstallMethod(installMethod): + return hostedshell.DefaultVersionResolverConfig(installParams) + case githubrelease.IsInstallMethod(installMethod): + return githubrelease.DefaultVersionResolverConfig(installParams) + } + + return "", nil, nil +} diff --git a/tool/resolve_version.go b/tool/resolve_version.go index 0afa8fa..f9056a6 100644 --- a/tool/resolve_version.go +++ b/tool/resolve_version.go @@ -6,8 +6,19 @@ import ( "github.com/Masterminds/semver/v3" "github.com/anchore/binny" + "github.com/anchore/binny/tool/git" + "github.com/anchore/binny/tool/githubrelease" + "github.com/anchore/binny/tool/goproxy" ) +func VersionResolverMethods() []string { + return []string{ + githubrelease.ResolveMethod, + goproxy.ResolveMethod, + git.ResolveMethod, + } +} + func ResolveVersion(tool binny.VersionResolver, intent binny.VersionIntent) (string, error) { want := intent.Want constraint := intent.Constraint diff --git a/tool/testdata/store/valid-sha256-only/.binny.state.json b/tool/testdata/store/valid-sha256-only/.binny.state.json new file mode 100644 index 0000000..4174f55 --- /dev/null +++ b/tool/testdata/store/valid-sha256-only/.binny.state.json @@ -0,0 +1,68 @@ +{ + "entries": [ + { + "name": "quill", + "version": "v0.4.1", + "digests": { + "sha256": "56656877b8b0e0c06a96e83df12157565b91bb8f6b55c4051c0466edf0f08b85" + }, + "path": "quill" + }, + { + "name": "chronicle", + "version": "v0.7.0", + "digests": { + "sha256": "e011590e5d55188e03a2fd58524853ddacd23ec2e5d58535e061339777c4043f" + }, + "path": "chronicle" + }, + { + "name": "gosimports", + "version": "v0.3.8", + "digests": { + "sha256": "9e5837236320efadb7a94675866cbd95e7a9716d635f3863603859698a37591a" + }, + "path": "gosimports" + }, + { + "name": "glow", + "version": "v1.5.1", + "digests": { + "sha256": "c6f05b9383f97fbb6fb2bb84b87b3b99ed7a1708d8a1634ff66d5bff8180f3b0" + }, + "path": "glow" + }, + { + "name": "goreleaser", + "version": "v1.20.0", + "digests": { + "sha256": "307dd15253ab292a57dff221671659f3133593df485cc08fdd8158d63222bb16" + }, + "path": "goreleaser" + }, + { + "name": "golangci-lint", + "version": "v1.54.2", + "digests": { + "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d" + }, + "path": "golangci-lint" + }, + { + "name": "bouncer", + "version": "v0.4.0", + "digests": { + "sha256": "de42a2453c8e9b2587358c1f244a5cc0091c71385126f0fa3c0b3aec0feeaa4d" + }, + "path": "bouncer" + }, + { + "name": "task", + "version": "v3.29.1", + "digests": { + "sha256": "8d92c81f07960c5363a1f424e88dd4b64a1dd4251378d53873fa65ea1aab271b" + }, + "path": "task" + } + ] +} diff --git a/tool/testdata/store/valid/bouncer b/tool/testdata/store/valid-sha256-only/bouncer similarity index 100% rename from tool/testdata/store/valid/bouncer rename to tool/testdata/store/valid-sha256-only/bouncer diff --git a/tool/testdata/store/valid/chronicle b/tool/testdata/store/valid-sha256-only/chronicle similarity index 100% rename from tool/testdata/store/valid/chronicle rename to tool/testdata/store/valid-sha256-only/chronicle diff --git a/tool/testdata/store/valid/glow b/tool/testdata/store/valid-sha256-only/glow similarity index 100% rename from tool/testdata/store/valid/glow rename to tool/testdata/store/valid-sha256-only/glow diff --git a/tool/testdata/store/valid/golangci-lint b/tool/testdata/store/valid-sha256-only/golangci-lint similarity index 100% rename from tool/testdata/store/valid/golangci-lint rename to tool/testdata/store/valid-sha256-only/golangci-lint diff --git a/tool/testdata/store/valid/goreleaser b/tool/testdata/store/valid-sha256-only/goreleaser similarity index 100% rename from tool/testdata/store/valid/goreleaser rename to tool/testdata/store/valid-sha256-only/goreleaser diff --git a/tool/testdata/store/valid/gosimports b/tool/testdata/store/valid-sha256-only/gosimports similarity index 100% rename from tool/testdata/store/valid/gosimports rename to tool/testdata/store/valid-sha256-only/gosimports diff --git a/tool/testdata/store/valid/quill b/tool/testdata/store/valid-sha256-only/quill similarity index 100% rename from tool/testdata/store/valid/quill rename to tool/testdata/store/valid-sha256-only/quill diff --git a/tool/testdata/store/valid/task b/tool/testdata/store/valid-sha256-only/task similarity index 100% rename from tool/testdata/store/valid/task rename to tool/testdata/store/valid-sha256-only/task diff --git a/tool/testdata/store/valid/.binny.state.json b/tool/testdata/store/valid/.binny.state.json deleted file mode 100644 index 2f7619d..0000000 --- a/tool/testdata/store/valid/.binny.state.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "entries": [ - { - "name": "quill", - "version": "v0.4.1", - "sha256": "56656877b8b0e0c06a96e83df12157565b91bb8f6b55c4051c0466edf0f08b85", - "path": "quill" - }, - { - "name": "chronicle", - "version": "v0.7.0", - "sha256": "e011590e5d55188e03a2fd58524853ddacd23ec2e5d58535e061339777c4043f", - "path": "chronicle" - }, - { - "name": "gosimports", - "version": "v0.3.8", - "sha256": "9e5837236320efadb7a94675866cbd95e7a9716d635f3863603859698a37591a", - "path": "gosimports" - }, - { - "name": "glow", - "version": "v1.5.1", - "sha256": "c6f05b9383f97fbb6fb2bb84b87b3b99ed7a1708d8a1634ff66d5bff8180f3b0", - "path": "glow" - }, - { - "name": "goreleaser", - "version": "v1.20.0", - "sha256": "307dd15253ab292a57dff221671659f3133593df485cc08fdd8158d63222bb16", - "path": "goreleaser" - }, - { - "name": "golangci-lint", - "version": "v1.54.2", - "sha256": "06c3715b43f4e92d0e9ec98ba8aa0f0c08c8963b2862ec130ec8e1c1ad9e1d1d", - "path": "golangci-lint" - }, - { - "name": "bouncer", - "version": "v0.4.0", - "sha256": "de42a2453c8e9b2587358c1f244a5cc0091c71385126f0fa3c0b3aec0feeaa4d", - "path": "bouncer" - }, - { - "name": "task", - "version": "v3.29.1", - "sha256": "8d92c81f07960c5363a1f424e88dd4b64a1dd4251378d53873fa65ea1aab271b", - "path": "task" - } - ] -}