Add runtime version reporting

This commit is contained in:
2026-10-05 16:49:54 +00:00
parent 9e855c81a6
commit d692607e2b
11 changed files with 292 additions and 9 deletions

View File

@@ -1,10 +1,11 @@
# ReleaseKit
ReleaseKit is an opinionated Go library and executable for a shared release
process. The Go module and documented root `releasekit` facade are established;
the library operations, runtime version package, executable, tests, and release
automation remain to be implemented. Their selected behavior and sequence are
described in the [product roadmap](docs/roadmap/releasekit.md) and
process. The Go module, root `releasekit` facade, `buildinfo` runtime package,
and `releasekit version` command are established; the library operations beyond
runtime version access, remaining CLI commands, and release automation remain
to be implemented. Their selected behavior and sequence are described in the
[product roadmap](docs/roadmap/releasekit.md) and
[implementation plan](docs/roadmap/implementation.md).
Run the offline Go test suite from the repository root with:
@@ -13,6 +14,12 @@ Run the offline Go test suite from the repository root with:
go test ./...
```
The current command prints ReleaseKit's version:
```sh
go run ./cmd/releasekit version
```
## Documentation
- [ReleaseKit proposal](docs/roadmap/releasekit.md): proposed publication, builds,
@@ -21,6 +28,8 @@ go test ./...
policy reading.
- [Architecture policy](docs/policy/architecture.md): responsibility and dependency
boundaries for development.
- [Runtime version guidance](docs/runtime.md): how applications read their
embedded version through `buildinfo`.
- [ADR 0001](docs/adr/0001-public-facade.md): the accepted public facade decision.
- [Policy adoption roadmap](docs/roadmap/policy.md): completed adoption and pending
release bootstrap work.

12
buildinfo/version.go Normal file
View File

@@ -0,0 +1,12 @@
// Package buildinfo provides runtime version information for applications
// built with ReleaseKit.
package buildinfo
import "gitea.maximumdirect.net/eric/ReleaseKit/internal/runtimeinfo"
// Version returns the injected release identifier when available. Otherwise,
// it returns the executable's main module version, or "dev" when that metadata
// is unavailable or identifies a development build.
func Version() string {
return runtimeinfo.Version()
}

View File

@@ -0,0 +1,81 @@
package buildinfo_test
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
)
func TestVersionInConsumerExecutable(t *testing.T) {
repo, err := filepath.Abs("..")
if err != nil {
t.Fatal(err)
}
dir := t.TempDir()
module := fmt.Sprintf(`module example.com/versionfixture
go 1.25.0
require gitea.maximumdirect.net/eric/ReleaseKit v0.0.0
replace gitea.maximumdirect.net/eric/ReleaseKit => %s
`, filepath.ToSlash(repo))
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(module), 0o600); err != nil {
t.Fatal(err)
}
program := `package main
import (
"fmt"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
)
func main() { fmt.Print(buildinfo.Version()) }
`
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(program), 0o600); err != nil {
t.Fatal(err)
}
for _, test := range []struct {
name string
linkerArgs []string
want string
}{
{name: "development build", want: "dev"},
{
name: "injected release version",
linkerArgs: []string{"-X", versioncontract.InjectedVersionLinkerSymbol + "=v1.2.3"},
want: "v1.2.3",
},
} {
t.Run(test.name, func(t *testing.T) {
binary := filepath.Join(t.TempDir(), "versionfixture")
args := []string{"build", "-o", binary}
if len(test.linkerArgs) > 0 {
args = append(args, "-ldflags", fmt.Sprint(test.linkerArgs[0], " ", test.linkerArgs[1]))
}
args = append(args, ".")
build := exec.Command("go", args...)
build.Dir = dir
build.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOSUMDB=off")
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build consumer fixture: %v\n%s", err, output)
}
command := exec.Command(binary)
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("run consumer fixture: %v\n%s", err, output)
}
if string(output) != test.want {
t.Fatalf("version output = %q, want %q", output, test.want)
}
})
}
}

23
cmd/releasekit/main.go Normal file
View File

@@ -0,0 +1,23 @@
package main
import (
"fmt"
"io"
"os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
)
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
func run(args []string, stdout, stderr io.Writer) int {
if len(args) == 1 && args[0] == "version" {
fmt.Fprintf(stdout, "releasekit %s\n", buildinfo.Version())
return 0
}
fmt.Fprintln(stderr, "usage: releasekit version")
return 2
}

View File

@@ -0,0 +1,35 @@
package main
import (
"bytes"
"fmt"
"testing"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
)
func TestVersionCommand(t *testing.T) {
var stdout, stderr bytes.Buffer
if status := run([]string{"version"}, &stdout, &stderr); status != 0 {
t.Fatalf("run() status = %d, want 0; stderr = %q", status, stderr.String())
}
if want := fmt.Sprintf("releasekit %s\n", buildinfo.Version()); stdout.String() != want {
t.Fatalf("version output = %q, want %q", stdout.String(), want)
}
if stderr.Len() != 0 {
t.Fatalf("version command wrote to stderr: %q", stderr.String())
}
}
func TestUnknownCommand(t *testing.T) {
var stdout, stderr bytes.Buffer
if status := run([]string{"publish"}, &stdout, &stderr); status != 2 {
t.Fatalf("run() status = %d, want 2", status)
}
if stdout.Len() != 0 {
t.Fatalf("unknown command wrote to stdout: %q", stdout.String())
}
if want := "usage: releasekit version\n"; stderr.String() != want {
t.Fatalf("usage output = %q, want %q", stderr.String(), want)
}
}

View File

@@ -42,10 +42,10 @@ Run the offline Go test suite from the repository root with:
go test ./...
```
The implemented package inventory currently contains only the root
`releasekit` facade package. Its public operations are added as their behavior is
implemented; see the [implementation plan](roadmap/implementation.md) for
current progress.
The implemented package inventory currently contains the root `releasekit`
facade and `buildinfo` runtime facade, plus the `cmd/releasekit` executable.
Orchestration operations are added as their behavior is implemented; see the
[implementation plan](roadmap/implementation.md) for current progress.
For prose changes, review the diff and check local links, fragment destinations,
provenance, template residue, and consistency with current repository contents.

