From 5d182b5fd6c4728b5917e9170f9081af157159eb Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 2 Oct 2026 00:59:44 +0000 Subject: [PATCH] docker: a plain docker build . stamps the git version (closes #210) A plain `docker build .`, which is how upaas builds, stamped `dev`: `.dockerignore` left out `.git` and the builder declared `ARG VERSION=dev`. `.dockerignore` now sends `.git` without `.git/config`, which can hold a credential, and lists no tracked file, which git would count as deleted. `ARG VERSION` has no default. The Makefile takes a non-empty `VERSION` from the command line or the environment, so a build arg still wins; otherwise `git describe` runs in the builder, which trusts the checkout whoever owns it, as a context sent as a tar archive keeps its owners. A new `make version` prints the version; the build fails when the context carries `.git` and it comes out empty, `dev` or `unknown`. Model: opus-5-5 --- .dockerignore | 14 +++++++------- Dockerfile | 30 +++++++++++++++++++++++++----- Dockerfile.fmt | 5 ++--- Makefile | 16 ++++++++++++---- README.md | 26 ++++++++++++++++++++------ TODO.md | 2 ++ script/docker | 4 ++-- 7 files changed, 70 insertions(+), 27 deletions(-) diff --git a/.dockerignore b/.dockerignore index 0e93e56..136b403 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,9 +1,9 @@ -.git/ +# .git is sent, without its config: the builder stage derives the version it +# stamps into the binary from it, and `git describe` does not need the config, +# which can hold a credential (a password in the remote URL, a CI token). No +# tracked file may be listed here: git in the build would see it as deleted +# and mark the version -dirty, and an excluded .md would silently drop out of +# the prettier check in Dockerfile.fmt. +.git/config bin/ node_modules/ -# No .md may be excluded: Dockerfile.fmt checks every document with -# prettier, and an exclusion here would drop a file from that check while -# prettier still reports every file it was handed clean. -LICENSE -.editorconfig -.gitignore diff --git a/Dockerfile b/Dockerfile index c289c07..8f419c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,6 +24,11 @@ FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4 RUN apk add --no-cache git make gcc musl-dev binutils-gold +# A build context sent as a tar archive keeps its files' owners, and git +# refuses to read a checkout owned by another user. Trust this one +# whoever owns it. +RUN git config --system --add safe.directory /src + # Force BuildKit to run the lint stage before proceeding COPY --from=lint /src/go.sum /dev/null @@ -36,11 +41,26 @@ COPY . . # Run the tests - build fails if any test fails RUN make test -# Build the binary. .dockerignore leaves out .git, so `git describe` in -# the Makefile cannot find the version here: script/docker passes it as -# --build-arg VERSION, and a build that passes none reports `dev`. -ARG VERSION=dev -RUN make build VERSION="${VERSION}" +# Version stamped into the binary: the VERSION build arg when one is +# given and not empty (script/docker passes one), otherwise what +# `git describe` says of the .git in the build context, so a plain +# `docker build .` of a clone stamps its tag or short commit. The build +# arg reaches make through the environment. +ARG VERSION + +# A context that carries .git, as a directory or as a file, must yield a +# real version: one that is empty, `dev` or `unknown` cannot be traced +# back to a commit. +RUN version="$(make version)"; \ + if [ -e .git ]; then \ + case "$version" in \ + "" | dev | unknown) \ + echo "version is \"$version\" although the build context carries .git" >&2; \ + exit 1 ;; \ + esac; \ + fi + +RUN make build # Runtime stage # alpine 3.21, 2026-02-28 diff --git a/Dockerfile.fmt b/Dockerfile.fmt index be7d0ba..9837231 100644 --- a/Dockerfile.fmt +++ b/Dockerfile.fmt @@ -30,9 +30,8 @@ COPY . . # --config, not discovery: a .prettierrc that failed to arrive would # otherwise leave prettier on its defaults, where proseWrap is "preserve" # and every wrap this check exists to enforce passes. Missing the file is -# a hard error instead. --no-editorconfig for the same reason in reverse: -# .editorconfig is not in the build context, so honouring it here and on -# a developer's machine would be two different answers. +# a hard error instead. --no-editorconfig so that .prettierrc alone sets +# the style. RUN prettier --config .prettierrc --no-editorconfig --check "**/*.md" # Write path. Not a check: script/fmt builds this and takes the files. diff --git a/Makefile b/Makefile index 5d36bc7..48a01cf 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,13 @@ -.PHONY: all bootstrap setup build lint fmt fmt-check test check clean hooks docker +.PHONY: all bootstrap setup build version lint fmt fmt-check test check clean hooks docker BINARY := dnswatcher -# `make build VERSION=...` overrides this; the Dockerfile does so, as the -# image has no .git to describe. -VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +# VERSION given on the command line (`make build VERSION=...`) or in the +# environment, which is how the Dockerfile's VERSION build arg arrives, +# wins over what `git describe` says of this checkout. An empty one counts +# as not given; `override` is what replaces an empty command-line value. +ifeq ($(VERSION),) +override VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") +endif LDFLAGS := -X main.Version=$(VERSION) # Standard targets are thin shims; the implementations live in script/ @@ -21,6 +25,10 @@ setup: build: go build -ldflags "$(LDFLAGS)" -o bin/$(BINARY) ./cmd/dnswatcher +# Prints the version `make build` stamps; the Dockerfile checks it. +version: + @echo "$(VERSION)" + test: @script/test diff --git a/README.md b/README.md index 94bd31d..4f14c1d 100644 --- a/README.md +++ b/README.md @@ -574,6 +574,7 @@ provide: ```sh make build # Build binary to bin/dnswatcher +make version # Print the version make build stamps make test # Run tests with race detector make lint # Run golangci-lint in Docker (requires docker) make fmt # Format code and Markdown (requires docker) @@ -584,13 +585,26 @@ make clean # Remove build artifacts ### Build-Time Variables `make build` sets the version with `-ldflags "-X main.Version=..."`, taking it -from `git describe --tags --always --dirty`, or from `VERSION` when given on the -command line (`make build VERSION=1.2.3`). The version appears in the startup -log and in the health check response. +from `VERSION` when given on the command line (`make build VERSION=1.2.3`) or in +the environment, otherwise from `git describe --tags --always --dirty`, and +`dev` without git metadata. An empty `VERSION` counts as not given. The version +appears in the startup log and in the health check response. -The Docker image has no `.git`, so the `Dockerfile` takes the version as -`--build-arg VERSION`. `make docker` passes it; a plain `docker build` passes -none, and that image reports `dev`. +The image takes it the same way, from the `.git` the build context carries, so a +plain `docker build .` of a clone stamps the commit it was built from; a clone +without tags stamps the short commit. A clone made with `--depth 1` carries at +most a tag on its own commit, so such a clone of an untagged commit stamps the +short commit. In a build from a directory, `.dockerignore` keeps out +`.git/config`, which `git describe` does not need and which can hold a +credential. Docker does not apply `.dockerignore` to a context sent as a tar +archive, as upaas sends it, so that context carries `.git/config` into the +build. It also keeps its files' owners, so git in the build trusts the checkout +whoever owns it. A non-empty `--build-arg VERSION=...` takes precedence; +`make docker` passes the version `git describe` gives on the host. The build +fails when the context carries `.git`, as a directory or as a file, and the +version comes out empty, `dev` or `unknown`. `.dockerignore` must list no +tracked file: git in the build would see it as deleted and mark the version +`-dirty`. --- diff --git a/TODO.md b/TODO.md index 6ecc0d6..596ca42 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149 # Completed Steps +- 2026-10-02: a plain `docker build .` of a clone stamps its tag or short + commit, not `dev`: the build context now carries `.git` (closes #210). - 2026-10-02: a query a server refuses is not resent asking for recursion, and every root server refusing is reported as DNS interception (closes #206). - 2026-10-02: a push to a branch cancels that branch's older CI run, and the diff --git a/script/docker b/script/docker index 93d2408..9ce387c 100755 --- a/script/docker +++ b/script/docker @@ -14,8 +14,8 @@ main() { cd "$ROOT" # Own line: a failing command substitution inside an argument does # not trip `set -e`, so the inline form degrades silently to an - # empty constant. VERSION is computed here because .dockerignore - # excludes .git, so `git describe` in a build stage cannot find it. + # empty constant. The VERSION build arg takes precedence over what + # the build would derive from the .git in its context. version="$(git describe --tags --always --dirty 2>/dev/null || true)" [ -n "$version" ] || version="unknown" docker build --no-cache-filter=lint,builder \