diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e10a54..35937b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,12 +22,17 @@ ### Breaking Changes -* The CLI now consists of multiple subcommands, thus being incompatible with the CLI in cyclonedx-gomod `v0.x`. -* Detected licenses (when using the `-licenses` flag) will now use the `components/evidence/licenses` node instead of `components/licenses`. Tools that consume SBOMs and don't support CycloneDX v1.3 yet may not recognize those licenses. -* Version normalization has been removed ([#60](https://github.com/CycloneDX/cyclonedx-gomod/pull/60)). As a consequence, `+incompatible` suffixes and `v` prefixes (`-noprefix` flag in cyclonedx-gomod v0.x) are not trimmed anymore. +* The CLI now consists of multiple subcommands, thus being incompatible with the CLI in cyclonedx-gomod `v0.x` +* Detected licenses (when using the `-licenses` flag) will now use the `components/evidence/licenses` node instead of `components/licenses`. Tools that consume SBOMs and don't support CycloneDX v1.3 yet may not recognize those licenses +* Version normalization has been removed ([#60](https://github.com/CycloneDX/cyclonedx-gomod/pull/60)). As a consequence, `+incompatible` suffixes and `v` prefixes (`-novprefix` flag in `v0.x`) are not trimmed anymore +* The `-reproducible` flag has been removed ### Dependency Updates * Update `github.com/CycloneDX/cyclonedx-go` from `v0.3.0` to `v0.4.0` (via [`5bab19b`](https://github.com/CycloneDX/cyclonedx-gomod/commit/5bab19bbed9c6de22112ebeb2f71691c4b4163f5)) * Update `golang.org/x/mod` from `v0.4.2` to `v0.5.0` (via [#57](https://github.com/CycloneDX/cyclonedx-gomod/pull/57)) * Update `golang.org/x/crypto` from `v0.0.0-20210711020723-a769d52b0f97` to `v0.0.0-20210817164053-32db794688a5` (via [`75ae52a`](https://github.com/CycloneDX/cyclonedx-gomod/commit/75ae52ac039d9d702a1861c9625d0a14116097ce)) + +### Known Issues + +* When used with Go 1.17 or newer, with modules that have `go 1.17` in their `go.mod`, the dependency graph may contain too many edges ([#64](https://github.com/CycloneDX/cyclonedx-gomod/issues/64)) diff --git a/README.md b/README.md index 76a44534..f153f3bc 100644 --- a/README.md +++ b/README.md @@ -66,14 +66,13 @@ Noteworthy environment variables that act as build constraints are: A complete overview of all environment variables can be found here: https://pkg.go.dev/cmd/go#hdr-Environment_variables -Unless the -reproducible flag is provided, build constraints will be -included as properties of the main component. +Applicable build constraints will be included as properties of the main component. The -main flag should be used to specify the path to the application's main file. -main must point to a go file within MODPATH. If -main is not specified, "main.go" is assumed. -By passing -files, all files that would be compiled into the binary will be included -as subcomponents of their respective module. Files versions follow the v0.0.0-SHORTHASH pattern, +By passing -files, all files that would be included in a binary will be attached +as subcomponents of their respective module. File versions follow the v0.0.0-SHORTHASH pattern, where SHORTHASH is the first 12 characters of the file's SHA1 hash. Examples: @@ -81,16 +80,15 @@ Examples: $ cyclonedx-gomod app -json -output acme-app.bom.json -files -licenses -main cmd/acme-app/main.go /usr/src/acme-module FLAGS - -files=false Include files - -json=false Output in JSON - -licenses=false Resolve module licenses - -main main.go Path to the application's main file, relative to MODPATH - -noserial=false Omit serial number - -output - Output file path (or - for STDOUT) - -reproducible=false Make the SBOM reproducible by omitting dynamic content - -serial ... Serial number - -std=false Include Go standard library as component and dependency of the module - -verbose=false Enable verbose output + -files=false Include files + -json=false Output in JSON + -licenses=false Resolve module licenses + -main main.go Path to the application's main file, relative to MODPATH + -noserial=false Omit serial number + -output - Output file path (or - for STDOUT) + -serial ... Serial number + -std=false Include Go standard library as component and dependency of the module + -verbose=false Enable verbose output ``` #### `bin` @@ -103,24 +101,25 @@ Generate SBOM for a binary. When license resolution is enabled, all modules (including the main module) will be downloaded to the module cache using "go mod download". +For the download of the main module to work, its version has to be provided +via the -version flag. Please note that data embedded in binaries shouldn't be trusted, unless there's solid evidence that the binaries haven't been modified since they've been built. Example: - $ cyclonedx-gomod bin -json -output minikube-v1.22.0.bom.json -version v1.22.0 ./minikube + $ cyclonedx-gomod bin -json -output acme-app-v1.0.0.bom.json -version v1.0.0 ./acme-app FLAGS - -json=false Output in JSON - -licenses=false Resolve module licenses - -noserial=false Omit serial number - -output - Output file path (or - for STDOUT) - -reproducible=false Make the SBOM reproducible by omitting dynamic content - -serial ... Serial number - -std=false Include Go standard library as component and dependency of the module - -verbose=false Enable verbose output - -version ... Version of the main component + -json=false Output in JSON + -licenses=false Resolve module licenses + -noserial=false Omit serial number + -output - Output file path (or - for STDOUT) + -serial ... Serial number + -std=false Include Go standard library as component and dependency of the module + -verbose=false Enable verbose output + -version ... Version of the main component ``` #### `mod` @@ -136,16 +135,15 @@ Examples: $ cyclonedx-gomod mod -reproducible -test -output bom.xml ./cyclonedx-go FLAGS - -json=false Output in JSON - -licenses=false Resolve module licenses - -noserial=false Omit serial number - -output - Output file path (or - for STDOUT) - -reproducible=false Make the SBOM reproducible by omitting dynamic content - -serial ... Serial number - -std=false Include Go standard library as component and dependency of the module - -test=false Include test dependencies - -type application Type of the main component - -verbose=false Enable verbose output + -json=false Output in JSON + -licenses=false Resolve module licenses + -noserial=false Omit serial number + -output - Output file path (or - for STDOUT) + -serial ... Serial number + -std=false Include Go standard library as component and dependency of the module + -test=false Include test dependencies + -type application Type of the main component + -verbose=false Enable verbose output ``` ### Examples diff --git a/internal/cli/cmd/app/app.go b/internal/cli/cmd/app/app.go index c0589017..3b575cc8 100644 --- a/internal/cli/cmd/app/app.go +++ b/internal/cli/cmd/app/app.go @@ -20,14 +20,15 @@ package app import ( "context" "flag" + "fmt" "strings" cdx "github.com/CycloneDX/cyclonedx-go" - cliutil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util" + cliUtil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util" "github.com/CycloneDX/cyclonedx-gomod/internal/gocmd" "github.com/CycloneDX/cyclonedx-gomod/internal/gomod" "github.com/CycloneDX/cyclonedx-gomod/internal/sbom" - modconv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module" + modConv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module" "github.com/peterbourgon/ff/v3/ffcli" "github.com/rs/zerolog/log" ) @@ -57,14 +58,13 @@ Noteworthy environment variables that act as build constraints are: A complete overview of all environment variables can be found here: https://pkg.go.dev/cmd/go#hdr-Environment_variables -Unless the -reproducible flag is provided, build constraints will be -included as properties of the main component. +Applicable build constraints will be included as properties of the main component. The -main flag should be used to specify the path to the application's main file. -main must point to a go file within MODPATH. If -main is not specified, "main.go" is assumed. -By passing -files, all files that would be compiled into the binary will be included -as subcomponents of their respective module. Files versions follow the v0.0.0-SHORTHASH pattern, +By passing -files, all files that would be included in a binary will be attached +as subcomponents of their respective module. File versions follow the v0.0.0-SHORTHASH pattern, where SHORTHASH is the first 12 characters of the file's SHA1 hash. Examples: @@ -81,7 +81,7 @@ Examples: options.ModuleDir = args[0] } - cliutil.ConfigureLogger(options.LogOptions) + cliUtil.ConfigureLogger(options.LogOptions) return Exec(options) }, @@ -96,30 +96,34 @@ func Exec(options Options) error { modules, err := gomod.GetModulesFromPackages(options.ModuleDir, options.Main) if err != nil { - return err + return fmt.Errorf("failed to collect modules: %w", err) } // Dependencies need to be applied prior to determining the main // module's version, because `go mod graph` omits that version. err = gomod.ApplyModuleGraph(options.ModuleDir, modules) if err != nil { - return err + return fmt.Errorf("failed to apply module graph: %w", err) } + // Determine version of main module modules[0].Version, err = gomod.GetModuleVersion(modules[0].Dir) if err != nil { - return err + return fmt.Errorf("failed to determine version of main module: %w", err) } - mainComponent, err := modconv.ToComponent(modules[0], - modconv.WithComponentType(cdx.ComponentTypeApplication), - modconv.WithFiles(options.IncludeFiles), - modconv.WithLicenses(options.ResolveLicenses), + // Convert main module + mainComponent, err := modConv.ToComponent(modules[0], + modConv.WithComponentType(cdx.ComponentTypeApplication), + modConv.WithFiles(options.IncludeFiles), + modConv.WithLicenses(options.ResolveLicenses), ) if err != nil { - return err + return fmt.Errorf("failed to convert main module: %w", err) } + // Build properties (e.g. the Go version) depend on the environment + // and are thus only included when the SBOM doesn't have to be reproducible. if !options.SBOMOptions.Reproducible { buildProperties, err := createBuildProperties() if err != nil { @@ -132,43 +136,43 @@ func Exec(options Options) error { } } - components, err := modconv.ToComponents(modules[1:], - modconv.WithFiles(options.IncludeFiles), - modconv.WithLicenses(options.ResolveLicenses), - modconv.WithModuleHashes(), + // Convert the other modules + components, err := modConv.ToComponents(modules[1:], + modConv.WithFiles(options.IncludeFiles), + modConv.WithLicenses(options.ResolveLicenses), + modConv.WithModuleHashes(), ) if err != nil { - return err + return fmt.Errorf("failed to convert modules: %w", err) } - dependencies := sbom.BuildDependencyGraph(modules) - bom := cdx.NewBOM() - - err = cliutil.SetSerialNumber(bom, options.SBOMOptions) + err = cliUtil.SetSerialNumber(bom, options.SBOMOptions) if err != nil { - return err + return fmt.Errorf("failed to set serial number: %w", err) } + // Assemble metadata bom.Metadata = &cdx.Metadata{ Component: mainComponent, } - err = cliutil.AddCommonMetadata(bom, options.SBOMOptions) + err = cliUtil.AddCommonMetadata(bom, options.SBOMOptions) if err != nil { - return err + return fmt.Errorf("failed to add common metadata: %w", err) } bom.Components = &components + dependencies := sbom.BuildDependencyGraph(modules) bom.Dependencies = &dependencies if options.IncludeStd { - err = cliutil.AddStdComponent(bom) + err = cliUtil.AddStdComponent(bom) if err != nil { - return err + return fmt.Errorf("failed to add stdlib component: %w", err) } } - return cliutil.WriteBOM(bom, options.OutputOptions) + return cliUtil.WriteBOM(bom, options.OutputOptions) } var buildEnv = []string{ diff --git a/internal/cli/cmd/bin/bin.go b/internal/cli/cmd/bin/bin.go index 44335746..4049e3bd 100644 --- a/internal/cli/cmd/bin/bin.go +++ b/internal/cli/cmd/bin/bin.go @@ -25,10 +25,10 @@ import ( "path/filepath" cdx "github.com/CycloneDX/cyclonedx-go" - cliutil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util" + cliUtil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util" "github.com/CycloneDX/cyclonedx-gomod/internal/gomod" "github.com/CycloneDX/cyclonedx-gomod/internal/sbom" - modconv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module" + modConv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module" "github.com/peterbourgon/ff/v3/ffcli" "github.com/rs/zerolog/log" ) @@ -55,16 +55,16 @@ unless there's solid evidence that the binaries haven't been modified since they've been built. Example: - $ cyclonedx-gomod bin -json -output minikube-v1.22.0.bom.json -version v1.22.0 ./minikube`, + $ cyclonedx-gomod bin -json -output acme-app-v1.0.0.bom.json -version v1.0.0 ./acme-app`, FlagSet: fs, Exec: func(_ context.Context, args []string) error { if len(args) != 1 { return fmt.Errorf("no binary path provided") } + options.BinaryPath = args[0] - cliutil.ConfigureLogger(options.LogOptions) + cliUtil.ConfigureLogger(options.LogOptions) - options.BinaryPath = args[0] return Exec(options) }, } @@ -87,10 +87,11 @@ func Exec(options Options) error { modules[0].Version = options.Version } + // If we want to resolve licenses, we have to download the modules first if options.ResolveLicenses { err = downloadModules(modules, hashes) if err != nil { - return err + return fmt.Errorf("failed to download modules: %w", err) } } @@ -99,20 +100,22 @@ func Exec(options Options) error { modules[0].Dependencies = append(modules[0].Dependencies, &modules[i]) } - mainComponent, err := modconv.ToComponent(modules[0], - modconv.WithComponentType(cdx.ComponentTypeApplication), - modconv.WithLicenses(options.ResolveLicenses), + // Convert main module + mainComponent, err := modConv.ToComponent(modules[0], + modConv.WithComponentType(cdx.ComponentTypeApplication), + modConv.WithLicenses(options.ResolveLicenses), ) if err != nil { - return err + return fmt.Errorf("failed to convert main module: %w", err) } - components, err := modconv.ToComponents(modules[1:], - modconv.WithLicenses(options.ResolveLicenses), + // Convert the other modules + components, err := modConv.ToComponents(modules[1:], + modConv.WithLicenses(options.ResolveLicenses), withModuleHashes(hashes), ) if err != nil { - return err + return fmt.Errorf("failed to convert modules: %w", err) } dependencyGraph := sbom.BuildDependencyGraph(modules) @@ -124,7 +127,7 @@ func Exec(options Options) error { bom := cdx.NewBOM() - err = cliutil.SetSerialNumber(bom, options.SBOMOptions) + err = cliUtil.SetSerialNumber(bom, options.SBOMOptions) if err != nil { return err } @@ -133,7 +136,7 @@ func Exec(options Options) error { Component: mainComponent, Properties: &binaryProperties, } - err = cliutil.AddCommonMetadata(bom, options.SBOMOptions) + err = cliUtil.AddCommonMetadata(bom, options.SBOMOptions) if err != nil { return err } @@ -142,10 +145,10 @@ func Exec(options Options) error { bom.Dependencies = &dependencyGraph bom.Compositions = createCompositions(*mainComponent, components) - return cliutil.WriteBOM(bom, options.OutputOptions) + return cliUtil.WriteBOM(bom, options.OutputOptions) } -func withModuleHashes(hashes map[string]string) modconv.Option { +func withModuleHashes(hashes map[string]string) modConv.Option { return func(m gomod.Module, c *cdx.Component) error { h1, ok := hashes[m.Coordinates()] if !ok { diff --git a/internal/cli/cmd/mod/mod.go b/internal/cli/cmd/mod/mod.go index 614a6c99..126abf62 100644 --- a/internal/cli/cmd/mod/mod.go +++ b/internal/cli/cmd/mod/mod.go @@ -25,11 +25,11 @@ import ( "io" cdx "github.com/CycloneDX/cyclonedx-go" - cliutil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util" + cliUtil "github.com/CycloneDX/cyclonedx-gomod/internal/cli/util" "github.com/CycloneDX/cyclonedx-gomod/internal/gocmd" "github.com/CycloneDX/cyclonedx-gomod/internal/gomod" "github.com/CycloneDX/cyclonedx-gomod/internal/sbom" - modconv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module" + modConv "github.com/CycloneDX/cyclonedx-gomod/internal/sbom/convert/module" "github.com/peterbourgon/ff/v3/ffcli" "github.com/rs/zerolog/log" ) @@ -60,7 +60,7 @@ Examples: options.ModuleDir = args[0] } - cliutil.ConfigureLogger(options.LogOptions) + cliUtil.ConfigureLogger(options.LogOptions) return Exec(options) }, @@ -80,15 +80,16 @@ func Exec(options Options) error { return fmt.Errorf("downloading modules failed: %w", err) } + // Try to collect modules from vendor/ directory first and if that fails, use `go list`. modules, err := gomod.GetVendoredModules(options.ModuleDir, options.IncludeTest) if err != nil { if errors.Is(err, gomod.ErrNotVendoring) { modules, err = gomod.GetModules(options.ModuleDir, options.IncludeTest) if err != nil { - return err + return fmt.Errorf("failed to collect modules: %w", err) } } else { - return err + return fmt.Errorf("failed to collect vendored modules: %w", err) } } @@ -97,54 +98,55 @@ func Exec(options Options) error { return fmt.Errorf("failed to apply module graph: %w", err) } + // Determine version of main module modules[0].Version, err = gomod.GetModuleVersion(modules[0].Dir) if err != nil { log.Warn().Err(err).Msg("failed to determine version of main module") } - mainComponent, err := modconv.ToComponent(modules[0], - modconv.WithComponentType(cdx.ComponentType(options.ComponentType)), - modconv.WithLicenses(options.ResolveLicenses), + // Convert main module + mainComponent, err := modConv.ToComponent(modules[0], + modConv.WithComponentType(cdx.ComponentType(options.ComponentType)), + modConv.WithLicenses(options.ResolveLicenses), ) if err != nil { return fmt.Errorf("failed to convert main module: %w", err) } - components, err := modconv.ToComponents(modules[1:], - modconv.WithLicenses(options.ResolveLicenses), - modconv.WithModuleHashes(), + // Convert the other modules + components, err := modConv.ToComponents(modules[1:], + modConv.WithLicenses(options.ResolveLicenses), + modConv.WithModuleHashes(), ) if err != nil { return fmt.Errorf("failed to convert modules: %w", err) } - dependencyGraph := sbom.BuildDependencyGraph(modules) - bom := cdx.NewBOM() - - err = cliutil.SetSerialNumber(bom, options.SBOMOptions) + err = cliUtil.SetSerialNumber(bom, options.SBOMOptions) if err != nil { - return err + return fmt.Errorf("failed to set serial number: %w", err) } + // Assemble metadata bom.Metadata = &cdx.Metadata{ Component: mainComponent, } - - err = cliutil.AddCommonMetadata(bom, options.SBOMOptions) + err = cliUtil.AddCommonMetadata(bom, options.SBOMOptions) if err != nil { - return err + return fmt.Errorf("failed to add common metadata: %w", err) } bom.Components = &components + dependencyGraph := sbom.BuildDependencyGraph(modules) bom.Dependencies = &dependencyGraph if options.IncludeStd { - err = cliutil.AddStdComponent(bom) + err = cliUtil.AddStdComponent(bom) if err != nil { - return err + return fmt.Errorf("failed to add stdlib component: %w", err) } } - return cliutil.WriteBOM(bom, options.OutputOptions) + return cliUtil.WriteBOM(bom, options.OutputOptions) } diff --git a/internal/cli/options/options.go b/internal/cli/options/options.go index 30dfadf3..b9396dd3 100644 --- a/internal/cli/options/options.go +++ b/internal/cli/options/options.go @@ -66,7 +66,7 @@ func (o OutputOptions) Validate() error { type SBOMOptions struct { IncludeStd bool NoSerialNumber bool - Reproducible bool + Reproducible bool // Make the SBOM reproducible by omitting dynamic content ResolveLicenses bool SerialNumber string } @@ -74,7 +74,7 @@ type SBOMOptions struct { func (s *SBOMOptions) RegisterFlags(fs *flag.FlagSet) { fs.BoolVar(&s.IncludeStd, "std", false, "Include Go standard library as component and dependency of the module") fs.BoolVar(&s.NoSerialNumber, "noserial", false, "Omit serial number") - fs.BoolVar(&s.Reproducible, "reproducible", false, "Make the SBOM reproducible by omitting dynamic content") + // .Reproducible is used for testing only and intentionally omitted here fs.BoolVar(&s.ResolveLicenses, "licenses", false, "Resolve module licenses") fs.StringVar(&s.SerialNumber, "serial", "", "Serial number") } diff --git a/internal/gocmd/gocmd.go b/internal/gocmd/gocmd.go index 31ffd8b2..c5392bf3 100644 --- a/internal/gocmd/gocmd.go +++ b/internal/gocmd/gocmd.go @@ -156,6 +156,7 @@ func executeGoCommand(args []string, options ...commandOption) error { log.Debug(). Str("cmd", cmd.String()). + Str("dir", cmd.Dir). Msg("executing command") return cmd.Run() diff --git a/internal/gomod/binary.go b/internal/gomod/binary.go index c1b34fea..49dc9c8d 100644 --- a/internal/gomod/binary.go +++ b/internal/gomod/binary.go @@ -60,7 +60,7 @@ func parseModulesFromBinary(reader io.Reader) ([]Module, map[string]string) { Main: true, }) moduleIndex += 1 - case "dep": // Depdendency module + case "dep": // Dependency module module := Module{ Path: fields[1], Version: fields[2], diff --git a/internal/gomod/module.go b/internal/gomod/module.go index 9ecd9c63..89acc08c 100644 --- a/internal/gomod/module.go +++ b/internal/gomod/module.go @@ -84,13 +84,13 @@ func GetModule(moduleDir string) (*Module, error) { err := gocmd.GetModule(moduleDir, buf) if err != nil { - return nil, err + return nil, fmt.Errorf("listing module failed: %w", err) } var module Module err = json.NewDecoder(buf).Decode(&module) if err != nil { - return nil, err + return nil, fmt.Errorf("decoding module info failed: %w", err) } return &module, nil @@ -147,7 +147,7 @@ func parseModules(reader io.Reader) ([]Module, error) { return modules, nil } -// sortModules sorts a given Module slice ascendingly by path. +// sortModules sorts a given Module slice ascending by path. // Main modules take precedence, so that they will represent the first elements of the sorted slice. // If the path of two modules are equal, they'll be compared by their semantic version instead. func sortModules(modules []Module) { diff --git a/internal/gomod/package.go b/internal/gomod/package.go index 9aba0b9e..eb383cda 100644 --- a/internal/gomod/package.go +++ b/internal/gomod/package.go @@ -59,20 +59,19 @@ func GetModulesFromPackages(moduleDir, packagePattern string) ([]Module, error) } buf := new(bytes.Buffer) - err := gocmd.ListPackages(moduleDir, packagePattern, buf) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to list packages for pattern \"%s\": %w", packagePattern, err) } pkgMap, err := parsePackages(buf) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to parse `go list` output: %w", err) } modules, err := convertPackages(moduleDir, pkgMap) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to convert packages to modules: %w", err) } err = ResolveLocalReplacements(moduleDir, modules) @@ -85,7 +84,7 @@ func GetModulesFromPackages(moduleDir, packagePattern string) ([]Module, error) return modules, nil } -// parsePackageInfo parses the output of `go list -json`. +// parsePackages parses the output of `go list -json`. // The keys of the returned map are module coordinates (path@version). func parsePackages(reader io.Reader) (map[string][]Package, error) { pkgsMap := make(map[string][]Package) diff --git a/internal/gomod/version.go b/internal/gomod/version.go index d346fe97..c9e4128f 100644 --- a/internal/gomod/version.go +++ b/internal/gomod/version.go @@ -31,8 +31,7 @@ import ( "golang.org/x/mod/semver" ) -// GetModuleVersion attempts to detect a given module's version by first -// calling GetVersionFromTag and if that fails, GetPseudoVersion on it. +// GetModuleVersion attempts to detect a given module's version. // // If no Git repository is found in moduleDir, directories will be traversed // upwards until the root directory is reached. This is done to accommodate @@ -63,7 +62,8 @@ func GetModuleVersion(moduleDir string) (string, error) { } } -// GetVersionFromTag checks if the current commit is annotated with a tag and if it is, returns that tag's name. +// GetVersionFromTag checks if the HEAD commit is annotated with a tag and if it is, returns that tag's name. +// If the HEAD commit is not tagged, a pseudo version will be generated and returned instead. func GetVersionFromTag(moduleDir string) (string, error) { repo, err := git.PlainOpen(moduleDir) if err != nil {