Repository: caddyserver/caddy (language: go)

Issue:
Improve `caddy version` to include revision information
Go 1.18 adds more information into the build that could be used to make `caddy version` show more useful information for `go build` builds, instead of just showing `(devel)` which is pretty useless. https://tip.golang.org/doc/go1.18#go-version
Improve `caddy version` to include revision information
Go 1.18 adds more information into the build that could be used to make `caddy version` show more useful information for `go build` builds, instead of just showing `(devel)` which is pretty useless. https://tip.golang.org/doc/go1.18#go-version


Discussion / hints:
Ok, so this works, but annoyingly not for `go run`, only `go build`. Will open a PR soon.
Meh, ok, so it only kinda works. The `vcs.revision` key gives us the commit SHA, but not the tag or "version" like what the current build info does.

I suppose that's what https://github.com/golang/go/issues/29228 and https://github.com/golang/go/issues/50603 are for.

But if no proper version is available, we can still print out something useful instead of `(devel)` or `unknown` this way. Unfortunately we still have to maintain our current build practices of putting Caddy as the non-main package in the meantime.
Ok, so this works, but annoyingly not for `go run`, only `go build`. Will open a PR soon.
Meh, ok, so it only kinda works. The `vcs.revision` key gives us the commit SHA, but not the tag or "version" like what the current build info does.

I suppose that's what https://github.com/golang/go/issues/29228 and https://github.com/golang/go/issues/50603 are for.

But if no proper version is available, we can still print out something useful instead of `(devel)` or `unknown` this way. Unfortunately we still have to maintain our current build practices of putting Caddy as the non-main package in the meantime.

