From 51d2bd24dfd5839c5a341192b9da3f2ed20325ed Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 06:24:22 +0000 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. 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 +++++++++++++++- cmd/rtnetmon/main.go | 6 +++++- internal/cli/export_test.go | 2 +- internal/cli/root.go | 17 +++++++++-------- 5 files changed, 55 insertions(+), 21 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/cmd/rtnetmon/main.go b/cmd/rtnetmon/main.go index eac0fed..499c802 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 +// 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() }