diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f7d1c5e --- /dev/null +++ b/.dockerignore @@ -0,0 +1,65 @@ +# .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 `**/`, 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. + +# .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 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 + +# 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. Public certificates +# (*.crt, *.cer) are deliberately absent: they are legitimate inputs. +**/*.[pP][eE][mM] +**/*.[kK][eE][yY] +**/*.[pP]12 +**/*.[pP][fF][xX] +**/[iI][dD]_[rR][sS][aA] +**/[iI][dD]_[dD][sS][aA] +**/[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: never a build input, and it churns COPY. +**/*.swp +**/*.swo +**/*~ +**/*.bak +**/.idea +**/.vscode +**/*.sublime-* + +# This repo's host-built artifacts: `make` and `make test-coverage`. +/bsdaily +/coverage.out +/coverage.html diff --git a/Dockerfile b/Dockerfile index 6104a62..bb1f677 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,10 +24,9 @@ FROM golang:1.26.4-alpine@sha256:3ad57304ad93bbec8548a0437ad9e06a455660655d9af01 # Depend on lint stage passing COPY --from=lint /src/go.sum /dev/null -ARG VERSION=dev - -# Install build deps plus the sqlite3 and zstd CLIs the tests/tool shell out to -RUN apk add --no-cache make build-base sqlite zstd +# Install build deps plus the sqlite3 and zstd CLIs the tests/tool shell out +# to, and git, which the build step below derives the version with +RUN apk add --no-cache make build-base sqlite zstd git WORKDIR /src @@ -41,8 +40,22 @@ COPY . . # Run tests RUN make test -# Build (pure Go, no CGO required since we use modernc.org/sqlite) -RUN CGO_ENABLED=0 go build -o /bsdaily ./cmd/bsdaily +# Build (pure Go, no CGO required since we use modernc.org/sqlite). +# 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. 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 -ldflags="-X main.Version=${version:-dev}" \ + -o /bsdaily ./cmd/bsdaily # Runtime stage # alpine:3.21 diff --git a/Makefile b/Makefile index 04ff721..aff96bb 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ .PHONY: all bootstrap setup check test lint fmt fmt-check build clean deps test-coverage test-integration install release release-snapshot docker hooks -# Version number -VERSION := 0.1.0-dev +# Stamped into the binary: the same `git describe` the Dockerfile runs, so a +# host build reports the same version as the image; dev when it yields nothing. +VERSION ?= $(shell git describe --tags --always) # Default target all: bsdaily @@ -36,7 +37,7 @@ lint: # Build binary (pure Go; no CGO required since we use modernc.org/sqlite). bsdaily: internal/*/*.go cmd/bsdaily/*.go - CGO_ENABLED=0 go build -o $@ ./cmd/bsdaily + CGO_ENABLED=0 go build -ldflags "-X main.Version=$(or $(VERSION),dev)" -o $@ ./cmd/bsdaily # Clean build artifacts. clean: diff --git a/TODO.md b/TODO.md index 611a0b9..a48b75f 100644 --- a/TODO.md +++ b/TODO.md @@ -15,11 +15,15 @@ pre-1.0 # Next Step Bring the repo into policy compliance in one commit: add .gitignore, -.dockerignore, .editorconfig, and .golangci.yml. Verify `make check` stays -green with the new lint config. +.editorconfig, and .golangci.yml. Verify `make check` stays green with the new +lint config. # Completed Steps +- 2026-10-02: A plain `docker build .` and a host `make` build stamp the git tag + or short commit into the binary, which `bsdaily` logs on the first line of + every run and prints with `--version`; added the canonical `.dockerignore`, + which keeps `.git/config` out of the build context. - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile shims, README Entrypoints section - 2026-06-28: Fixed errcheck lint failures; added compilation smoke test; @@ -36,8 +40,7 @@ green with the new lint config. # Future Steps -- Add .gitignore, .dockerignore, .editorconfig, .golangci.yml (the Next - Step). +- Add .gitignore, .editorconfig, .golangci.yml (the Next Step). - Expand tests beyond the compilation smoke test: unit tests for the extraction, verification, and atomic-publish paths. - Cut a first SemVer release once compliance and test coverage land. diff --git a/cmd/bsdaily/main.go b/cmd/bsdaily/main.go index 7d5409e..142493d 100644 --- a/cmd/bsdaily/main.go +++ b/cmd/bsdaily/main.go @@ -10,6 +10,11 @@ import ( "github.com/spf13/cobra" ) +// Version is the git tag or short commit, set at link time with -X by the +// Dockerfile and the Makefile, logged on the first line of every run and +// printed by --version. Builds that do not set it report dev. +var Version = "dev" + func main() { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ Level: slog.LevelInfo, @@ -23,8 +28,11 @@ func main() { rootCmd := &cobra.Command{ Use: "bsdaily", Short: "Extract a single day's data from the latest daily snapshot", + Version: Version, SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { + slog.Info("starting", "version", Version) + hasDate := dateFlag != "" hasFrom := fromFlag != "" hasTo := toFlag != "" diff --git a/script/docker b/script/docker index 2884e41..07b626c 100755 --- a/script/docker +++ b/script/docker @@ -1,7 +1,8 @@ #!/bin/sh # script/docker: build the Docker image tagged with the project name. # Identical in all repos; the tag comes from script/projectname. -# Generic: needs no adaptation. +# --no-cache because the gate phases the final stage depends on are RUN +# steps, and a cached one is a check that did not run. set -eu SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" @@ -9,7 +10,15 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)" main() { cd "$ROOT" - docker build -t "$("$SCRIPT_DIR/projectname")" . + # Own line: a failing command substitution inside an argument does + # not trip `set -e`, so the inline form degrades silently to an + # empty constant. The VERSION build argument takes precedence over + # the version a build stage derives from the .git in the context. + version="$(git describe --tags --always --dirty 2>/dev/null || true)" + [ -n "$version" ] || version="unknown" + docker build --no-cache \ + --build-arg VERSION="$version" \ + -t "$("$SCRIPT_DIR/projectname")" . } main "$@"