From 4f55fcbef5d640c1f26605ca96fcab2fcfd3ca23 Mon Sep 17 00:00:00 2001 From: djmil Date: Sat, 13 Jun 2026 20:32:24 +0000 Subject: [PATCH] build stamping - store build metadata (version, commit, date) - multi-binary build: `make build` builds all packages from ./cmd - devcontainer adds ./bin to the PATH by default --- .devcontainer/devcontainer.json | 4 +++- .vscode/tasks.json | 8 -------- CLAUDE.md | 1 + Makefile | 25 ++++++++++++++++--------- README.md | 19 +++++++------------ cmd/app/config.go | 3 +++ cmd/app/main.go | 6 ++++++ internal/buildinfo/buildinfo.go | 25 +++++++++++++++++++++++++ 8 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 internal/buildinfo/buildinfo.go diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index cd82449..c9ecb0c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -62,6 +62,8 @@ "forwardPorts": [8080], "remoteEnv": { - "GOPRIVATE": "gitea.djmil.dev" + "GOPRIVATE": "gitea.djmil.dev", + // Adds ./bin/ to PATH so built binaries are runnable by name after `make build`. + "PATH": "${containerWorkspaceFolder}/bin:${localEnv:PATH}" } } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index bfa924f..ac487d0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,14 +9,6 @@ "presentation": { "reveal": "always", "panel": "shared" }, "problemMatcher": "$go" }, - { - "label": "run", - "type": "shell", - "command": "make run", - "group": "none", - "presentation": { "reveal": "always", "panel": "dedicated" }, - "isBackground": false - }, { "label": "test", "type": "shell", diff --git a/CLAUDE.md b/CLAUDE.md index d0a1ee6..1884a74 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -153,3 +153,4 @@ make clean # remove bin/ - 2026-04-01 — Added make release: lists tags with no args; validates semver, runs test-race+lint+security, then tags+pushes. - 2026-04-23 — Documented result layering rule: pkg/ libraries only return Expect[T]; .Expect()/.Must() calls belong in application-layer code. - 2026-06-03 — pkg/logger v0.4.0: replaced NewDevelopment with NewCLI(level, debugFile); two-mode model (human text on TTY / JSON when piped); debug file mode; IsInteractive() helper. Established "form over mechanism" as core design principle. +- 2026-06-13 — Build stamping + multi-binary build: internal/buildinfo (Version, Commit, BuildTime injected via -ldflags); make build discovers all cmd/* via find and produces named binaries in ./bin/; make run replaced with make run/ pattern; devcontainer adds ./bin to PATH via ${containerWorkspaceFolder}. diff --git a/Makefile b/Makefile index fddf519..ab2a534 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,19 @@ -.PHONY: help init setup build run test test-race test-verbose lint lint-fix security docs release clean +.PHONY: help init setup build test test-race test-verbose lint lint-fix security docs release clean include tools.versions # ── Variables ────────────────────────────────────────────────────────────────── -BINARY_NAME := app -BINARY_PATH := ./bin/$(BINARY_NAME) -CMD_PATH := ./cmd/app +MODULE := $(shell go list -m) +VERSION ?= $(shell git describe --tags 2>/dev/null || echo dev) +COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown) +BUILT_AT := $(shell date -u +%Y-%m-%dT%H:%M:%SZ) +LDFLAGS := -ldflags "\ + -X $(MODULE)/internal/buildinfo.Version=$(VERSION) \ + -X $(MODULE)/internal/buildinfo.Commit=$(COMMIT) \ + -X $(MODULE)/internal/buildinfo.BuildTime=$(BUILT_AT)" + +CMDS := $(shell find cmd -mindepth 1 -maxdepth 1 -type d 2>/dev/null) +BINS := $(patsubst cmd/%,bin/%,$(CMDS)) # ── Default target ───────────────────────────────────────────────────────────── help: ## Show this help message @@ -34,12 +42,11 @@ tools: ## Install tool binaries to GOPATH/bin (versions from tools.versions) go install golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION) # ── Build ────────────────────────────────────────────────────────────────────── -build: ## Compile the binary to ./bin/ - go build -o $(BINARY_PATH) $(CMD_PATH) +build: $(BINS) ## Compile all cmd/* binaries to ./bin/ (stamped with version, commit, build time) -# ── Run ──────────────────────────────────────────────────────────────────────── -run: ## Run the application with default flags - go run $(CMD_PATH)/main.go +bin/%: cmd/% + @mkdir -p bin + go build $(LDFLAGS) -o $@ ./$< # ── Test ─────────────────────────────────────────────────────────────────────── test: ## Run all tests diff --git a/README.md b/README.md index ef0b5a4..501939b 100644 --- a/README.md +++ b/README.md @@ -75,10 +75,13 @@ make tools # (optional) install tool binaries to GOPATH/bin for IDE inte ### 3. Build and run ```bash -make build # compiles to ./bin/app -make run # go run with default flags +make build # compiles every cmd/* binary to ./bin/, stamped with git commit + build time ``` +Every subdirectory under `cmd/` becomes a named binary in `./bin/`. In the devcontainer, +`./bin/` is on `PATH` automatically, so after `make build` you can run `app` (or any other +binary) directly — with any flags — from any directory in the terminal. + --- ## Daily workflow @@ -89,7 +92,8 @@ make test-race # … with race detector make lint # go vet + golangci-lint make lint-fix # go fix + golangci-lint auto-fix make security # gosec + govulncheck -make release # list tags, or run full checks then tag+push (make release VERSION=v0.1.0) +make release # list tags +make release VERSION=v0.1.0 # run full checks then tag+push ``` > **Keyboard shortcut (VSCode):** `Ctrl+Shift+B` → build, `Ctrl+Shift+T` → test. @@ -130,15 +134,6 @@ Commands live in `.claude/commands/` and are available to anyone who clones the --- -## Releasing - -```bash -make release # list all existing tags -make release VERSION=v0.1.0 # run full checks, then tag and push -``` - ---- - ## Devcontainer Open this repo in VSCode and choose **"Reopen in Container"**. Run `make init` to diff --git a/cmd/app/config.go b/cmd/app/config.go index 7360c89..5f97d14 100644 --- a/cmd/app/config.go +++ b/cmd/app/config.go @@ -6,6 +6,7 @@ import ( // Config is the root configuration object. Add sub-structs as the app grows. type Config struct { + Version bool App AppConfig Logger LoggerConfig Greeter GreeterConfig @@ -38,6 +39,7 @@ type GreeterConfig struct { // cfg := config.parseArgs() // fmt.Println(cfg.App.Port) func parseArgs() *Config { + version := flag.Bool("version", false, "print version information and exit") name := flag.String("name", "Gopher", "application name") port := flag.Int("port", 8080, "listen port") env := flag.String("env", "dev", "environment: dev | staging | prod") @@ -47,6 +49,7 @@ func parseArgs() *Config { flag.Parse() return &Config{ + Version: *version, App: AppConfig{ Port: *port, Env: *env, diff --git a/cmd/app/main.go b/cmd/app/main.go index f6aa4aa..2defa6c 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -10,6 +10,7 @@ import ( "os" "path/filepath" + "gitea.djmil.dev/go/template/internal/buildinfo" "gitea.djmil.dev/go/template/internal/greeter" "gitea.djmil.dev/go/template/pkg/logger" "gitea.djmil.dev/go/template/pkg/result" @@ -44,6 +45,11 @@ func newApp(cfg *Config) *app { func main() { conf := parseArgs() + if conf.Version { + fmt.Println(buildinfo.String()) + return + } + app := newApp(conf) app.log.WithFields(map[string]any{ "app": filepath.Base(os.Args[0]), diff --git a/internal/buildinfo/buildinfo.go b/internal/buildinfo/buildinfo.go new file mode 100644 index 0000000..97acd08 --- /dev/null +++ b/internal/buildinfo/buildinfo.go @@ -0,0 +1,25 @@ +// Package buildinfo exposes build-time metadata injected via -ldflags. +// All vars default to safe fallbacks so the binary runs without stamping. +// +// Wire into the binary by passing LDFLAGS from the Makefile: +// +// go build -ldflags "-X .../internal/buildinfo.Commit=$(git rev-parse --short HEAD) \ +// -X .../internal/buildinfo.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ +// -X .../internal/buildinfo.Version=$(git describe --tags)" +package buildinfo + +import "fmt" + +// Version is the semver release tag (e.g. "v1.2.3") or "dev" for untagged builds. +var Version = "dev" + +// Commit is the short git SHA of the build (e.g. "a1b2c3d"). +var Commit = "unknown" + +// BuildTime is the UTC timestamp when the binary was compiled (RFC 3339). +var BuildTime = "unknown" + +// String returns a one-line summary suitable for a --version flag or startup log line. +func String() string { + return fmt.Sprintf("version=%s commit=%s built=%s", Version, Commit, BuildTime) +}