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
This commit is contained in:
parent
3915cf2dda
commit
4f55fcbef5
@ -62,6 +62,8 @@
|
|||||||
"forwardPorts": [8080],
|
"forwardPorts": [8080],
|
||||||
|
|
||||||
"remoteEnv": {
|
"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}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
.vscode/tasks.json
vendored
8
.vscode/tasks.json
vendored
@ -9,14 +9,6 @@
|
|||||||
"presentation": { "reveal": "always", "panel": "shared" },
|
"presentation": { "reveal": "always", "panel": "shared" },
|
||||||
"problemMatcher": "$go"
|
"problemMatcher": "$go"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "run",
|
|
||||||
"type": "shell",
|
|
||||||
"command": "make run",
|
|
||||||
"group": "none",
|
|
||||||
"presentation": { "reveal": "always", "panel": "dedicated" },
|
|
||||||
"isBackground": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "test",
|
"label": "test",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
|
|||||||
@ -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-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-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-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/<name> pattern; devcontainer adds ./bin to PATH via ${containerWorkspaceFolder}.
|
||||||
|
|||||||
25
Makefile
25
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
|
include tools.versions
|
||||||
|
|
||||||
# ── Variables ──────────────────────────────────────────────────────────────────
|
# ── Variables ──────────────────────────────────────────────────────────────────
|
||||||
BINARY_NAME := app
|
MODULE := $(shell go list -m)
|
||||||
BINARY_PATH := ./bin/$(BINARY_NAME)
|
VERSION ?= $(shell git describe --tags 2>/dev/null || echo dev)
|
||||||
CMD_PATH := ./cmd/app
|
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 ─────────────────────────────────────────────────────────────
|
# ── Default target ─────────────────────────────────────────────────────────────
|
||||||
help: ## Show this help message
|
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)
|
go install golang.org/x/pkgsite/cmd/pkgsite@$(PKGSITE_VERSION)
|
||||||
|
|
||||||
# ── Build ──────────────────────────────────────────────────────────────────────
|
# ── Build ──────────────────────────────────────────────────────────────────────
|
||||||
build: ## Compile the binary to ./bin/
|
build: $(BINS) ## Compile all cmd/* binaries to ./bin/ (stamped with version, commit, build time)
|
||||||
go build -o $(BINARY_PATH) $(CMD_PATH)
|
|
||||||
|
|
||||||
# ── Run ────────────────────────────────────────────────────────────────────────
|
bin/%: cmd/%
|
||||||
run: ## Run the application with default flags
|
@mkdir -p bin
|
||||||
go run $(CMD_PATH)/main.go
|
go build $(LDFLAGS) -o $@ ./$<
|
||||||
|
|
||||||
# ── Test ───────────────────────────────────────────────────────────────────────
|
# ── Test ───────────────────────────────────────────────────────────────────────
|
||||||
test: ## Run all tests
|
test: ## Run all tests
|
||||||
|
|||||||
19
README.md
19
README.md
@ -75,10 +75,13 @@ make tools # (optional) install tool binaries to GOPATH/bin for IDE inte
|
|||||||
### 3. Build and run
|
### 3. Build and run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
make build # compiles to ./bin/app
|
make build # compiles every cmd/* binary to ./bin/, stamped with git commit + build time
|
||||||
make run # go run with default flags
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
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
|
## Daily workflow
|
||||||
@ -89,7 +92,8 @@ make test-race # … with race detector
|
|||||||
make lint # go vet + golangci-lint
|
make lint # go vet + golangci-lint
|
||||||
make lint-fix # go fix + golangci-lint auto-fix
|
make lint-fix # go fix + golangci-lint auto-fix
|
||||||
make security # gosec + govulncheck
|
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.
|
> **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
|
## Devcontainer
|
||||||
|
|
||||||
Open this repo in VSCode and choose **"Reopen in Container"**. Run `make init` to
|
Open this repo in VSCode and choose **"Reopen in Container"**. Run `make init` to
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
// Config is the root configuration object. Add sub-structs as the app grows.
|
// Config is the root configuration object. Add sub-structs as the app grows.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
Version bool
|
||||||
App AppConfig
|
App AppConfig
|
||||||
Logger LoggerConfig
|
Logger LoggerConfig
|
||||||
Greeter GreeterConfig
|
Greeter GreeterConfig
|
||||||
@ -38,6 +39,7 @@ type GreeterConfig struct {
|
|||||||
// cfg := config.parseArgs()
|
// cfg := config.parseArgs()
|
||||||
// fmt.Println(cfg.App.Port)
|
// fmt.Println(cfg.App.Port)
|
||||||
func parseArgs() *Config {
|
func parseArgs() *Config {
|
||||||
|
version := flag.Bool("version", false, "print version information and exit")
|
||||||
name := flag.String("name", "Gopher", "application name")
|
name := flag.String("name", "Gopher", "application name")
|
||||||
port := flag.Int("port", 8080, "listen port")
|
port := flag.Int("port", 8080, "listen port")
|
||||||
env := flag.String("env", "dev", "environment: dev | staging | prod")
|
env := flag.String("env", "dev", "environment: dev | staging | prod")
|
||||||
@ -47,6 +49,7 @@ func parseArgs() *Config {
|
|||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
|
Version: *version,
|
||||||
App: AppConfig{
|
App: AppConfig{
|
||||||
Port: *port,
|
Port: *port,
|
||||||
Env: *env,
|
Env: *env,
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"gitea.djmil.dev/go/template/internal/buildinfo"
|
||||||
"gitea.djmil.dev/go/template/internal/greeter"
|
"gitea.djmil.dev/go/template/internal/greeter"
|
||||||
"gitea.djmil.dev/go/template/pkg/logger"
|
"gitea.djmil.dev/go/template/pkg/logger"
|
||||||
"gitea.djmil.dev/go/template/pkg/result"
|
"gitea.djmil.dev/go/template/pkg/result"
|
||||||
@ -44,6 +45,11 @@ func newApp(cfg *Config) *app {
|
|||||||
func main() {
|
func main() {
|
||||||
conf := parseArgs()
|
conf := parseArgs()
|
||||||
|
|
||||||
|
if conf.Version {
|
||||||
|
fmt.Println(buildinfo.String())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
app := newApp(conf)
|
app := newApp(conf)
|
||||||
app.log.WithFields(map[string]any{
|
app.log.WithFields(map[string]any{
|
||||||
"app": filepath.Base(os.Args[0]),
|
"app": filepath.Base(os.Args[0]),
|
||||||
|
|||||||
25
internal/buildinfo/buildinfo.go
Normal file
25
internal/buildinfo/buildinfo.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user