View File

@@ -456,7 +456,7 @@ LGPL licensing and attribution notices remain consistent.
## Stage 2: Implement the runtime version facade
Status: Pending. Depends on Stage 1.
Status: Complete. Depends on Stage 1.
1. Implement `buildinfo.Version` and its internal injected-string and main-module
fallback logic. Keep runtime state private and the public accessor small.
@@ -470,6 +470,15 @@ Acceptance: a source-built fixture reports `dev`; a fixture linked with a stable
version reports that version. Fallback metadata uses the executable main module.
CLI version output is correct and no runtime orchestration initialization occurs.
Completion evidence: implemented `buildinfo.Version`, its private injected
string and main-module fallback, and the internal linker-symbol contract. Unit
tests cover injection precedence and missing, development, and stable main
module metadata. Built and ran a separate consumer executable from source and
with an injected stable version using network-disabled Go commands; both
reported the selected value. CLI tests cover its version line, and the CLI's
dependency closure contains no orchestration package. Added runtime consumer
guidance and documented the command contract.
## Stage 3: Protect public contracts and dependency direction
Status: Pending. Depends on Stage 2.

24
docs/runtime.md Normal file
View File

@@ -0,0 +1,24 @@
# Runtime version
Applications can read their embedded version through the public `buildinfo`
package:
```go
import "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
version := buildinfo.Version()
```
`Version` returns the identifier injected into the executable when present.
Otherwise it returns the executable's main module version when that metadata is
available and is not `(devel)`. It returns `dev` when neither source provides a
release identifier. Applications should use this accessor for version output;
the runtime implementation and ReleaseKit's builder own the injection detail.
ReleaseKit's current command prints its own runtime version:
```sh
go run ./cmd/releasekit version
```
The output has the form `releasekit <version>`.

View File

@@ -0,0 +1,26 @@
// Package runtimeinfo implements the private runtime version state used by the
// public buildinfo facade.
package runtimeinfo
import "runtime/debug"
var injectedVersion string
// Version returns the injected version or the running executable's main module
// version, falling back to "dev" when neither provides a release identifier.
func Version() string {
return version(injectedVersion, debug.ReadBuildInfo)
}
func version(injected string, readBuildInfo func() (*debug.BuildInfo, bool)) string {
if injected != "" {
return injected
}
info, ok := readBuildInfo()
if ok && info != nil && info.Main.Version != "" && info.Main.Version != "(devel)" {
return info.Main.Version
}
return "dev"
}

View File

@@ -0,0 +1,57 @@
package runtimeinfo
import (
"runtime/debug"
"testing"
)
func TestVersion(t *testing.T) {
tests := []struct {
name string
injected string
info *debug.BuildInfo
ok bool
want string
}{
{
name: "injected version takes precedence",
injected: "v1.2.3",
info: &debug.BuildInfo{Main: debug.Module{Version: "v4.5.6"}},
ok: true,
want: "v1.2.3",
},
{
name: "main module version",
info: &debug.BuildInfo{Main: debug.Module{Version: "v4.5.6"}},
ok: true,
want: "v4.5.6",
},
{
name: "missing build information",
want: "dev",
},
{
name: "development main module",
info: &debug.BuildInfo{Main: debug.Module{Version: "(devel)"}},
ok: true,
want: "dev",
},
{
name: "empty main module version",
info: &debug.BuildInfo{Main: debug.Module{}},
ok: true,
want: "dev",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := version(test.injected, func() (*debug.BuildInfo, bool) {
return test.info, test.ok
})
if got != test.want {
t.Fatalf("version() = %q, want %q", got, test.want)
}
})
}
}

View File

@@ -0,0 +1,7 @@
// Package versioncontract holds the internal contract shared by runtime
// version reporting and the release builder.
package versioncontract
// InjectedVersionLinkerSymbol is the -X linker target used to set the runtime
// version. Keep it in sync with runtimeinfo.injectedVersion.
const InjectedVersionLinkerSymbol = "gitea.maximumdirect.net/eric/ReleaseKit/internal/runtimeinfo.injectedVersion"