Fix patch:
diff --git a/caddy.go b/caddy.go
index 54c890bf3b8..6964f77ce8a 100644
--- a/caddy.go
+++ b/caddy.go
@@ -127,7 +127,9 @@ func Load(cfgJSON []byte, forceReload bool) error {
 // forcefully reloaded, then errConfigUnchanged This function is safe for
 // concurrent use.
 // The ifMatchHeader can optionally be given a string of the format:
-//    "<path> <hash>"
+//
+//	"<path> <hash>"
+//
 // where <path> is the absolute path in the config and <hash> is the expected hash of
 // the config at that path. If the hash in the ifMatchHeader doesn't match
 // the hash of the config, then an APIError with status 412 will be returned.
@@ -791,38 +793,102 @@ func InstanceID() (uuid.UUID, error) {
 	return uuid.ParseBytes(uuidFileBytes)
 }
 
-// GoModule returns the build info of this Caddy
-// build from debug.BuildInfo (requires Go modules).
-// If no version information is available, a non-nil
-// value will still be returned, but with an
-// unknown version.
-func GoModule() *debug.Module {
-	var mod debug.Module
-	return goModule(&mod)
-}
-
-// goModule holds the actual implementation of GoModule.
-// Allocating debug.Module in GoModule() and passing a
-// reference to goModule enables mid-stack inlining.
-func goModule(mod *debug.Module) *debug.Module {
-	mod.Version = "unknown"
+// Version returns the Caddy version in a simple/short form, and
+// a full version string. The short form will not have spaces and
+// is intended for User-Agent strings and similar, but may be
+// omitting valuable information. Note that Caddy must be compiled
+// in a special way to properly embed complete version information.
+// First this function tries to get the version from the embedded
+// build info provided by go.mod dependencies; then it tries to
+// get info from embedded VCS information, which requires having
+// built Caddy from a git repository. If no version is available,
+// this function returns "(devel)" becaise Go uses that, but for
+// the simple form we change it to "unknown".
+//
+// See relevant Go issues: https://github.com/golang/go/issues/29228
+// and https://github.com/golang/go/issues/50603.
+//
+// This function is experimental and subject to change or removal.
+func Version() (simple, full string) {
+	// the currently-recommended way to build Caddy involves
+	// building it as a dependency so we can extract version
+	// information from go.mod tooling; once the upstream
+	// Go issues are fixed, we should just be able to use
+	// bi.Main... hopefully.
+	var module *debug.Module
 	bi, ok := debug.ReadBuildInfo()
 	if ok {
-		mod.Path = bi.Main.Path
-		// The recommended way to build Caddy involves
-		// creating a separate main module, which
-		// TODO: track related Go issue: https://github.com/golang/go/issues/29228
-		// once that issue is fixed, we should just be able to use bi.Main... hopefully.
+		// find the Caddy module in the dependency list
 		for _, dep := range bi.Deps {
 			if dep.Path == ImportPath {
-				return dep
+				module = dep
+				break
 			}
 		}
-		return &bi.Main
 	}
-	return mod
+	if module != nil {
+		simple, full = module.Version, module.Version
+		if module.Sum != "" {
+			full += " " + module.Sum
+		}
+		if module.Replace != nil {
+			full += " => " + module.Replace.Path
+			if module.Replace.Version != "" {
+				simple = module.Replace.Version + "_custom"
+				full += "@" + module.Replace.Version
+			}
+			if module.Replace.Sum != "" {
+				full += " " + module.Replace.Sum
+			}
+		}
+	}
+
+	if full == "" {
+		var vcsRevision string
+		var vcsTime time.Time
+		var vcsModified bool
+		for _, setting := range bi.Settings {
+			switch setting.Key {
+			case "vcs.revision":
+				vcsRevision = setting.Value
+			case "vcs.time":
+				vcsTime, _ = time.Parse(time.RFC3339, setting.Value)
+			case "vcs.modified":
+				vcsModified, _ = strconv.ParseBool(setting.Value)
+			}
+		}
+
+		if vcsRevision != "" {
+			var modified string
+			if vcsModified {
+				modified = "+modified"
+			}
+			full = fmt.Sprintf("%s%s (%s)", vcsRevision, modified, vcsTime.Format(time.RFC822))
+			simple = vcsRevision
+
+			// use short checksum for simple, if hex-only
+			if _, err := hex.DecodeString(simple); err == nil {
+				simple = simple[:8]
+			}
+
+			// append date to simple since it can be convenient
+			// to know the commit date as part of the version
+			if !vcsTime.IsZero() {
+				simple += "-" + vcsTime.Format("20060102")
+			}
+		}
+	}
+
+	if simple == "" || simple == "(devel)" {
+		simple = "unknown"
+	}
+
+	return
 }
 
+// ActiveContext returns the currently-active context.
+// This function is experimental and might be changed
+// or removed in the future.
 func ActiveContext() Context {
 	currentCtxMu.RLock()
 	defer currentCtxMu.RUnlock()
@@ -867,4 +933,5 @@ var (
 var errSameConfig = errors.New("config is unchanged")
 
 // ImportPath is the package import path for Caddy core.
+// This identifier may be removed in the future.
 const ImportPath = "github.com/caddyserver/caddy/v2"
diff --git a/cmd/commandfuncs.go b/cmd/commandfuncs.go
index a4b7bdf7505..67015f78d50 100644
--- a/cmd/commandfuncs.go
+++ b/cmd/commandfuncs.go
@@ -331,30 +331,17 @@ func cmdReload(fl Flags) (int, error) {
 }
 
 func cmdVersion(_ Flags) (int, error) {
-	fmt.Println(CaddyVersion())
+	_, full := caddy.Version()
+	fmt.Println(full)
 	return caddy.ExitCodeSuccess, nil
 }
 
-func cmdBuildInfo(fl Flags) (int, error) {
+func cmdBuildInfo(_ Flags) (int, error) {
 	bi, ok := debug.ReadBuildInfo()
 	if !ok {
 		return caddy.ExitCodeFailedStartup, fmt.Errorf("no build information")
 	}
-
-	fmt.Printf("go_version: %s\n", runtime.Version())
-	fmt.Printf("go_os:      %s\n", runtime.GOOS)
-	fmt.Printf("go_arch:    %s\n", runtime.GOARCH)
-	fmt.Printf("path:       %s\n", bi.Path)
-	fmt.Printf("main:       %s %s %s\n", bi.Main.Path, bi.Main.Version, bi.Main.Sum)
-	fmt.Println("dependencies:")
-
-	for _, goMod := range bi.Deps {
-		fmt.Printf("%s %s %s", goMod.Path, goMod.Version, goMod.Sum)
-		if goMod.Replace != nil {
-			fmt.Printf(" => %s %s %s", goMod.Replace.Path, goMod.Replace.Version, goMod.Replace.Sum)
-		}
-		fmt.Println()
-	}
+	fmt.Println(bi)
 	return caddy.ExitCodeSuccess, nil
 }
 
diff --git a/cmd/main.go b/cmd/main.go
index e5a4edfe252..e932b6b8a5c 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -38,8 +38,8 @@ import (
 
 func init() {
 	// set a fitting User-Agent for ACME requests
-	goModule := caddy.GoModule()
-	cleanModVersion := strings.TrimPrefix(goModule.Version, "v")
+	version, _ := caddy.Version()
+	cleanModVersion := strings.TrimPrefix(version, "v")
 	certmagic.UserAgent = "Caddy/" + cleanModVersion
 
 	// by using Caddy, user indicates agreement to CA terms
@@ -441,11 +441,12 @@ func parseEnvFile(envInput io.Reader) (map[string]string, error) {
 }
 
 func printEnvironment() {
+	_, version := caddy.Version()
 	fmt.Printf("caddy.HomeDir=%s\n", caddy.HomeDir())
 	fmt.Printf("caddy.AppDataDir=%s\n", caddy.AppDataDir())
 	fmt.Printf("caddy.AppConfigDir=%s\n", caddy.AppConfigDir())
 	fmt.Printf("caddy.ConfigAutosavePath=%s\n", caddy.ConfigAutosavePath)
-	fmt.Printf("caddy.Version=%s\n", CaddyVersion())
+	fmt.Printf("caddy.Version=%s\n", version)
 	fmt.Printf("runtime.GOOS=%s\n", runtime.GOOS)
 	fmt.Printf("runtime.GOARCH=%s\n", runtime.GOARCH)
 	fmt.Printf("runtime.Compiler=%s\n", runtime.Compiler)
@@ -462,25 +463,6 @@ func printEnvironment() {
 	}
 }
 
-// CaddyVersion returns a detailed version string, if available.
-func CaddyVersion() string {
-	goModule := caddy.GoModule()
-	ver := goModule.Version
-	if goModule.Sum != "" {
-		ver += " " + goModule.Sum
-	}
-	if goModule.Replace != nil {
-		ver += " => " + goModule.Replace.Path
-		if goModule.Replace.Version != "" {
-			ver += "@" + goModule.Replace.Version
-		}
-		if goModule.Replace.Sum != "" {
-			ver += " " + goModule.Replace.Sum
-		}
-	}
-	return ver
-}
-
 // StringSlice is a flag.Value that enables repeated use of a string flag.
 type StringSlice []string
 
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
index 28481337009..313f4ebd1e0 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
@@ -94,10 +94,8 @@ func (t *Transport) Provision(ctx caddy.Context) error {
 		t.Root = "{http.vars.root}"
 	}
 
-	t.serverSoftware = "Caddy"
-	if mod := caddy.GoModule(); mod.Version != "" {
-		t.serverSoftware += "/" + mod.Version
-	}
+	version, _ := caddy.Version()
+	t.serverSoftware = "Caddy/" + version
 
 	// Set a relatively short default dial timeout.
 	// This is helpful to make load-balancer retries more speedy.
diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go
index ce23944cfcd..ddb01e82fc5 100644
--- a/modules/caddyhttp/tracing/tracer.go
+++ b/modules/caddyhttp/tracing/tracer.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/caddyserver/caddy/v2"
 
-	caddycmd "github.com/caddyserver/caddy/v2/cmd"
 	"github.com/caddyserver/caddy/v2/modules/caddyhttp"
 	"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
 	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
@@ -52,7 +51,8 @@ func newOpenTelemetryWrapper(
 		spanName: spanName,
 	}
 
-	res, err := ot.newResource(webEngineName, caddycmd.CaddyVersion())
+	version, _ := caddy.Version()
+	res, err := ot.newResource(webEngineName, version)
 	if err != nil {
 		return ot, fmt.Errorf("creating resource error: %w", err)
 	}


Test patch: