From 7cb0554fc765d078f03f499ba91a5d884a780626 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 20 Sep 2023 17:34:42 -0400 Subject: [PATCH] fix git version resolver and add docs Signed-off-by: Alex Goodman --- .binny.yaml | 2 +- README.md | 200 ++++++++++++++++++++++++--- Taskfile.yaml | 2 +- cmd/binny/cli/command/update_lock.go | 4 +- tool/git/version_resolver.go | 23 ++- tool/resolve_version.go | 2 +- 6 files changed, 201 insertions(+), 32 deletions(-) diff --git a/.binny.yaml b/.binny.yaml index 8b2cb5e..d1388f7 100644 --- a/.binny.yaml +++ b/.binny.yaml @@ -1,7 +1,7 @@ tools: - name: binny version: - # since the module is ., "current" means whatever is checked out + # since the module is . then "current" means whatever is checked out want: current method: go-install with: diff --git a/README.md b/README.md index cc84d50..67fb937 100644 --- a/README.md +++ b/README.md @@ -17,32 +17,190 @@ curl -sSfL https://raw.githubusercontent.com/anchore/binny/main/install.sh | sh ## Usage Keep a configuration in your repo with the binaries you want to manage. For example: + ```yaml # .binny.yaml -- name: gh - version: - want: v2.33.0 - method: github-release - with: - repo: cli/cli +tools: + - name: gh + version: + want: v2.33.0 + method: github-release + with: + repo: cli/cli + + - name: quill + version: + want: v0.4.1 + method: github-release + with: + repo: anchore/quill + + - name: chronicle + version: + want: v0.7.0 + method: github-release + with: + repo: anchore/chronicle +``` -- name: quill - version: - want: v0.4.1 - method: github-release - with: - repo: anchore/quill +Then you can run: + - `binny install` to install all tools in the configuration + - `binny install ` to install a specific tool + - `binny check` to verify all configured tools are installed + - `binny update-lock` to update any pinned versions in the configuration with the latest available versions + +You can add tools to the configuration one of two ways: + - manually, by adding a new entry to the configuration file (see the [Configuration](#configuration) section below) + - with the `binny add ` commands, which will handle the configuration for you -- name: chronicle - version: - want: v0.7.0 + +## Configuration + +The configuration file is a YAML file with a list of tools to manage. Each tool has a name, a version, and +a method for installing it. You can optionally specify a specific method for checking the latest version of +the tool, however, this is not necessary as all install methods have a default version resolver. + + +### Tool Configuration + +Each tool has the following configuration options: + +```yaml +name: chronicle +version: + want: v0.7.0 + constraint: <= v0.9.0 method: github-release with: - repo: anchore/chronicle + # arbitrary key-value pairs for the version resolver method +with: + # arbitrary key-value pairs for the install method ``` -Then you can run: - - `benny install` to install all binaries in the configuration - - `benny install ` to install a specific binary - - `benny check` to verify all configured binaries are installed - - `benny update-lock` to update any pinned versions in the configuration with the latest available versions +| Option | Description | +|----------------|--------------------------------------------------------------------------------------------------------------------------------------------------------| +| `name` | The name of the tool to install. This is used to determine the installation directory and the name of the binary. | +| `version.want` | The version of the tool to install. This can be a specific version, or a version range. | +| `version.constraint` | A constraint on the version of the tool to install. This is used to determine the latest version of the tool to update to. | +| `version.method` | The method to use to determine the latest version of the tool. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | +| `version.with` | The configuration options for the version method. See the [Version Resolver Methods](#version-resolver-methods) section for more details. | +| `method` | The method to use to install the tool. See the [Install Methods](#install-methods) section for more details. | +| `with` | The configuration options for the install method. See the [Install Methods](#install-methods) section for more details. | + + +### Install Methods + +Install methods specify where the tool binary should be pulled or built from. + + +#### `github-release` + +The `github-release` install method uses the GitHub Releases API to download the latest release of a tool. It requires the following configuration options: + +| Option | Description | +|--------|-------------------------------------------------------------------------------------------------| +| `repo` | The GitHub repository to reference releases from. This should be in the format `/` | + +The default version resolver for this method is `github-release`. + + +#### `go-install` + +The `go-install` install method uses `go install` to install a tool. It requires the following configuration options: + +| Option | Description | +|-------------------------|-----------------------------------------------------------------------------| +| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | +| `entrypoint` (optional) | The path within the repo to the main package for the tool (e.g. `cmd/syft`) | +| `ldflags` (optional) | A list of ldflags to pass to `go install` (e.g. `-X main.version={{ .Version }}`) | + +The `module` option allows for a special entry: +- `.` or `path/to/module/on/disk` + +The `ldflags` allow for templating with the following variables: + +| Variable | Description | +|--------|--------------------------------------------------------------------------------------------| +| `{{ .Version }}` | The resolved version of the tool (which may differe from that of the `version.want` value) | + +In addition to these variables, [sprig functions](http://masterminds.github.io/sprig/) are allowed; for example: +```yaml +ldflags: +- -X main.buildDate={{ now | date "2006-01-02T15:04:05Z07:00" }} +``` + +For more information about sprig functions, see the [sprig documentation](http://masterminds.github.io/sprig/). + +The default version resolver for this method is `go-proxy`. + + +#### `hosted-shell` + +The `hosted-shell` install method uses a hosted shell script to install a tool. It requires the following configuration options: + +| Option | Description | +|--------|------------------------------------------------------------| +| `url` | The URL to the hosted shell script (e.g. `https://raw.githubusercontent.com/anchore/syft/main/install.sh`) | +| `args` (optional) | Arguments to pass to the shell script (as a single string) | + +If the URL refers to either `github.com` or `raw.githubusercontent.com` then the default version resolver is `github-release`. +Otherwise, the version resolver must be specified manually. + + + +### Version Methods + +The version method specifies how to determine the latest version for a tool. + +#### `git` + +The `git` version method will use a git repo on disk as a source for resolving versions via tags. It requires the following configuration options: + +| Option | Description | +|--------|----------------------------------------| +| `path` | The path to the git repository on disk | + +The `version.want` option allows a special entry: +- `current`: use the current commit checked out in the repo + +**note**: this method is still under development. Currently it is most useful for tools that are being used where that are developed: + +```yaml + - name: binny + version: + # since the module is . then "current" means whatever is checked out + want: current + method: go-install + with: + # aka: github.com/anchore/binny, without going through github / goproxy (stay local) + 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" }} +``` + +#### `github-release` + +The `github-release` version method uses the GitHub Releases API to determine the latest release of a tool. It requires the following configuration options: + +| Option | Description | +|--------|-------------------------------------------------------------------------------------------------| +| `repo` | The GitHub repository to reference releases from. This should be in the format `/` | + +The `version.want` option allows a special entry: +- `latest`: don't pin to a version, use the latest available + +#### `go-proxy` + +The `go-proxy` version method reaches out to `proxy.golang.org` to determine the latest version of a Go module. It requires the following configuration options: + +| Option | Description | +|--------|--------------------------------------------------------------------------------------------------| +| `module` | The FQDN to the Go module (e.g. `github.com/anchore/syft`) | + +The `version.want` option allows a special entry: +- `latest`: don't pin to a version, use the latest available diff --git a/Taskfile.yaml b/Taskfile.yaml index 7416be0..3111142 100644 --- a/Taskfile.yaml +++ b/Taskfile.yaml @@ -45,7 +45,7 @@ tasks: - "{{ .TOOL_DIR }}/binny" status: - "test -f {{ .TOOL_DIR }}/binny" - # we just need a release of benny, doesn't matter which one (binny will update itself, this is just a bootstrap step) + # we just need a release of binny, doesn't matter which one (binny will update itself, this is just a bootstrap step) # note: on the first release of binny, switch to the curl approach # cmd: "curl -sSfL https://raw.githubusercontent.com/{{ .OWNER }}/{{ .PROJECT }}/main/install.sh | sh -s -- -b {{ .TOOL_DIR }}" cmd: "go build -o {{ .TOOL_DIR }}/binny ./cmd/binny" diff --git a/cmd/binny/cli/command/update_lock.go b/cmd/binny/cli/command/update_lock.go index 0dd16d1..5ed0de4 100644 --- a/cmd/binny/cli/command/update_lock.go +++ b/cmd/binny/cli/command/update_lock.go @@ -68,7 +68,7 @@ func (p updateLockYamlPatcher) PatchYaml(node *yaml.Node) error { } func getUpdatedConfig(cfg option.Core, names []string) (*option.Core, error) { - nameSet := strset.New() + nameSet := strset.New(names...) if len(names) == 0 { nameSet.Add(cfg.Tools.Names()...) } @@ -88,7 +88,7 @@ func getUpdatedConfig(cfg option.Core, names []string) (*option.Core, error) { newVersion, err := t.UpdateVersion(intent.Want, intent.Constraint) if err != nil { - return nil, err + return nil, fmt.Errorf("unable to update version for tool %q: %w", toolCfg.Name, err) } if newVersion == toolCfg.Version.Want { diff --git a/tool/git/version_resolver.go b/tool/git/version_resolver.go index 1771057..4cf904b 100644 --- a/tool/git/version_resolver.go +++ b/tool/git/version_resolver.go @@ -1,6 +1,7 @@ package git import ( + "errors" "fmt" "github.com/go-git/go-git/v5" @@ -27,6 +28,10 @@ func NewVersionResolver(cfg VersionResolutionParameters) *VersionResolver { } func (v VersionResolver) UpdateVersion(want, constraint string) (string, error) { + if want == "current" { + // always use the same reference + return want, nil + } return v.ResolveVersion(want, constraint) } @@ -62,7 +67,7 @@ func headCommit(repoPath string) (string, error) { } ref, err := r.Head() if err != nil { - return "", fmt.Errorf("unable fetch head: %w", err) + return "", fmt.Errorf("unable to fetch head for %q: %w", repoPath, err) } return ref.Hash().String(), nil } @@ -73,22 +78,28 @@ func byReference(repoPath, ref string) (string, error) { return "", fmt.Errorf("unable to open repo: %w", err) } + // try by tag first... plumbRef, err := r.Tag(ref) if err != nil { - return "", fmt.Errorf("unable fetch tag: %w", err) + if !errors.Is(err, git.ErrTagNotFound) { + return "", fmt.Errorf("unable to fetch tag for %q: %w", ref, err) + } } if plumbRef != nil { return plumbRef.Name().String(), nil } - plumbRef, err = r.Reference(plumbing.ReferenceName(ref), true) + // then by hash... + commit, err := r.CommitObject(plumbing.NewHash(ref)) if err != nil { - return "", fmt.Errorf("unable fetch reference: %w", err) + if !errors.Is(err, plumbing.ErrReferenceNotFound) { + return "", fmt.Errorf("unable to fetch hash for %q: %w", ref, err) + } } - if plumbRef != nil { - return plumbRef.Hash().String(), nil + if commit != nil { + return commit.Hash.String(), nil } return "", nil diff --git a/tool/resolve_version.go b/tool/resolve_version.go index f9056a6..e22a6ed 100644 --- a/tool/resolve_version.go +++ b/tool/resolve_version.go @@ -27,7 +27,7 @@ func ResolveVersion(tool binny.VersionResolver, intent binny.VersionIntent) (str resolvedVersion, err := tool.ResolveVersion(want, constraint) if err != nil { - return "", err + return "", fmt.Errorf("failed to resolve version: %w", err) } if constraint != "" {