From 1a156c9f3fba1ea54822a82aa6dee9215b7ea2d2 Mon Sep 17 00:00:00 2001 From: clawbot Date: Fri, 2 Oct 2026 07:06:29 +0000 Subject: [PATCH] Report the version: docker build . stamps the git tag or short commit (closes #7) attrsum --version now prints the git tag or short commit. make build stamps it with -X from git describe, and the Dockerfile takes the VERSION build argument when given, otherwise git describe --tags --always on the .git in the build context, failing if .git is present and no version comes out. A new .dockerignore, the canonical one, keeps .git/config out of the context, and also this repo's host-built /attrsum. CI checks out full history so it sees the 1.0.0 tag and stamps what a full clone does. script/docker is replaced with the canonical copy. Model: opus-5-5 --- .dockerignore | 63 ++++++++++++++++++++++++++++++++++++++ .gitea/workflows/check.yml | 4 +++ Dockerfile | 16 +++++++++- Makefile | 7 ++++- TODO.md | 7 +++-- attrsum.go | 9 ++++-- script/docker | 12 +++++++- 7 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4f8abc8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,63 @@ +# .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 binary (`make build`); the image builds its own. +/attrsum diff --git a/.gitea/workflows/check.yml b/.gitea/workflows/check.yml index a55bc55..bd2f014 100644 --- a/.gitea/workflows/check.yml +++ b/.gitea/workflows/check.yml @@ -6,4 +6,8 @@ jobs: steps: # actions/checkout v4.2.2, 2026-02-28 - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + # Full history and tags, so the build stamps the same + # `git describe` version as a full clone. + with: + fetch-depth: 0 - run: script/cibuild diff --git a/Dockerfile b/Dockerfile index d1530bf..f584e84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,21 @@ RUN make check # Build the binary (still as the unprivileged user: it owns /src, so git VCS # stamping sees consistent ownership). -RUN make build +# +# 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 "version is '$version' although .git is present" >&2; \ + exit 1; \ + fi; \ + make build VERSION="${version:-dev}" # Runtime stage # alpine 3.21, 2026-02-28 diff --git a/Makefile b/Makefile index 75c062e..22d77ab 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,11 @@ TESTDIR := $(HOME)/Documents/_SYSADMIN/cyberdyne +# The version `make build` stamps into the binary: the git tag or short +# commit, -dirty with uncommitted changes. `make build VERSION=x` stamps x, +# which is how the Dockerfile passes its version in. +VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev) + # Standard targets are thin shims; the implementations live in script/ # per the scripts-to-rule-them-all pattern. @@ -35,7 +40,7 @@ hooks: @script/install-precommit build: clean - @go build . + @go build -ldflags "-X main.Version=$(VERSION)" . clean: @rm -f attrsum diff --git a/TODO.md b/TODO.md index 3a9a45e..4a231e5 100644 --- a/TODO.md +++ b/TODO.md @@ -26,6 +26,11 @@ $(HOME)/Documents/_SYSADMIN/cyberdyne path. # Completed Steps +* 2026-10-02: `attrsum --version` reports the git tag or short commit, + stamped by `make build` and by a plain `docker build .` of a clone; + `.dockerignore` sends `.git` without `.git/config` and keeps a + host-built `attrsum` out; CI checks out full history so it stamps + the same version * 2026-02-02: correctness pass: track actual bytes read instead of stale file size, atomic failure tracking in ProcessCheck, detect file modification during checksum (TOCTOU), propagate countFiles @@ -39,8 +44,6 @@ $(HOME)/Documents/_SYSADMIN/cyberdyne path. # Future Steps -* Add Dockerfile and .dockerignore that run make check, images pinned - by sha256, plus a Makefile docker target * Add .gitea/workflows/check.yml * Restructure README.md into the standard sections: Description, Getting Started, Rationale, Design, TODO, License, Author (Getting diff --git a/attrsum.go b/attrsum.go index e9b0e1c..fce1043 100644 --- a/attrsum.go +++ b/attrsum.go @@ -36,6 +36,10 @@ const ( progressThrottle = 250 * time.Millisecond ) +// Version is the git tag or short commit, set at link time with -X by +// `make build`. A build that does not set it reports dev. +var Version = "dev" //nolint:gochecknoglobals // set at link time with -X + // Sentinel errors returned by the command implementations. var ( errNoPaths = errors.New("no paths provided") @@ -101,8 +105,9 @@ func main() { opts := &options{} rootCmd := &cobra.Command{ - Use: "attrsum", - Short: "Compute and verify file checksums via xattrs", + Use: "attrsum", + Short: "Compute and verify file checksums via xattrs", + Version: Version, } rootCmd.SilenceUsage = true rootCmd.SilenceErrors = true diff --git a/script/docker b/script/docker index 9b9ea86..07b626c 100755 --- a/script/docker +++ b/script/docker @@ -1,6 +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. +# --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)" @@ -8,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 "$@" -- 2.54.0