Report the version: docker build . stamps the git tag or short commit (closes #7)
check / check (push) Waiting to run

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
This commit was merged in pull request #8.
This commit is contained in:
2026-10-02 11:02:38 +02:00
parent 7f75f2ee72
commit 9f3af05c91
7 changed files with 111 additions and 7 deletions
+63
View File
@@ -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
+4
View File
@@ -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
+15 -1
View File
@@ -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
+6 -1
View File
@@ -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
+5 -2
View File
@@ -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
+7 -2
View File
@@ -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
+11 -1
View File
@@ -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 "$@"