Stamp the git tag or short commit into the binary (closes #10) #11

Merged
clawbot merged 1 commits from issue-10-version-stamp into next 2026-10-02 10:21:25 +02:00
6 changed files with 60 additions and 23 deletions
+25 -10
View File
@@ -1,19 +1,28 @@
# .dockerignore does NOT use .gitignore semantics. Docker matches with # .dockerignore does NOT use .gitignore semantics. Docker matches with
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross # moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
# `/` and an unprefixed pattern is anchored at the context root. Every # `/` and an unprefixed pattern is anchored at the context root. Every
# depth-independent pattern therefore needs `**/`; only genuinely # depth-independent pattern therefore needs `**/`, or `config/.env` and
# root-anchored entries go unprefixed. Never transplant these into # `certs/server.key` still ship while this file reads as solved. Only
# .gitignore, where `**/` is wrong. # genuinely root-anchored entries go unprefixed. Never transplant these
# into .gitignore, where `**/` is wrong.
# #
# Matching is case-sensitive, so secrets use character ranges rather # Matching is case-sensitive, so secrets use character ranges rather
# than an ALL-CAPS twin, which would still miss `Server.Key`. # than an ALL-CAPS twin, which would still miss `Server.Key`.
#
# Extend with this repo's own host-built artifacts, written anchored:
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
# deletes the package directory from the context.
# Excluding .git means `git describe` cannot run in any build stage and # .git is sent without its config. Without a VERSION build argument the
# fails quietly there; rtnetmon embeds no version, so this is safe. # stage that compiles runs `git describe --tags --always` on .git, which
.git # does not need .git/config; that file can hold a credential, such as a
# password in a remote URL or the token the CI checkout step stores there.
.git/config
# Agent scratch: one full checkout of the repo per in-flight agent. # Agent scratch: one full checkout of the repo per in-flight agent.
# Anchored because agents run at the repo root here. # Anchored because it occurs once where agents run at the repo root.
# KNOWN GAP: a repo running agents in subdirectories still ships
# `services/api/.claude/` and must add its own anchored entry.
.claude .claude
# This repo's own host-built artifacts, root-anchored so `bin/` is not # This repo's own host-built artifacts, root-anchored so `bin/` is not
@@ -22,12 +31,15 @@
/rtnetmon /rtnetmon
main.go.old main.go.old
# Environment files. # Environment files. `*.env` covers bare `.env` and the `prod.env`
# convention. Re-include a committed template with a negation if the
# build needs one: `!docs/example.env`.
**/*.[eE][nN][vV] **/*.[eE][nN][vV]
**/.[eE][nN][vV].* **/.[eE][nN][vV].*
**/.[eE][nN][vV][rR][cC] **/.[eE][nN][vV][rR][cC]
# Private keys and the bundles carrying them. # Private keys and the bundles carrying them. Public certificates
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
**/*.[pP][eE][mM] **/*.[pP][eE][mM]
**/*.[kK][eE][yY] **/*.[kK][eE][yY]
**/*.[pP]12 **/*.[pP]12
@@ -37,11 +49,14 @@ main.go.old
**/[iI][dD]_[eE][cC][dD][sS][aA] **/[iI][dD]_[eE][cC][dD][sS][aA]
**/[iI][dD]_[eE][dD]25519 **/[iI][dD]_[eE][dD]25519
# Dependencies: restored inside the image, never copied in.
**/node_modules
# OS metadata. # OS metadata.
**/.DS_Store **/.DS_Store
**/Thumbs.db **/Thumbs.db
# Editor state. # Editor state: never a build input, and it churns COPY.
**/*.swp **/*.swp
**/*.swo **/*.swo
**/*~ **/*~
+15 -1
View File
@@ -34,4 +34,18 @@ WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
RUN CGO_ENABLED=0 go build -trimpath -o /rtnetmon ./cmd/rtnetmon/ # The version stamped into the binary: the VERSION build argument when one
# is given, otherwise `git describe --tags --always` of the .git the build
# context carries: the tag on a tagged commit, tag-N-gHASH on a commit after
# one, the short commit when no tag is reachable. git ships in this base
# image. A context that carries .git and still yields no version fails the
# build. With neither, as from a source tarball, the binary reports dev.
ARG VERSION
RUN version="${VERSION:-$(git describe --tags --always)}"; \
if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \
[ "$version" = unknown ]; }; then \
echo "no version could be derived although the build context carries .git" >&2; \
exit 1; \
fi; \
CGO_ENABLED=0 go build -trimpath -ldflags="-X main.Version=${version:-dev}" \
-o /rtnetmon ./cmd/rtnetmon/
+5 -2
View File
@@ -5,6 +5,8 @@
# below are thin shims that call them. # below are thin shims that call them.
.DEFAULT_GOAL := check .DEFAULT_GOAL := check
VERSION ?= $(shell git describe --tags --always)
bootstrap: bootstrap:
@script/bootstrap @script/bootstrap
@@ -36,13 +38,14 @@ hooks:
@script/install-precommit @script/install-precommit
build: build:
CGO_ENABLED=0 go build -trimpath -o bin/rtnetmon ./cmd/rtnetmon CGO_ENABLED=0 go build -trimpath -ldflags "-X main.Version=$(VERSION)" \
-o bin/rtnetmon ./cmd/rtnetmon
run: build run: build
./bin/rtnetmon ./bin/rtnetmon
dev: dev:
go run ./cmd/rtnetmon go run -ldflags "-X main.Version=$(VERSION)" ./cmd/rtnetmon
deps: deps:
go mod download go mod download
+5 -1
View File
@@ -8,8 +8,12 @@ import (
"git.eeqj.de/sneak/rtnetmon/internal/cli" "git.eeqj.de/sneak/rtnetmon/internal/cli"
) )
// Version is the git tag or short commit, set at link time with -X by the
// Makefile and the Dockerfile. Builds that do not set it report dev.
var Version = "dev" //nolint:gochecknoglobals // set at link time with -X
func main() { func main() {
err := cli.Execute() err := cli.Execute(Version)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
+1 -1
View File
@@ -3,4 +3,4 @@ package cli
import "github.com/spf13/cobra" import "github.com/spf13/cobra"
// NewRootCmd exposes newRootCmd for external tests. // NewRootCmd exposes newRootCmd for external tests.
func NewRootCmd() *cobra.Command { return newRootCmd() } func NewRootCmd() *cobra.Command { return newRootCmd("dev") }
+9 -8
View File
@@ -51,8 +51,9 @@ func defaultTCPHosts() []string {
} }
} }
// newRootCmd builds the cobra root command with its flags bound. // newRootCmd builds the cobra root command with its flags bound. version is
func newRootCmd() *cobra.Command { // logged at startup.
func newRootCmd(version string) *cobra.Command {
cfg := &config{} cfg := &config{}
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "rtnetmon", Use: "rtnetmon",
@@ -60,7 +61,7 @@ func newRootCmd() *cobra.Command {
Long: `rtnetmon is a dual-interface network monitoring dashboard that provides Long: `rtnetmon is a dual-interface network monitoring dashboard that provides
real-time visibility into network health, packet loss, and latency.`, real-time visibility into network health, packet loss, and latency.`,
RunE: func(cmd *cobra.Command, _ []string) error { RunE: func(cmd *cobra.Command, _ []string) error {
return runMonitor(cmd, cfg) return runMonitor(cmd, cfg, version)
}, },
} }
registerFlags(cmd, cfg) registerFlags(cmd, cfg)
@@ -88,8 +89,8 @@ func registerFlags(cmd *cobra.Command, cfg *config) {
// runMonitor detects the interfaces to monitor, then constructs and runs the // runMonitor detects the interfaces to monitor, then constructs and runs the
// monitor from cfg. // monitor from cfg.
func runMonitor(cmd *cobra.Command, cfg *config) error { func runMonitor(cmd *cobra.Command, cfg *config, version string) error {
monitor.Logf(cfg.LogFile, "Starting rtnetmon") monitor.Logf(cfg.LogFile, "Starting rtnetmon %s", version)
specs, err := detectInterfaces(cmd, cfg) specs, err := detectInterfaces(cmd, cfg)
if err != nil { if err != nil {
@@ -169,7 +170,7 @@ func detectInterfaces(
return specs, nil return specs, nil
} }
// Execute builds the root command and runs it. // Execute builds the root command and runs it. version is logged at startup.
func Execute() error { func Execute(version string) error {
return newRootCmd().Execute() return newRootCmd(version).Execute()
} }