From bce8bdbb2e3345fa2e9045b67c5d7b0d99e77f6b Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 10:21:25 +0200 Subject: [PATCH] Stamp the git tag or short commit into the binary (closes #10) rtnetmon had no version. main.Version is now set at link time and logged in the first startup line. `make build` and `make dev` set it from `git describe --tags --always` unless VERSION is given. The Dockerfile takes it from the VERSION build argument when one is given, otherwise from `git describe --tags --always` of the .git the build context now carries, and fails the build if the context carries .git and no version comes out. .dockerignore follows the canonical copy: .git is sent, .git/config, which can hold a credential, is not. Model: opus-5-5 --- .dockerignore | 35 +++++++++++++++++++++++++---------- Dockerfile | 16 +++++++++++++++- Makefile | 7 +++++-- cmd/rtnetmon/main.go | 6 +++++- internal/cli/export_test.go | 2 +- internal/cli/root.go | 17 +++++++++-------- 6 files changed, 60 insertions(+), 23 deletions(-) diff --git a/.dockerignore b/.dockerignore index b04a35f..8032fd8 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,19 +1,28 @@ # .dockerignore does NOT use .gitignore semantics. Docker matches with # moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross # `/` and an unprefixed pattern is anchored at the context root. Every -# depth-independent pattern therefore needs `**/`; only genuinely -# root-anchored entries go unprefixed. Never transplant these into -# .gitignore, where `**/` is wrong. +# depth-independent pattern therefore needs `**/`, or `config/.env` and +# `certs/server.key` still ship while this file reads as solved. Only +# genuinely root-anchored entries go unprefixed. Never transplant these +# into .gitignore, where `**/` is wrong. # # Matching is case-sensitive, so secrets use character ranges rather # 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 -# fails quietly there; rtnetmon embeds no version, so this is safe. -.git +# .git is sent without its config. Without a VERSION build argument the +# stage that compiles runs `git describe --tags --always` on .git, which +# 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. -# 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 # This repo's own host-built artifacts, root-anchored so `bin/` is not @@ -22,12 +31,15 @@ /rtnetmon 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][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] **/*.[kK][eE][yY] **/*.[pP]12 @@ -37,11 +49,14 @@ main.go.old **/[iI][dD]_[eE][cC][dD][sS][aA] **/[iI][dD]_[eE][dD]25519 +# Dependencies: restored inside the image, never copied in. +**/node_modules + # OS metadata. **/.DS_Store **/Thumbs.db -# Editor state. +# Editor state: never a build input, and it churns COPY. **/*.swp **/*.swo **/*~ diff --git a/Dockerfile b/Dockerfile index 60b93dd..0b7fd8f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,4 +34,18 @@ WORKDIR /src COPY go.mod go.sum ./ RUN go mod download 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/ diff --git a/Makefile b/Makefile index cf34572..615bb94 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,8 @@ # below are thin shims that call them. .DEFAULT_GOAL := check +VERSION ?= $(shell git describe --tags --always) + bootstrap: @script/bootstrap @@ -36,13 +38,14 @@ hooks: @script/install-precommit 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 ./bin/rtnetmon dev: - go run ./cmd/rtnetmon + go run -ldflags "-X main.Version=$(VERSION)" ./cmd/rtnetmon deps: go mod download diff --git a/cmd/rtnetmon/main.go b/cmd/rtnetmon/main.go index eac0fed..5b7383d 100644 --- a/cmd/rtnetmon/main.go +++ b/cmd/rtnetmon/main.go @@ -8,8 +8,12 @@ import ( "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() { - err := cli.Execute() + err := cli.Execute(Version) if err != nil { log.Fatal(err) } diff --git a/internal/cli/export_test.go b/internal/cli/export_test.go index e768ab2..eac4a40 100644 --- a/internal/cli/export_test.go +++ b/internal/cli/export_test.go @@ -3,4 +3,4 @@ package cli import "github.com/spf13/cobra" // NewRootCmd exposes newRootCmd for external tests. -func NewRootCmd() *cobra.Command { return newRootCmd() } +func NewRootCmd() *cobra.Command { return newRootCmd("dev") } diff --git a/internal/cli/root.go b/internal/cli/root.go index 3a08014..29af2d5 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -51,8 +51,9 @@ func defaultTCPHosts() []string { } } -// newRootCmd builds the cobra root command with its flags bound. -func newRootCmd() *cobra.Command { +// newRootCmd builds the cobra root command with its flags bound. version is +// logged at startup. +func newRootCmd(version string) *cobra.Command { cfg := &config{} cmd := &cobra.Command{ Use: "rtnetmon", @@ -60,7 +61,7 @@ func newRootCmd() *cobra.Command { Long: `rtnetmon is a dual-interface network monitoring dashboard that provides real-time visibility into network health, packet loss, and latency.`, RunE: func(cmd *cobra.Command, _ []string) error { - return runMonitor(cmd, cfg) + return runMonitor(cmd, cfg, version) }, } 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 // monitor from cfg. -func runMonitor(cmd *cobra.Command, cfg *config) error { - monitor.Logf(cfg.LogFile, "Starting rtnetmon") +func runMonitor(cmd *cobra.Command, cfg *config, version string) error { + monitor.Logf(cfg.LogFile, "Starting rtnetmon %s", version) specs, err := detectInterfaces(cmd, cfg) if err != nil { @@ -169,7 +170,7 @@ func detectInterfaces( return specs, nil } -// Execute builds the root command and runs it. -func Execute() error { - return newRootCmd().Execute() +// Execute builds the root command and runs it. version is logged at startup. +func Execute(version string) error { + return newRootCmd(version).Execute() }