Compare commits

...
2 Commits
Author SHA1 Message Date
clawbot 15f0c263c8 Make the golangci-lint settings take effect: v2 config layout, linter pinned at v2.12.2 (closes #10)
check / check (push) Successful in 3m27s
golangci-lint v2 ignores a top-level linters-settings block without a
warning, so limits such as the 88-column line length were never
applied. .golangci.yml is now the canonical copy from sneak/prompts,
verbatim, which keeps its settings under linters.settings and also
configures depguard and gomodguard_v2. golangci-lint is pinned at
v2.12.2 by commit in the Dockerfile and script/bootstrap. The long
lines in attrsum.go are rewrapped and one nolint directive moves to
its own line; behaviour is unchanged.

Model: opus-5-5
2026-10-06 00:00:51 +00:00
clawbot 6a0dfa9f2c Run make try against a temporary directory (closes #12)
check / check (push) Successful in 3m36s
`make try` ran against a fixed directory on one person's machine. It
failed for anyone else, and where that directory existed it rewrote the
checksum attributes and modification times of real files.

It now writes three small files into a new directory from `mktemp -d`,
runs the same sequence of commands on it, and removes it with an EXIT
trap. All steps run in one shell, so the trap also removes the
directory when a step fails.

`TODO.md`: the `try` sentence leaves Next Step and is recorded under
Completed Steps.

Model: opus-5-5
2026-10-06 01:59:55 +02:00
6 changed files with 191 additions and 89 deletions
+80 -14
View File
@@ -1,32 +1,98 @@
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
linters:
default: all
enable:
# Successor to the deprecated gomodguard. Named explicitly, rather than
# left to `default: all`, because it carries the module policy below.
- gomodguard_v2
disable:
# Genuinely incompatible with project patterns
- exhaustruct # Requires all struct fields
- depguard # Dependency allow/block lists
- godot # Requires comments to end with periods
- 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
# Deprecated: the warning is attached to the old name, so it is
# silenced by disabling that name, not by enabling the successor.
- wsl # Deprecated, replaced by wsl_v5
- gomodguard # Deprecated, replaced by gomodguard_v2
settings:
lll:
line-length: 88
funlen:
lines: 80
statements: 50
cyclop:
max-complexity: 15
dupl:
threshold: 100
depguard:
# Test-support code must not be compiled into the shipped binary. A
# test-support package exists to hand a test privileges the program
# itself must never have, so a file that is not a test must not import
# one. Test files, and the files inside a package whose directory name
# ends in `test`, are where that code belongs, and are exempt.
#
# The deny list below is the one part of this file a repository is
# expected to extend, and the only part it may. depguard matches an
# import path against a list of prefixes, so it cannot be told "any path
# whose last segment ends in test"; a repository's own test-support
# packages have to be named here one at a time, by full import path,
# under a module path that differs from repository to repository. Add
# them; change nothing else.
rules:
test-support:
list-mode: lax
files:
- "$all"
- "!$test"
- "!**/*test/**"
deny:
- pkg: net/http/httptest
desc: >-
Test-support code belongs in test files and in packages whose
directory name ends in test, not in the shipped binary.
# Only decisions already recorded in the Go package defaults are
# listed here. Every entry matches the module path exactly.
gomodguard_v2:
blocked:
- module: github.com/rs/zerolog
recommendations:
- log/slog
reason: "Structured logging is stdlib log/slog."
# One entry per pre-fork module path, because the later releases
# are separate paths. A prefix match would be shorter but would
# also reach github.com/go-redis/redismock, the test double for
# the successor these entries recommend.
- module: github.com/go-redis/redis
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/go-redis/redis/v7
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/go-redis/redis/v8
recommendations:
- github.com/redis/go-redis/v9
reason: "Pre-fork module; use the maintained go-redis v9."
- module: github.com/sergi/go-diff
recommendations:
- github.com/aymanbagabas/go-udiff
reason: "No unified diff output; use go-udiff."
- module: github.com/hexops/gotextdiff
recommendations:
- github.com/aymanbagabas/go-udiff
reason: "Unmaintained fork; use go-udiff."
issues:
exclude-use-default: false
max-issues-per-linter: 0
max-same-issues: 0
+2 -2
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-10-05
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5
# goimports v0.42.0
RUN go install golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0
+18 -11
View File
@@ -1,7 +1,5 @@
.PHONY: default bootstrap setup test lint fmt fmt-check check docker hooks build clean try
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.
@@ -45,13 +43,22 @@ build: clean
clean:
@rm -f attrsum
# Runs the binary on three small files in a new temporary directory, which
# the trap removes when the recipe ends, also when a step fails. The check
# after clear is expected to fail, so its exit status is ignored.
try: build
./attrsum sum add -v $(TESTDIR)
./attrsum check -v $(TESTDIR)
./attrsum clear -v $(TESTDIR)
-./attrsum check -v $(TESTDIR)
./attrsum sum add -v $(TESTDIR)
./attrsum check -v $(TESTDIR)
touch $(TESTDIR)/*
./attrsum sum update -v $(TESTDIR)
./attrsum check -v $(TESTDIR)
@set -ex; \
dir=$$(mktemp -d); \
trap 'rm -rf "$$dir"' EXIT; \
echo one >"$$dir/a"; \
echo two >"$$dir/b"; \
echo three >"$$dir/c"; \
./attrsum sum add -v "$$dir"; \
./attrsum check -v "$$dir"; \
./attrsum clear -v "$$dir"; \
./attrsum check -v "$$dir" || true; \
./attrsum sum add -v "$$dir"; \
./attrsum check -v "$$dir"; \
touch "$$dir"/*; \
./attrsum sum update -v "$$dir"; \
./attrsum check -v "$$dir"
+9 -3
View File
@@ -20,12 +20,18 @@ have landed since the tag.
Policy scaffold commit: add LICENSE, REPO_POLICIES.md, .editorconfig,
.golangci.yml, and a comprehensive .gitignore (currently only the
attrsum binary), and extend the Makefile (only test/build/clean/try
today) with lint, fmt, fmt-check, check, and hooks targets. Fix the try
target to use a temp fixture instead of the hardcoded
$(HOME)/Documents/_SYSADMIN/cyberdyne path.
today) with lint, fmt, fmt-check, check, and hooks targets.
# Completed Steps
* 2026-10-05: golangci-lint settings take effect: canonical
`.golangci.yml` (v2 layout, settings under `linters.settings`),
golangci-lint pinned at v2.12.2 in `Dockerfile` and
`script/bootstrap`, and the code fixed for what the settings now
report (long lines in `attrsum.go` rewrapped)
* 2026-10-05: `make try` runs on three small files in a temporary
directory that it removes afterwards, also when a step fails,
instead of on a fixed directory on one person's machine
* 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
+79 -56
View File
@@ -76,7 +76,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,
@@ -171,11 +172,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 +198,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 +265,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 +383,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 +451,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 +496,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 +769,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
}
+3 -3
View File
@@ -9,9 +9,9 @@ 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-10-05 (same pins as the Dockerfile)
# golangci-lint v2.12.2
GOLANGCI_LINT_REF="github.com/golangci/golangci-lint/v2/cmd/golangci-lint@c0d3ddc9cf3faa61a4e378e879ece580256d76e5"
# goimports v0.42.0
GOIMPORTS_REF="golang.org/x/tools/cmd/goimports@009367f5c17a8d4c45a961a3a509277190a9a6f0"