From f2fb3fc1f5d97a5fc583b6dc20bb85c6a99430b2 Mon Sep 17 00:00:00 2001 From: file-diff-agent Date: Wed, 6 May 2026 20:10:32 +0000 Subject: [PATCH 1/2] Initialize agent task: Add --version endpoint that returns detailed version info tag and commit Add --version endpoint that returns detailed version info tag and commit if possible. Add instructions how to build it with this info From 7c896f34d9b6b5c14cf05184b4a33670475f97fb Mon Sep 17 00:00:00 2001 From: file-diff-agent Date: Wed, 6 May 2026 20:16:00 +0000 Subject: [PATCH 2/2] Add version endpoint and build metadata --- Makefile | 11 +++- README.md | 38 +++++++++++++ cmd/arkiv-storaged/main.go | 17 +++++- integration/full_test.go | 78 +++++++++++++++++++++++-- version/http.go | 24 ++++++++ version/http_test.go | 55 ++++++++++++++++++ version/version.go | 114 +++++++++++++++++++++++++++++++++++++ version/version_test.go | 35 ++++++++++++ 8 files changed, 363 insertions(+), 9 deletions(-) create mode 100644 version/http.go create mode 100644 version/http_test.go create mode 100644 version/version.go create mode 100644 version/version_test.go diff --git a/Makefile b/Makefile index 0088b05..b434c05 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,21 @@ BIN := arkiv-storaged CMD := ./cmd/arkiv-storaged OUT := ./bin/$(BIN) +VERSION_PKG := github.com/Arkiv-Network/arkiv-storage-service/version + +TAG ?= $(shell git describe --tags --abbrev=0 --always 2>/dev/null || echo unknown) +COMMIT ?= $(shell git rev-parse HEAD 2>/dev/null || echo unknown) +DIRTY ?= $(shell test -z "$$(git status --porcelain 2>/dev/null)" && echo false || echo true) +BUILD_TIME ?= $(shell date -u '+%Y-%m-%dT%H:%M:%SZ') +LDFLAGS := -X '$(VERSION_PKG).Tag=$(TAG)' -X '$(VERSION_PKG).Commit=$(COMMIT)' -X '$(VERSION_PKG).Dirty=$(DIRTY)' -X '$(VERSION_PKG).BuildTime=$(BUILD_TIME)' .PHONY: build install test lint clean build: - go build -o $(OUT) $(CMD) + go build -ldflags "$(LDFLAGS)" -o $(OUT) $(CMD) install: - go install $(CMD) + go install -ldflags "$(LDFLAGS)" $(CMD) test: build go test ./... diff --git a/README.md b/README.md index 8862a07..e9144d2 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,33 @@ make build # writes to ./bin/arkiv-storaged make install # installs to $GOPATH/bin ``` +`make build` and `make install` embed version metadata from the current git +checkout when available: + +- `tag` from `git describe --tags --abbrev=0 --always` +- `commit` from `git rev-parse HEAD` +- `dirty` from whether the worktree has uncommitted changes +- `buildTime` from the current UTC time + +For release or CI builds, the same fields can be set explicitly: + +```sh +go build \ + -ldflags "\ + -X github.com/Arkiv-Network/arkiv-storage-service/version.Tag=v0.1.0 \ + -X github.com/Arkiv-Network/arkiv-storage-service/version.Commit=$(git rev-parse HEAD) \ + -X github.com/Arkiv-Network/arkiv-storage-service/version.Dirty=false \ + -X github.com/Arkiv-Network/arkiv-storage-service/version.BuildTime=$(date -u '+%Y-%m-%dT%H:%M:%SZ')" \ + -o ./bin/arkiv-storaged \ + ./cmd/arkiv-storaged +``` + +Print the embedded version information without starting the daemon: + +```sh +arkiv-storaged --version +``` + Run it: ```sh @@ -35,6 +62,7 @@ Flags: -chain-addr listen address for the chain ingest server (default 127.0.0.1:2704) -query-addr listen address for the query server (default 127.0.0.1:2705) -data-dir path to the data directory (default ~/.arkiv-storaged) + -version print build version information and exit ``` ### Configuration file @@ -60,6 +88,16 @@ The service exposes two HTTP JSON-RPC 2.0 servers: - `arkiv_getEntityByAddress` — fetch a single entity by address - `arkiv_getEntityCount` — total number of live entities at the head +Both listeners also expose a plain HTTP version endpoint: + +```sh +curl http://127.0.0.1:2704/version +curl http://127.0.0.1:2705/version +``` + +The response is JSON and includes the embedded tag, full commit, short commit, +dirty flag, build time, Go version, and any available Go VCS metadata. + ## Development ```sh diff --git a/cmd/arkiv-storaged/main.go b/cmd/arkiv-storaged/main.go index 136a123..8e29035 100644 --- a/cmd/arkiv-storaged/main.go +++ b/cmd/arkiv-storaged/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/json" "errors" "flag" "fmt" @@ -15,6 +16,7 @@ import ( "github.com/Arkiv-Network/arkiv-storage-service/chain" "github.com/Arkiv-Network/arkiv-storage-service/query" "github.com/Arkiv-Network/arkiv-storage-service/store" + "github.com/Arkiv-Network/arkiv-storage-service/version" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/ethdb/pebble" "gopkg.in/yaml.v2" @@ -61,6 +63,7 @@ func main() { chainAddr := flag.String("chain-addr", "127.0.0.1:2704", "address for the chain ingest JSON-RPC server (arkiv-op-reth → storaged)") queryAddr := flag.String("query-addr", "127.0.0.1:2705", "address for the query JSON-RPC server (SDK → storaged)") dataDir := flag.String("data-dir", defaultDataDir(), "path to the data directory (config.yaml read here; PebbleDB opened at /db)") + showVersion := flag.Bool("version", false, "print build version information and exit") flag.Usage = func() { fmt.Fprintf(os.Stderr, `arkiv-storaged — Arkiv entity storage daemon @@ -79,6 +82,16 @@ Flags: flag.Parse() + if *showVersion { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + if err := enc.Encode(version.Current()); err != nil { + fmt.Fprintf(os.Stderr, "print version: %v\n", err) + os.Exit(1) + } + return + } + log := slog.New(slog.NewTextHandler(os.Stderr, nil)) // Load config file from the data dir resolved so far. @@ -125,8 +138,8 @@ Flags: os.Exit(1) } - chainHTTP := &http.Server{Addr: *chainAddr, Handler: chainSrv} - queryHTTP := &http.Server{Addr: *queryAddr, Handler: querySrv} + chainHTTP := &http.Server{Addr: *chainAddr, Handler: version.Handler(chainSrv)} + queryHTTP := &http.Server{Addr: *queryAddr, Handler: version.Handler(querySrv)} // Start both servers. go func() { diff --git a/integration/full_test.go b/integration/full_test.go index 2af4df3..ade797a 100644 --- a/integration/full_test.go +++ b/integration/full_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net" + "net/http" "os" "os/exec" "sort" @@ -15,6 +16,7 @@ import ( "github.com/Arkiv-Network/arkiv-storage-service/chain" "github.com/Arkiv-Network/arkiv-storage-service/query" "github.com/Arkiv-Network/arkiv-storage-service/types" + "github.com/Arkiv-Network/arkiv-storage-service/version" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/rpc" @@ -22,6 +24,12 @@ import ( var binaryPath string +const ( + testVersionTag = "v-test" + testVersionCommit = "0123456789abcdef0123456789abcdef01234567" + testVersionBuildTime = "2026-05-06T20:30:00Z" +) + func TestMain(m *testing.M) { tmp, err := os.CreateTemp("", "arkiv-storaged-*") if err != nil { @@ -31,7 +39,13 @@ func TestMain(m *testing.M) { _ = tmp.Close() binaryPath = tmp.Name() - build := exec.Command("go", "build", "-o", binaryPath, "../cmd/arkiv-storaged") + ldflags := fmt.Sprintf( + "-X github.com/Arkiv-Network/arkiv-storage-service/version.Tag=%s -X github.com/Arkiv-Network/arkiv-storage-service/version.Commit=%s -X github.com/Arkiv-Network/arkiv-storage-service/version.Dirty=true -X github.com/Arkiv-Network/arkiv-storage-service/version.BuildTime=%s", + testVersionTag, + testVersionCommit, + testVersionBuildTime, + ) + build := exec.Command("go", "build", "-ldflags", ldflags, "-o", binaryPath, "../cmd/arkiv-storaged") build.Stdout = os.Stderr build.Stderr = os.Stderr if err := build.Run(); err != nil { @@ -56,7 +70,6 @@ var ( iAddr2 = common.Address(iKey2[:20]) iAddr3 = common.Address(iKey3[:20]) - iOwner1 = common.HexToAddress("0xaaaa000000000000000000000000000000000001") iOwner2 = common.HexToAddress("0xaaaa000000000000000000000000000000000002") iOwner3 = common.HexToAddress("0xaaaa000000000000000000000000000000000003") @@ -150,8 +163,10 @@ func strAttr(name, val string) types.Attribute { // ----- test environment ----- type testEnv struct { - c *rpc.Client // chain client - q *rpc.Client // query client + c *rpc.Client // chain client + q *rpc.Client // query client + chainAddr string + queryAddr string } // freePort returns a free TCP port on localhost. @@ -221,7 +236,60 @@ func newTestEnv(t *testing.T) *testEnv { } t.Cleanup(queryClient.Close) - return &testEnv{c: chainClient, q: queryClient} + return &testEnv{c: chainClient, q: queryClient, chainAddr: chainAddr, queryAddr: queryAddr} +} + +func assertVersionInfo(t *testing.T, info version.Info) { + t.Helper() + if info.Tag != testVersionTag { + t.Fatalf("tag = %q, want %q", info.Tag, testVersionTag) + } + if info.Commit != testVersionCommit { + t.Fatalf("commit = %q, want %q", info.Commit, testVersionCommit) + } + if info.CommitShort != testVersionCommit[:12] { + t.Fatalf("commitShort = %q, want %q", info.CommitShort, testVersionCommit[:12]) + } + if !info.Dirty { + t.Fatal("dirty = false, want true") + } + if info.BuildTime != testVersionBuildTime { + t.Fatalf("buildTime = %q, want %q", info.BuildTime, testVersionBuildTime) + } + if info.GoVersion == "" { + t.Fatal("goVersion is empty") + } +} + +func TestVersionFlag(t *testing.T) { + out, err := exec.Command(binaryPath, "--version").Output() + if err != nil { + t.Fatalf("arkiv-storaged --version: %v", err) + } + var info version.Info + if err := json.Unmarshal(out, &info); err != nil { + t.Fatalf("decode version output: %v\n%s", err, out) + } + assertVersionInfo(t, info) +} + +func TestVersionEndpoint(t *testing.T) { + env := newTestEnv(t) + for _, addr := range []string{env.chainAddr, env.queryAddr} { + resp, err := http.Get("http://" + addr + "/version") + if err != nil { + t.Fatalf("GET /version from %s: %v", addr, err) + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + t.Fatalf("GET /version from %s status = %d, want %d", addr, resp.StatusCode, http.StatusOK) + } + var info version.Info + if err := json.NewDecoder(resp.Body).Decode(&info); err != nil { + t.Fatalf("decode /version from %s: %v", addr, err) + } + assertVersionInfo(t, info) + } } func (e *testEnv) commit(t *testing.T, blocks ...types.ArkivBlock) { diff --git a/version/http.go b/version/http.go new file mode 100644 index 0000000..ee5b877 --- /dev/null +++ b/version/http.go @@ -0,0 +1,24 @@ +package version + +import ( + "encoding/json" + "net/http" +) + +// Handler serves GET /version and delegates all other requests to next. +func Handler(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/version" { + next.ServeHTTP(w, r) + return + } + if r.Method != http.MethodGet { + w.Header().Set("Allow", http.MethodGet) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(Current()) + }) +} diff --git a/version/http_test.go b/version/http_test.go new file mode 100644 index 0000000..14d0169 --- /dev/null +++ b/version/http_test.go @@ -0,0 +1,55 @@ +package version + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHandlerServesVersion(t *testing.T) { + oldTag := Tag + t.Cleanup(func() { Tag = oldTag }) + Tag = "v-test" + + handler := Handler(http.NotFoundHandler()) + req := httptest.NewRequest(http.MethodGet, "/version", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusOK) + } + + var info Info + if err := json.NewDecoder(rec.Body).Decode(&info); err != nil { + t.Fatalf("decode response: %v", err) + } + if info.Tag != "v-test" { + t.Fatalf("tag = %q, want %q", info.Tag, "v-test") + } +} + +func TestHandlerDelegatesOtherPaths(t *testing.T) { + handler := Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusAccepted) + })) + req := httptest.NewRequest(http.MethodPost, "/", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + if rec.Code != http.StatusAccepted { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusAccepted) + } +} + +func TestHandlerRejectsNonGetVersionRequests(t *testing.T) { + handler := Handler(http.NotFoundHandler()) + req := httptest.NewRequest(http.MethodPost, "/version", nil) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, req) + if rec.Code != http.StatusMethodNotAllowed { + t.Fatalf("status = %d, want %d", rec.Code, http.StatusMethodNotAllowed) + } +} diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..0e09966 --- /dev/null +++ b/version/version.go @@ -0,0 +1,114 @@ +package version + +import ( + "runtime" + "runtime/debug" + "strings" +) + +const unknown = "unknown" + +// These variables are intended to be populated by -ldflags at build time. +var ( + Tag = unknown + Commit = unknown + Dirty = unknown + BuildTime = unknown +) + +// Info describes the build running this process. +type Info struct { + Tag string `json:"tag"` + Commit string `json:"commit"` + CommitShort string `json:"commitShort,omitempty"` + Dirty bool `json:"dirty"` + BuildTime string `json:"buildTime,omitempty"` + GoVersion string `json:"goVersion"` + ModuleVersion string `json:"moduleVersion,omitempty"` + VCSRevision string `json:"vcsRevision,omitempty"` + VCSTime string `json:"vcsTime,omitempty"` + VCSModified *bool `json:"vcsModified,omitempty"` +} + +// Current returns detailed version information, using ldflags first and Go's +// embedded VCS metadata as a fallback when available. +func Current() Info { + info := Info{ + Tag: clean(Tag), + Commit: clean(Commit), + BuildTime: clean(BuildTime), + GoVersion: runtime.Version(), + } + + if buildInfo, ok := debug.ReadBuildInfo(); ok { + if buildInfo.Main.Version != "" && buildInfo.Main.Version != "(devel)" { + info.ModuleVersion = buildInfo.Main.Version + if info.Tag == unknown { + info.Tag = buildInfo.Main.Version + } + } + + for _, setting := range buildInfo.Settings { + switch setting.Key { + case "vcs.revision": + info.VCSRevision = setting.Value + if info.Commit == unknown { + info.Commit = setting.Value + } + case "vcs.time": + info.VCSTime = setting.Value + if info.BuildTime == unknown { + info.BuildTime = setting.Value + } + case "vcs.modified": + if modified, ok := parseBool(setting.Value); ok { + info.VCSModified = &modified + if _, dirtyKnown := parseBool(Dirty); !dirtyKnown { + info.Dirty = modified + } + } + } + } + } + + if dirty, ok := parseBool(Dirty); ok { + info.Dirty = dirty + } + if info.CommitShort = shortCommit(info.Commit); info.CommitShort == unknown { + info.CommitShort = "" + } + if info.BuildTime == unknown { + info.BuildTime = "" + } + return info +} + +func clean(value string) string { + value = strings.TrimSpace(value) + if value == "" { + return unknown + } + return value +} + +func parseBool(value string) (bool, bool) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "true", "1", "yes", "dirty": + return true, true + case "false", "0", "no", "clean": + return false, true + default: + return false, false + } +} + +func shortCommit(commit string) string { + commit = clean(commit) + if commit == unknown { + return unknown + } + if len(commit) <= 12 { + return commit + } + return commit[:12] +} diff --git a/version/version_test.go b/version/version_test.go new file mode 100644 index 0000000..7b2cd94 --- /dev/null +++ b/version/version_test.go @@ -0,0 +1,35 @@ +package version + +import "testing" + +func TestCurrentUsesLdflagsAndShortCommit(t *testing.T) { + oldTag, oldCommit, oldDirty, oldBuildTime := Tag, Commit, Dirty, BuildTime + t.Cleanup(func() { + Tag, Commit, Dirty, BuildTime = oldTag, oldCommit, oldDirty, oldBuildTime + }) + + Tag = "v1.2.3" + Commit = "0123456789abcdef" + Dirty = "true" + BuildTime = "2026-05-06T20:30:00Z" + + info := Current() + if info.Tag != "v1.2.3" { + t.Fatalf("tag = %q, want %q", info.Tag, "v1.2.3") + } + if info.Commit != "0123456789abcdef" { + t.Fatalf("commit = %q, want %q", info.Commit, "0123456789abcdef") + } + if info.CommitShort != "0123456789ab" { + t.Fatalf("commitShort = %q, want %q", info.CommitShort, "0123456789ab") + } + if !info.Dirty { + t.Fatal("dirty = false, want true") + } + if info.BuildTime != "2026-05-06T20:30:00Z" { + t.Fatalf("buildTime = %q, want %q", info.BuildTime, "2026-05-06T20:30:00Z") + } + if info.GoVersion == "" { + t.Fatal("goVersion is empty") + } +}