Author SHA1 Message Date
sneak a5d4cd6c13 Update golangci-lint to v2.12.2 with canonical config
check / check (push) Successful in 32s
- Replace .golangci.yml with the canonical v2-schema config: linter
  settings move under linters.settings (the previous top-level
  linters-settings block was ignored by golangci-lint v2, so the
  configured thresholds were not applied) and the obsolete
  issues.exclude-use-default key is dropped.
- Bump golangci-lint from the v2.10.1-era commit pin to @v2.12.2 in
  Dockerfile and script/bootstrap; refresh pin date comments.
- Wrap long lines in attrsum.go to satisfy the now-effective lll
  limit of 88 columns (15 findings); move one nolint:gosec directive
  to its own line.
- Record the change in TODO.md Completed Steps.
2026-08-07 17:11:06 +00:00
8 changed files with 109 additions and 176 deletions
-60
View File
@@ -1,60 +0,0 @@
# .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-*
+14 -12
View File
@@ -1,5 +1,9 @@
version: "2"
# Config schema uses the golangci-lint v2 layout (settings live under
# linters.settings, not top-level linters-settings) so that the
# thresholds below are actually applied by golangci-lint >= v2.
run:
timeout: 5m
modules-download-mode: readonly
@@ -14,19 +18,17 @@ linters:
- wsl # Deprecated, replaced by wsl_v5
- wrapcheck # Too verbose for internal packages
- varnamelen # Short names like db, id are idiomatic Go
linters-settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
+3 -17
View File
@@ -4,8 +4,8 @@ FROM golang@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4
RUN apk add --no-cache git make gcc musl-dev binutils-gold
# golangci-lint v2.10.1
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee
# golangci-lint v2.12.2, 2026-08-07
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
# goimports v0.42.0
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0
@@ -29,21 +29,7 @@ RUN make check
# Build the binary (still as the unprivileged user: it owns /src, so git VCS
# stamping sees consistent ownership).
#
# 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; \
make build VERSION="${version:-dev}"
RUN make build
# Runtime stage
# alpine 3.21, 2026-02-28
+1 -6
View File
@@ -2,11 +2,6 @@
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.
@@ -40,7 +35,7 @@ hooks:
@script/install-precommit
build: clean
@go build -ldflags "-X main.Version=$(VERSION)" .
@go build .
clean:
@rm -f attrsum
+5 -3
View File
@@ -26,9 +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`
* 2026-08-07: updated golangci-lint to v2.12.2: canonical
`.golangci.yml` (settings moved under `linters.settings` so the
configured thresholds actually apply), version pins bumped in
`Dockerfile` and `script/bootstrap`, and long lines wrapped to
satisfy the now-effective `lll` limit of 88
* 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
+81 -63
View File
@@ -36,10 +36,6 @@ 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")
@@ -76,7 +72,8 @@ func (s *Stats) Print(opts *options, operation string) {
return
}
fmt.Fprintf(os.Stderr, "\n%s complete: %d files processed, %d skipped, %d failed, %s bytes in %s\n",
fmt.Fprintf(os.Stderr,
"\n%s complete: %d files processed, %d skipped, %d failed, %s bytes in %s\n",
operation,
s.FilesProcessed,
s.FilesSkipped,
@@ -105,9 +102,8 @@ func main() {
opts := &options{}
rootCmd := &cobra.Command{
Use: "attrsum",
Short: "Compute and verify file checksums via xattrs",
Version: Version,
Use: "attrsum",
Short: "Compute and verify file checksums via xattrs",
}
rootCmd.SilenceUsage = true
rootCmd.SilenceErrors = true
@@ -171,11 +167,15 @@ func expandPaths(args []string) ([]string, error) {
}
// processFunc processes a single path within a command's run loop.
type processFunc func(opts *options, path string, stats *Stats, bar *progressbar.ProgressBar) error
type processFunc func(
opts *options, path string, stats *Stats, bar *progressbar.ProgressBar,
) error
// countAndBar counts the files under paths and returns a progress bar sized
// to that total. It always returns either a non-nil bar or a non-nil error.
func countAndBar(opts *options, paths []string, desc string) (*progressbar.ProgressBar, error) {
func countAndBar(
opts *options, paths []string, desc string,
) (*progressbar.ProgressBar, error) {
total, err := countFilesMultiple(opts, paths)
if err != nil {
return nil, err
@@ -193,7 +193,9 @@ func finishBar(bar *progressbar.ProgressBar) {
// runOverPaths runs process over each path, sharing the progress/stats
// bookkeeping common to the sum-add, sum-update and clear commands.
func runOverPaths(opts *options, args []string, desc, op string, process processFunc) error {
func runOverPaths(
opts *options, args []string, desc, op string, process processFunc,
) error {
paths, err := expandPaths(args)
if err != nil {
return err
@@ -258,46 +260,54 @@ func newSumCmd(opts *options) *cobra.Command {
return cmd
}
func processSumAdd(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error {
return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
if hasXattr(p, checksumKey) {
atomic.AddInt64(&s.FilesSkipped, 1)
func processSumAdd(
opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar,
) error {
return walkAndProcess(opts, dir, stats, bar,
func(p string, info os.FileInfo, s *Stats) error {
if hasXattr(p, checksumKey) {
atomic.AddInt64(&s.FilesSkipped, 1)
return nil
}
return nil
}
err := writeChecksumAndTime(opts, p, info, s)
if err != nil {
atomic.AddInt64(&s.FilesFailed, 1)
return err
}
return nil
})
}
func processSumUpdate(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error {
return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
t, err := readSumTime(p)
if err != nil || info.ModTime().After(t) {
werr := writeChecksumAndTime(opts, p, info, s)
if werr != nil {
err := writeChecksumAndTime(opts, p, info, s)
if err != nil {
atomic.AddInt64(&s.FilesFailed, 1)
return werr
return err
}
return nil
}
atomic.AddInt64(&s.FilesSkipped, 1)
return nil
})
})
}
func writeChecksumAndTime(opts *options, path string, info os.FileInfo, stats *Stats) error {
func processSumUpdate(
opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar,
) error {
return walkAndProcess(opts, dir, stats, bar,
func(p string, info os.FileInfo, s *Stats) error {
t, err := readSumTime(p)
if err != nil || info.ModTime().After(t) {
werr := writeChecksumAndTime(opts, p, info, s)
if werr != nil {
atomic.AddInt64(&s.FilesFailed, 1)
return werr
}
return nil
}
atomic.AddInt64(&s.FilesSkipped, 1)
return nil
})
}
func writeChecksumAndTime(
opts *options, path string, info os.FileInfo, stats *Stats,
) error {
// Record mtime before hashing to detect modifications during hash.
mtimeBefore := info.ModTime()
@@ -368,24 +378,27 @@ func newClearCmd(opts *options) *cobra.Command {
}
}
func processClear(opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar) error {
return walkAndProcess(opts, dir, stats, bar, func(p string, info os.FileInfo, s *Stats) error {
cleared, err := clearOne(opts, p)
if err != nil {
atomic.AddInt64(&s.FilesFailed, 1)
func processClear(
opts *options, dir string, stats *Stats, bar *progressbar.ProgressBar,
) error {
return walkAndProcess(opts, dir, stats, bar,
func(p string, info os.FileInfo, s *Stats) error {
cleared, err := clearOne(opts, p)
if err != nil {
atomic.AddInt64(&s.FilesFailed, 1)
return err
}
return err
}
if cleared {
atomic.AddInt64(&s.FilesProcessed, 1)
atomic.AddInt64(&s.BytesProcessed, info.Size())
} else {
atomic.AddInt64(&s.FilesSkipped, 1)
}
if cleared {
atomic.AddInt64(&s.FilesProcessed, 1)
atomic.AddInt64(&s.BytesProcessed, info.Size())
} else {
atomic.AddInt64(&s.FilesSkipped, 1)
}
return nil
})
return nil
})
}
// clearOne removes both checksum xattrs from a single path, reporting
@@ -433,7 +446,8 @@ func newCheckCmd(opts *options) *cobra.Command {
return runCheck(opts, a, cont)
},
}
cmd.Flags().BoolVar(&cont, "continue", false, "continue after errors and report each file")
cmd.Flags().BoolVar(&cont, "continue", false,
"continue after errors and report each file")
return cmd
}
@@ -477,13 +491,16 @@ func runCheck(opts *options, args []string, cont bool) error {
return finalErr
}
func processCheck(opts *options, dir string, cont bool, stats *Stats, bar *progressbar.ProgressBar) error {
func processCheck(
opts *options, dir string, cont bool, stats *Stats, bar *progressbar.ProgressBar,
) error {
// Track initial failed count to detect failures during this walk.
initialFailed := atomic.LoadInt64(&stats.FilesFailed)
err := walkAndProcess(opts, dir, stats, bar, func(p string, _ os.FileInfo, s *Stats) error {
return checkOne(opts, p, cont, s)
})
err := walkAndProcess(opts, dir, stats, bar,
func(p string, _ os.FileInfo, s *Stats) error {
return checkOne(opts, p, cont, s)
})
if err != nil {
if errors.Is(err, errVerification) {
return errVerification
@@ -747,7 +764,8 @@ func hasXattr(path, key string) bool {
func fileMultihash(path string) ([]byte, int64, error) {
// The path is supplied by the operator as the tree to checksum; reading
// it is the entire purpose of the tool.
f, err := os.Open(path) //nolint:gosec // G304: operator-specified path is the intended input
//nolint:gosec // G304: operator-specified path is the intended input
f, err := os.Open(path)
if err != nil {
return nil, 0, err
}
+4 -4
View File
@@ -4,14 +4,14 @@
# installed tools are skipped. Base tooling comes from nix, apt, brew,
# or apk (detected in that order); assumes nothing is present.
# golangci-lint and goimports are installed via `go install` at the same
# pinned commits the Dockerfile uses (never "latest").
# pinned refs the Dockerfile uses (never "latest").
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Pinned versions, 2026-07-07 (same pins as the Dockerfile)
# golangci-lint v2.10.1
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@5d1e709b7be35cb2025444e19de266b056b7b7ee"
# Pinned versions, 2026-08-07 (same pins as the Dockerfile)
# golangci-lint v2.12.2
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2"
# goimports v0.42.0
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"
+1 -11
View File
@@ -1,8 +1,6 @@
#!/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)"
@@ -10,15 +8,7 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
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. 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")" .
docker build -t "$("$SCRIPT_DIR/projectname")" .
}
main "$@"